Commit cb034d4f by Uman Shahzad

Implement client-side registration form validation.

Input forms that need validation will have AJAX requests
performed to get validation decisions live.

All but a few important and common form fields perform
generic validation; these will need a back-end handler
in the future in order to have them validated through AJAX requests.

Information is conveyed on focus and blur for both
errors and successes.
parent 39ac333b
......@@ -22,7 +22,9 @@ from notification_prefs import NOTIFICATION_PREF_KEY
from openedx.core.djangoapps.external_auth.models import ExternalAuthMap
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.core.djangoapps.user_api.accounts import USERNAME_INVALID_CHARS_ASCII, USERNAME_INVALID_CHARS_UNICODE
from openedx.core.djangoapps.user_api.accounts import (
USERNAME_BAD_LENGTH_MSG, USERNAME_INVALID_CHARS_ASCII, USERNAME_INVALID_CHARS_UNICODE
)
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from student.models import UserAttribute
from student.views import REGISTRATION_AFFILIATE_ID, REGISTRATION_UTM_CREATED_AT, REGISTRATION_UTM_PARAMETERS
......@@ -476,16 +478,16 @@ class TestCreateAccountValidation(TestCase):
# Missing
del params["username"]
assert_username_error("Username must be minimum of two characters long")
assert_username_error(USERNAME_BAD_LENGTH_MSG)
# Empty, too short
for username in ["", "a"]:
params["username"] = username
assert_username_error("Username must be minimum of two characters long")
assert_username_error(USERNAME_BAD_LENGTH_MSG)
# Too long
params["username"] = "this_username_has_31_characters"
assert_username_error("Username cannot be more than 30 characters long")
assert_username_error(USERNAME_BAD_LENGTH_MSG)
# Invalid
params["username"] = "invalid username"
......
......@@ -5,6 +5,8 @@ import json
from django.core.urlresolvers import reverse
from django.test import TestCase
from openedx.core.djangoapps.user_api.accounts import USERNAME_BAD_LENGTH_MSG
class TestLongUsernameEmail(TestCase):
......@@ -34,7 +36,7 @@ class TestLongUsernameEmail(TestCase):
obj = json.loads(response.content)
self.assertEqual(
obj['value'],
"Username cannot be more than 30 characters long",
USERNAME_BAD_LENGTH_MSG,
)
def test_long_email(self):
......
......@@ -21,7 +21,7 @@
var _fn = {
validate: {
template: _.template('<li><%= content %></li>'),
template: _.template('<li id="<%- id %>-validation-error-container"><%- content %></li>'),
msg: {
email: gettext("The email address you've provided isn't formatted correctly."),
......@@ -32,6 +32,7 @@
field: function(el) {
var $el = $(el),
id = $el.attr('id'),
required = true,
min = true,
max = true,
......@@ -66,6 +67,8 @@
});
}
response.id = id;
return response;
},
......@@ -107,7 +110,7 @@
regex: new RegExp(
[
'(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*',
'|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"',
'|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"', // eslint-disable-line max-len
')@((?:[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'
......@@ -124,7 +127,7 @@
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();
return $('label[for=' + id + '] > span.label-text').text().split('*')[0].trim();
},
getMessage: function($el, tests) {
......@@ -154,7 +157,10 @@
content = _.sprintf(_fn.validate.msg[key], context);
}
txt.push(_fn.validate.template({content: content}));
txt.push(_fn.validate.template({
content: content,
id: $el.attr('id')
}));
}
});
......@@ -173,7 +179,7 @@
return {
validate: _fn.validate.field
};
})();
}());
return utils;
});
......
......@@ -344,10 +344,7 @@ class RegisterFromCombinedPageTest(UniqueCourseTest):
# Verify that the expected errors are displayed.
errors = self.register_page.wait_for_errors()
self.assertIn(u'Please enter your Public Username.', errors)
self.assertIn(
u'You must agree to the édX Terms of Service and Honor Code',
errors
)
self.assertIn(u'You must agree to the édX Terms of Service and Honor Code', errors)
self.assertIn(u'Please select your Country.', errors)
self.assertIn(u'Please tell us your favorite movie.', errors)
......
......@@ -331,31 +331,6 @@ msgid "User profile"
msgstr ""
#: common/djangoapps/student/forms.py
msgid "Username must be minimum of two characters long"
msgstr ""
#: common/djangoapps/student/forms.py
#, python-format
msgid "Username cannot be more than %(limit_value)s characters long"
msgstr ""
#. Translators: This message is shown when the Unicode usernames are NOT
#. allowed
#: common/djangoapps/student/forms.py
msgid ""
"Usernames can only contain Roman letters, western numerals (0-9), "
"underscores (_), and hyphens (-)."
msgstr ""
#. Translators: This message is shown only when the Unicode usernames are
#. allowed
#: common/djangoapps/student/forms.py
msgid ""
"Usernames can only contain letters, numerals, underscore (_), numbers and "
"@/./+/-/_ characters."
msgstr ""
#: common/djangoapps/student/forms.py
msgid ""
"That e-mail address doesn't have an associated user account. Are you sure "
"you've registered?"
......@@ -9288,23 +9263,83 @@ msgstr ""
msgid "Enable course home page improvements."
msgstr ""
#: openedx/core/djangoapps/theming/views.py
#: openedx/core/djangoapps/user_api/accounts/__init__.py
msgid ""
"Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores "
"(_), and hyphens (-)."
msgstr ""
#: openedx/core/djangoapps/user_api/accounts/__init__.py
msgid ""
"Usernames can only contain letters, numerals, and @/./+/-/_ characters."
msgstr ""
#. Translators: This message is shown to users who attempt to create a new
#. account using
#. an invalid email format.
#: openedx/core/djangoapps/user_api/accounts/__init__.py
#, python-brace-format
msgid "Site theme changed to {site_theme}"
msgid "\"{email}\" is not a valid email address."
msgstr ""
#: openedx/core/djangoapps/theming/views.py
#: openedx/core/djangoapps/user_api/accounts/__init__.py
#, python-brace-format
msgid "Theme {site_theme} does not exist"
msgid ""
"It looks like {email_address} belongs to an existing account. Try again with"
" a different email address."
msgstr ""
#: openedx/core/djangoapps/theming/views.py
msgid "Site theme reverted to the default"
#: openedx/core/djangoapps/user_api/accounts/__init__.py
#, python-brace-format
msgid ""
"It looks like {username} belongs to an existing account. Try again with a "
"different username."
msgstr ""
#: openedx/core/djangoapps/theming/views.py
#: openedx/core/djangoapps/theming/templates/theming/theming-admin-fragment.html
msgid "Theming Administration"
#. Translators: This message is shown to users who enter a
#. username/email/password
#. with an inappropriate length (too short or too long).
#: openedx/core/djangoapps/user_api/accounts/__init__.py
#, python-brace-format
msgid "Username must be between {min} and {max} characters long."
msgstr ""
#: openedx/core/djangoapps/user_api/accounts/__init__.py
#, python-brace-format
msgid "Enter a valid email address that contains at least {min} characters."
msgstr ""
#: openedx/core/djangoapps/user_api/accounts/__init__.py
msgid "Please enter a password."
msgstr ""
#: openedx/core/djangoapps/user_api/accounts/__init__.py
msgid "Password is not long enough."
msgstr ""
#: openedx/core/djangoapps/user_api/accounts/__init__.py
#, python-brace-format
msgid "Password cannot be longer than {max} character."
msgstr ""
#. Translators: This message is shown to users who enter a password matching
#. the username they enter(ed).
#: openedx/core/djangoapps/user_api/accounts/__init__.py
msgid "Password cannot be the same as the username."
msgstr ""
#. Translators: These messages are shown to users who do not enter information
#. into the required field or enter it incorrectly.
#: openedx/core/djangoapps/user_api/accounts/__init__.py
msgid "Please enter your Full Name."
msgstr ""
#: openedx/core/djangoapps/user_api/accounts/__init__.py
msgid "The email addresses do not match."
msgstr ""
#: openedx/core/djangoapps/user_api/accounts/__init__.py
msgid "Please select your Country."
msgstr ""
#: openedx/core/djangoapps/user_api/accounts/api.py
......@@ -9392,24 +9427,6 @@ msgstr ""
msgid "Remember me"
msgstr ""
#. Translators: This message is shown to users who attempt to create a new
#. account using an email address associated with an existing account.
#: openedx/core/djangoapps/user_api/views.py
#, python-brace-format
msgid ""
"It looks like {email_address} belongs to an existing account. Try again with"
" a different email address."
msgstr ""
#. Translators: This message is shown to users who attempt to create a new
#. account using a username associated with an existing account.
#: openedx/core/djangoapps/user_api/views.py
#, python-brace-format
msgid ""
"It looks like {username} belongs to an existing account. Try again with a "
"different username."
msgstr ""
#. Translators: These instructions appear on the registration form,
#. immediately
#. below a field meant to hold the user's email address.
......@@ -9423,10 +9440,6 @@ msgstr ""
msgid "Confirm Email"
msgstr ""
#: openedx/core/djangoapps/user_api/views.py
msgid "The email addresses do not match."
msgstr ""
#. Translators: This example name is used as a placeholder in
#. a field on the registration form meant to hold the user's name.
#: openedx/core/djangoapps/user_api/views.py
......@@ -9506,10 +9519,6 @@ msgid "Company"
msgstr ""
#: openedx/core/djangoapps/user_api/views.py
msgid "Please select your Country."
msgstr ""
#: openedx/core/djangoapps/user_api/views.py
msgid "Review the Honor Code"
msgstr ""
......
......@@ -4219,6 +4219,10 @@ msgid "We couldn't create your account."
msgstr ""
#: lms/static/js/student_account/views/RegisterView.js
msgid "(required)"
msgstr ""
#: lms/static/js/student_account/views/RegisterView.js
msgid "You've successfully signed into %(currentProvider)s."
msgstr ""
......
......@@ -79,6 +79,7 @@
var buildIframe = function(link, modalSelector, contentSelector, tosLinkSelector) {
// Create an iframe with contents from the link and set its height to match the content area
return $('<iframe>', {
title: 'Terms of Service and Honor Code',
src: link.href,
load: function() {
var $iframeHead = $(this).contents().find('head'),
......
......@@ -6,43 +6,30 @@
'backbone',
'common/js/utils/edx.utils.validate',
'edx-ui-toolkit/js/utils/html-utils',
'edx-ui-toolkit/js/utils/string-utils',
'text!templates/student_account/form_errors.underscore'
],
function($, _, Backbone, EdxUtilsValidate, HtmlUtils, formErrorsTpl) {
], function($, _, Backbone, EdxUtilsValidate, HtmlUtils, StringUtils, formErrorsTpl) {
return Backbone.View.extend({
tagName: 'form',
el: '',
tpl: '',
fieldTpl: '#form_field-tpl',
formErrorsTpl: formErrorsTpl,
formErrorsJsHook: 'js-form-errors',
defaultFormErrorsTitle: gettext('An error occurred.'),
events: {},
errors: [],
formType: '',
$form: {},
fields: [],
liveValidationFields: [],
// String to append to required label fields
requiredStr: '',
/*
Translators: This string is appended to optional field labels on the student login, registration, and
profile forms.
Translators: This string is appended to optional field labels on the student login, registration, and
profile forms.
*/
optionalStr: gettext('(optional)'),
submitButton: '',
initialize: function(data) {
......@@ -157,7 +144,7 @@
$label,
key = '',
errors = [],
test = {};
validation = {};
for (i = 0; i < len; i++) {
$el = $(elements[i]);
......@@ -171,13 +158,13 @@
}
if (key) {
test = this.validate(elements[i]);
if (test.isValid) {
validation = this.validate(elements[i]);
if (validation.isValid) {
obj[key] = $el.attr('type') === 'checkbox' ? $el.is(':checked') : $el.val();
$el.removeClass('error');
$label.removeClass('error');
} else {
errors.push(test.message);
errors.push(validation.message);
$el.addClass('error');
$label.addClass('error');
}
......@@ -190,7 +177,13 @@
},
saveError: function(error) {
this.errors = ['<li>' + error.responseText + '</li>'];
this.errors = [
StringUtils.interpolate(
'<li>{error}</li>', {
error: error.responseText
}
)
];
this.renderErrors(this.defaultFormErrorsTitle, this.errors);
this.toggleDisableButton(false);
},
......@@ -201,24 +194,48 @@
renderErrors: function(title, errorMessages) {
this.clearFormErrors();
this.renderFormFeedback(this.formErrorsTpl, {
jsHook: this.formErrorsJsHook,
title: title,
messagesHtml: HtmlUtils.HTML(errorMessages.join(''))
});
if (title || errorMessages.length) {
this.renderFormFeedback(this.formErrorsTpl, {
jsHook: this.formErrorsJsHook,
title: title,
messagesHtml: HtmlUtils.HTML(errorMessages.join(''))
});
}
},
renderFormFeedback: function(template, context) {
var tpl = HtmlUtils.template(template);
HtmlUtils.prepend(this.$formFeedback, tpl(context));
},
// Scroll to feedback container
$('html,body').animate({
scrollTop: this.$formFeedback.offset().top
}, 'slow');
doOnErrorList: function(id, action) {
var i;
for (i = 0; i < this.errors.length; ++i) {
if (this.errors[i].includes(id)) {
action(i);
}
}
},
updateError: function(error, id) {
this.deleteError(id);
this.addError(error, id);
},
deleteError: function(id) {
var self = this;
this.doOnErrorList(id, function(index) {
self.errors.splice(index, 1);
});
},
// Focus on the feedback container to ensure screen readers see the messages.
this.$formFeedback.focus();
addError: function(error, id) {
this.errors.push(StringUtils.interpolate(
'<li id="{errorId}">{error}</li>', {
errorId: id,
error: error
}
));
},
/* Allows extended views to add non-form attributes
......@@ -244,6 +261,14 @@
this.clearFormErrors();
} else {
this.renderErrors(this.defaultFormErrorsTitle, this.errors);
// Scroll to feedback container
$('html,body').animate({
scrollTop: this.$formFeedback.offset().top
}, 'slow');
// Focus on the feedback container to ensure screen readers see the messages.
this.$formFeedback.focus();
this.toggleDisableButton(false);
}
......@@ -285,6 +310,29 @@
validate: function($el) {
return EdxUtilsValidate.validate($el);
},
liveValidate: function($el, url, dataType, data, method, model) {
$.ajax({
url: url,
dataType: dataType,
data: data,
method: method,
success: function(response) {
model.trigger('validation', $el, response);
}
});
},
inLiveValidationFields: function($el) {
var i,
name = $el.attr('name') || false;
for (i = 0; i < this.liveValidationFields.length; ++i) {
if (this.liveValidationFields[i] === name) {
return true;
}
}
return false;
}
});
});
......
......@@ -296,10 +296,6 @@
display: inline;
}
&.error {
color: $red;
}
&[for="register-data_sharing_consent"],
&[for="register-honor_code"],
&[for="register-terms_of_service"] {
......@@ -365,7 +361,22 @@
}
&.error {
border-color: $error-color;
border-color: $red;
}
&.success {
border-color: $success-color-hover;
}
}
textarea,
select {
&.error {
outline-color: $red;
}
&.success {
outline-color: $success-color-hover;
}
}
......@@ -384,9 +395,16 @@
&:active, &:focus {
outline: auto;
}
}
span,
label {
&.error {
outline-color: $error-color;
color: $red;
}
&.success {
color: $success-color-hover;
}
}
......@@ -394,6 +412,7 @@
@extend %t-copy-sub1;
color: $uxpl-gray-base;
}
.tip {
display: block;
}
......
<div class="form-field <%=type%>-<%= name %>">
<div class="form-field <%- type %>-<%- name %>">
<% if ( type !== 'checkbox' ) { %>
<label for="<%= form %>-<%= name %>">
<span class="label-text"><%= label %></span>
<% if ( required && requiredStr && (type !== 'hidden') ) { %><span class="label-required"><%= requiredStr %></span><% } %>
<% if ( !required && optionalStr && (type !== 'hidden') ) { %><span class="label-optional"><%= optionalStr %></span><% } %>
<label for="<%- form %>-<%- name %>">
<span class="label-text"><%- label %></span>
<% if ( required && type !== 'hidden' ) { %>
<span id="<%- form %>-<%- name %>-required-label"
class="label-required <% if ( !requiredStr ) { %>hidden<% } %>">
<% if ( requiredStr ) { %><%- requiredStr %><% }%>
</span>
<span class="icon fa" id="<%- form %>-<%- name %>-validation-icon" aria-hidden="true"></span>
<% } %>
<% if ( !required && optionalStr && (type !== 'hidden') ) { %>
<span class="label-optional" id="<%- form %>-<%- name %>-optional-label"><%- optionalStr %></span>
<% } %>
</label>
<% if (supplementalLink && supplementalText) { %>
<div class="supplemental-link">
......@@ -13,50 +21,59 @@
<% } %>
<% if ( type === 'select' ) { %>
<select id="<%= form %>-<%= name %>"
name="<%= name %>"
<select id="<%- form %>-<%- name %>"
name="<%- name %>"
class="input-inline"
<% if ( instructions ) { %>
aria-describedby="<%= form %>-<%= name %>-desc"
aria-describedby="<%- form %>-<%- name %>-desc <%- form %>-<%- name %>-validation-error"
<% } %>
<% if ( typeof errorMessages !== 'undefined' ) {
_.each(errorMessages, function( msg, type ) {%>
data-errormsg-<%= type %>="<%= msg %>"
data-errormsg-<%- type %>="<%- msg %>"
<% });
} %>
<% if ( required ) { %> aria-required="true" required<% } %>>
<% _.each(options, function(el) { %>
<option value="<%= el.value%>"<% if ( el.default ) { %> data-isdefault="true" selected<% } %>><%= el.name %></option>
<% }); %>
<% if ( required ) { %> aria-required="true" required<% } %>
>
<% _.each(options, function(el) { %>
<option value="<%- el.value%>"<% if ( el.default ) { %> data-isdefault="true" selected<% } %>><%- el.name %></option>
<% }); %>
</select>
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
<span id="<%- form %>-<%- name %>-validation-error" class="tip error" aria-live="assertive">
<span class="sr-only">ERROR: </span>
<span id="<%- form %>-<%- name %>-validation-error-msg"></span>
</span>
<% if ( instructions ) { %> <span class="tip tip-input" id="<%- form %>-<%- name %>-desc"><%- instructions %></span><% } %>
<% if (supplementalLink && supplementalText) { %>
<div class="supplemental-link">
<a href="<%- supplementalLink %>" target="_blank"><%- supplementalText %></a>
</div>
<% } %>
<% } else if ( type === 'textarea' ) { %>
<textarea id="<%= form %>-<%= name %>"
type="<%= type %>"
name="<%= name %>"
<textarea id="<%- form %>-<%- name %>"
type="<%- type %>"
name="<%- name %>"
class="input-block"
<% if ( instructions ) { %>
aria-describedby="<%= form %>-<%= name %>-desc"
aria-describedby="<%- form %>-<%- name %>-desc <%- form %>-<%- name %>-validation-error"
<% } %>
<% if ( restrictions.min_length ) { %> minlength="<%= restrictions.min_length %>"<% } %>
<% if ( restrictions.max_length ) { %> maxlength="<%= restrictions.max_length %>"<% } %>
<% if ( restrictions.min_length ) { %> minlength="<%- restrictions.min_length %>"<% } %>
<% if ( restrictions.max_length ) { %> maxlength="<%- restrictions.max_length %>"<% } %>
<% if ( typeof errorMessages !== 'undefined' ) {
_.each(errorMessages, function( msg, type ) {%>
data-errormsg-<%= type %>="<%= msg %>"
data-errormsg-<%- type %>="<%- msg %>"
<% });
} %>
<% if ( required ) { %> aria-required="true" required<% } %> ></textarea>
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
<% if (supplementalLink && supplementalText) { %>
<div class="supplemental-link">
<a href="<%- supplementalLink %>" target="_blank"><%- supplementalText %></a>
</div>
<% } %>
<% if ( required ) { %> aria-required="true" required<% } %>></textarea>
<span id="<%- form %>-<%- name %>-validation-error" class="tip error" aria-live="assertive">
<span class="sr-only">ERROR: </span>
<span id="<%- form %>-<%- name %>-validation-error-msg"></span>
</span>
<% if ( instructions ) { %> <span class="tip tip-input" id="<%- form %>-<%- name %>-desc"><%- instructions %></span><% } %>
<% if (supplementalLink && supplementalText) { %>
<div class="supplemental-link">
<a href="<%- supplementalLink %>" target="_blank"><%- supplementalText %></a>
</div>
<% } %>
<% } else { %>
<% if ( type === 'checkbox' ) { %>
<% if (supplementalLink && supplementalText) { %>
......@@ -65,30 +82,44 @@
</div>
<% } %>
<% } %>
<input id="<%= form %>-<%= name %>"
type="<%= type %>"
name="<%= name %>"
<input id="<%- form %>-<%- name %>"
type="<%- type %>"
name="<%- name %>"
class="input-block <% if ( type === 'checkbox' ) { %>checkbox<% } %>"
<% if ( instructions ) { %> aria-describedby="<%= form %>-<%= name %>-desc" <% } %>
<% if ( restrictions.min_length ) { %> minlength="<%= restrictions.min_length %>"<% } %>
<% if ( restrictions.max_length ) { %> maxlength="<%= restrictions.max_length %>"<% } %>
<% if ( instructions ) { %>
aria-describedby="<%- form %>-<%- name %>-desc <%- form %>-<%- name %>-validation-error"
<% } %>
<% if ( restrictions.min_length ) { %> minlength="<%- restrictions.min_length %>"<% } %>
<% if ( restrictions.max_length ) { %> maxlength="<%- restrictions.max_length %>"<% } %>
<% if ( required ) { %> required<% } %>
<% if ( typeof errorMessages !== 'undefined' ) {
_.each(errorMessages, function( msg, type ) {%>
data-errormsg-<%= type %>="<%= msg %>"
data-errormsg-<%- type %>="<%- msg %>"
<% });
} %>
<% if ( placeholder ) { %> placeholder="<%= placeholder %>"<% } %>
<% if ( placeholder ) { %> placeholder="<%- placeholder %>"<% } %>
value="<%- defaultValue %>"
/>
<% if ( type === 'checkbox' ) { %>
<label for="<%= form %>-<%= name %>">
<span class="label-text"><%= label %></span>
<% if ( required && requiredStr ) { %><span class="label-required"><%= requiredStr %></span><% } %>
<% if ( !required && optionalStr ) { %><span class="label-optional"><%= optionalStr %></span><% } %>
<label for="<%- form %>-<%- name %>">
<span class="label-text"><%- label %></span>
<% if ( required && type !== 'hidden' ) { %>
<span id="<%- form %>-<%- name %>-required-label"
class="label-required <% if ( !requiredStr ) { %>hidden<% } %>">
<% if ( requiredStr ) { %><%- requiredStr %><% }%>
</span>
<span class="icon fa" id="<%- form %>-<%- name %>-validation-icon" aria-hidden="true"></span>
<% } %>
<% if ( !required && optionalStr ) { %>
<span class="label-optional" id="<%- form %>-<%- name %>-optional-label"><%- optionalStr %></span>
<% } %>
</label>
<% } %>
<% if ( instructions ) { %> <span class="tip tip-input" id="<%= form %>-<%= name %>-desc"><%= instructions %></span><% } %>
<span id="<%- form %>-<%- name %>-validation-error" class="tip error" aria-live="assertive">
<span class="sr-only">ERROR: </span>
<span id="<%- form %>-<%- name %>-validation-error-msg"></span>
</span>
<% if ( instructions ) { %> <span class="tip tip-input" id="<%- form %>-<%- name %>-desc"><%- instructions %></span><% } %>
<% } %>
<% if( form === 'login' && name === 'password' ) { %>
......
......@@ -45,7 +45,7 @@ USERNAME_INVALID_CHARS_UNICODE = _(
# Translators: This message is shown to users who attempt to create a new account using
# an invalid email format.
EMAIL_INVALID_MSG = _(u"Email '{email}' format is not valid")
EMAIL_INVALID_MSG = _(u'"{email}" is not a valid email address.')
# Translators: This message is shown to users who attempt to create a new
# account using an username/email associated with an existing account.
......@@ -60,15 +60,31 @@ USERNAME_CONFLICT_MSG = _(
# Translators: This message is shown to users who enter a username/email/password
# with an inappropriate length (too short or too long).
USERNAME_BAD_LENGTH_MSG = _(u"Username '{username}' must be between {min} and {max} characters long")
EMAIL_BAD_LENGTH_MSG = _(u"Email '{email}' must be between {min} and {max} characters long")
PASSWORD_BAD_LENGTH_MSG = _(u"Password must be between {min} and {max} characters long")
USERNAME_BAD_LENGTH_MSG = _(u"Username must be between {min} and {max} characters long.").format(
min=USERNAME_MIN_LENGTH, max=USERNAME_MAX_LENGTH
)
EMAIL_BAD_LENGTH_MSG = _(u"Enter a valid email address that contains at least {min} characters.").format(
min=EMAIL_MIN_LENGTH
)
PASSWORD_EMPTY_MSG = _(u"Please enter a password.")
PASSWORD_BAD_MIN_LENGTH_MSG = _(u"Password is not long enough.")
PASSWORD_BAD_MAX_LENGTH_MSG = _(u"Password cannot be longer than {max} character.").format(max=PASSWORD_MAX_LENGTH)
# These strings are normally not user-facing.
USERNAME_BAD_TYPE_MSG = u"Username must be a string"
EMAIL_BAD_TYPE_MSG = u"Email must be a string"
PASSWORD_BAD_TYPE_MSG = u"Password must be a string"
USERNAME_BAD_TYPE_MSG = u"Username must be a string."
EMAIL_BAD_TYPE_MSG = u"Email must be a string."
PASSWORD_BAD_TYPE_MSG = u"Password must be a string."
# Translators: This message is shown to users who enter a password matching
# the username they enter(ed).
PASSWORD_CANT_EQUAL_USERNAME_MSG = _(u"Password cannot be the same as the username")
PASSWORD_CANT_EQUAL_USERNAME_MSG = _(u"Password cannot be the same as the username.")
# Translators: These messages are shown to users who do not enter information
# into the required field or enter it incorrectly.
REQUIRED_FIELD_NAME_MSG = _(u"Please enter your Full Name.")
REQUIRED_FIELD_CONFIRM_EMAIL_MSG = _(u"The email addresses do not match.")
REQUIRED_FIELD_COUNTRY_MSG = _(u"Please select your Country.")
REQUIRED_FIELD_CITY_MSG = _(u"Please enter your City.")
REQUIRED_FIELD_GOALS_MSG = _(u"Please tell us your goals.")
REQUIRED_FIELD_LEVEL_OF_EDUCATION_MSG = _(u"Please select your highest level of education completed.")
REQUIRED_FIELD_MAILING_ADDRESS_MSG = _(u"Please enter your mailing address.")
......@@ -36,7 +36,7 @@ from openedx.core.djangoapps.user_api.errors import (
AccountRequestError
)
from openedx.core.djangoapps.user_api.accounts.tests.testutils import (
INVALID_EMAILS, INVALID_PASSWORDS, INVALID_USERNAMES
INVALID_EMAILS, INVALID_PASSWORDS, INVALID_USERNAMES, VALID_USERNAMES_UNICODE
)
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.models import PendingEmailChange
......@@ -450,14 +450,7 @@ class AccountCreationUnicodeUsernameTest(TestCase):
PASSWORD = u'unicode-user-password'
EMAIL = u'unicode-user-username@example.com'
UNICODE_USERNAMES = [
u'Enchanté',
u'username_with_@',
u'username with spaces',
u'eastern_arabic_numbers_١٢٣',
]
@ddt.data(*UNICODE_USERNAMES)
@ddt.data(*VALID_USERNAMES_UNICODE)
def test_unicode_usernames(self, unicode_username):
with patch.dict(settings.FEATURES, {'ENABLE_UNICODE_USERNAME': False}):
with self.assertRaises(AccountUsernameInvalid):
......
......@@ -10,6 +10,12 @@ from openedx.core.djangoapps.user_api.accounts import (
)
INVALID_NAMES = [
None,
'',
u''
]
INVALID_USERNAMES_ASCII = [
'$invalid-ascii$',
'invalid-fŕáńḱ',
......@@ -52,6 +58,24 @@ INVALID_PASSWORDS = [
u'a' * (PASSWORD_MAX_LENGTH + 1)
]
INVALID_COUNTRIES = [
None,
"",
"--"
]
VALID_NAMES = [
'Validation Bot',
u'Validation Bot'
]
VALID_USERNAMES_UNICODE = [
u'Enchanté',
u'username_with_@',
u'username with spaces',
u'eastern_arabic_numbers_١٢٣',
]
VALID_USERNAMES = [
u'username',
u'a' * USERNAME_MIN_LENGTH,
......@@ -72,3 +96,9 @@ VALID_PASSWORDS = [
u'a' * PASSWORD_MIN_LENGTH,
u'a' * PASSWORD_MAX_LENGTH
]
VALID_COUNTRIES = [
u'PK',
u'Pakistan',
u'US'
]
......@@ -58,6 +58,11 @@ class AccountPasswordInvalid(AccountRequestError):
pass
class AccountCountryInvalid(AccountRequestError):
"""The requested country does not exist. """
pass
class AccountDataBadLength(AccountRequestError):
"""The requested account data is either too short or too long. """
pass
......
......@@ -33,7 +33,7 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from ..accounts import (
NAME_MAX_LENGTH, EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, USERNAME_BAD_LENGTH_MSG
)
from ..accounts.api import get_account_settings
from ..models import UserOrgTag
......@@ -1198,6 +1198,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
{"value": "none", "name": "No formal education", "default": False},
{"value": "other", "name": "Other education", "default": False},
],
"errorMessages": {
"required": "Please select your highest level of education completed."
}
}
)
......@@ -1224,6 +1227,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
{"value": "none", "name": "No formal education TRANSLATED", "default": False},
{"value": "other", "name": "Other education TRANSLATED", "default": False},
],
"errorMessages": {
"required": "Please select your highest level of education completed."
}
}
)
......@@ -1301,6 +1307,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
"type": "textarea",
"required": False,
"label": "Mailing address",
"errorMessages": {
"required": "Please enter your mailing address."
}
}
)
......@@ -1313,7 +1322,10 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
"required": False,
"label": u"Tell us why you're interested in {platform_name}".format(
platform_name=settings.PLATFORM_NAME
)
),
"errorMessages": {
"required": "Please tell us your goals."
}
}
)
......@@ -1325,6 +1337,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
"type": "text",
"required": False,
"label": "City",
"errorMessages": {
"required": "Please enter your City."
}
}
)
......@@ -1992,8 +2007,8 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
self.assertEqual(
response_json,
{
"username": [{"user_message": "Username must be minimum of two characters long"}],
"password": [{"user_message": "A valid password is required"}],
u"username": [{u"user_message": USERNAME_BAD_LENGTH_MSG}],
u"password": [{u"user_message": u"A valid password is required"}],
}
)
......
......@@ -10,19 +10,8 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from openedx.core.djangoapps.user_api.accounts import (
EMAIL_BAD_LENGTH_MSG, EMAIL_INVALID_MSG,
EMAIL_CONFLICT_MSG, EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH,
PASSWORD_BAD_LENGTH_MSG, PASSWORD_CANT_EQUAL_USERNAME_MSG,
PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH,
USERNAME_BAD_LENGTH_MSG, USERNAME_INVALID_CHARS_ASCII, USERNAME_INVALID_CHARS_UNICODE,
USERNAME_CONFLICT_MSG, USERNAME_MAX_LENGTH, USERNAME_MIN_LENGTH
)
from openedx.core.djangoapps.user_api.accounts.tests.testutils import (
VALID_EMAILS, VALID_PASSWORDS, VALID_USERNAMES,
INVALID_EMAILS, INVALID_PASSWORDS, INVALID_USERNAMES,
INVALID_USERNAMES_ASCII, INVALID_USERNAMES_UNICODE
)
from openedx.core.djangoapps.user_api import accounts
from openedx.core.djangoapps.user_api.accounts.tests import testutils
from openedx.core.lib.api import test_utils
......@@ -45,16 +34,30 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
decision
)
def assertNotValidationDecision(self, data, decision):
self.assertNotEqual(
self.get_validation_decision(data),
decision
)
def test_no_decision_for_empty_request(self):
self.assertValidationDecision({}, {})
self.assertValidationDecision(
{},
{}
)
def test_no_decision_for_invalid_request(self):
self.assertValidationDecision({'invalid_field': 'random_user_data'}, {})
self.assertValidationDecision(
{'invalid_field': 'random_user_data'},
{}
)
@ddt.data(
['email', (email for email in VALID_EMAILS)],
['password', (password for password in VALID_PASSWORDS)],
['username', (username for username in VALID_USERNAMES)]
['name', (name for name in testutils.VALID_NAMES)],
['email', (email for email in testutils.VALID_EMAILS)],
['password', (password for password in testutils.VALID_PASSWORDS)],
['username', (username for username in testutils.VALID_USERNAMES)],
['country', (country for country in testutils.VALID_COUNTRIES)]
)
@ddt.unpack
def test_positive_validation_decision(self, form_field_name, user_data):
......@@ -68,17 +71,19 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
@ddt.data(
# Skip None type for invalidity checks.
['email', (email for email in INVALID_EMAILS[1:])],
['password', (password for password in INVALID_PASSWORDS[1:])],
['username', (username for username in INVALID_USERNAMES[1:])]
['name', (name for name in testutils.INVALID_NAMES[1:])],
['email', (email for email in testutils.INVALID_EMAILS[1:])],
['password', (password for password in testutils.INVALID_PASSWORDS[1:])],
['username', (username for username in testutils.INVALID_USERNAMES[1:])],
['country', (country for country in testutils.INVALID_COUNTRIES[1:])]
)
@ddt.unpack
def test_negative_validation_decision(self, form_field_name, user_data):
"""
Test if {0} as any item in {1} gives a negative validation decision.
"""
self.assertNotEqual(
self.get_validation_decision({form_field_name: user_data}),
self.assertNotValidationDecision(
{form_field_name: user_data},
{form_field_name: ''}
)
......@@ -101,71 +106,91 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
'email': email
},
{
"username": USERNAME_CONFLICT_MSG.format(username=user.username) if username == user.username else '',
"email": EMAIL_CONFLICT_MSG.format(email_address=user.email) if email == user.email else ''
"username": accounts.USERNAME_CONFLICT_MSG.format(
username=user.username
) if username == user.username else '',
"email": accounts.EMAIL_CONFLICT_MSG.format(
email_address=user.email
) if email == user.email else ''
}
)
@ddt.data('', ('e' * EMAIL_MAX_LENGTH) + '@email.com')
def test_email_less_than_min_length_validation_decision(self, email):
@ddt.data('', ('e' * accounts.EMAIL_MAX_LENGTH) + '@email.com')
def test_email_bad_length_validation_decision(self, email):
self.assertValidationDecision(
{'email': email},
{'email': EMAIL_BAD_LENGTH_MSG.format(email=email, min=EMAIL_MIN_LENGTH, max=EMAIL_MAX_LENGTH)}
{'email': accounts.EMAIL_BAD_LENGTH_MSG}
)
def test_email_generically_invalid_validation_decision(self):
email = 'email'
self.assertValidationDecision(
{'email': email},
{'email': EMAIL_INVALID_MSG.format(email=email)}
{'email': accounts.EMAIL_INVALID_MSG.format(email=email)}
)
def test_confirm_email_matches_email(self):
email = 'user@email.com'
self.assertValidationDecision(
{'email': email, 'confirm_email': email},
{'email': '', 'confirm_email': ''}
)
@ddt.data('', 'users@other.email')
def test_confirm_email_doesnt_equal_email(self, confirm_email):
self.assertValidationDecision(
{'email': 'user@email.com', 'confirm_email': confirm_email},
{'email': '', 'confirm_email': accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG}
)
@ddt.data(
'u' * (USERNAME_MIN_LENGTH - 1),
'u' * (USERNAME_MAX_LENGTH + 1)
'u' * (accounts.USERNAME_MIN_LENGTH - 1),
'u' * (accounts.USERNAME_MAX_LENGTH + 1)
)
def test_username_less_than_min_length_validation_decision(self, username):
def test_username_bad_length_validation_decision(self, username):
self.assertValidationDecision(
{'username': username},
{
'username': USERNAME_BAD_LENGTH_MSG.format(
username=username,
min=USERNAME_MIN_LENGTH,
max=USERNAME_MAX_LENGTH
)
}
{'username': accounts.USERNAME_BAD_LENGTH_MSG}
)
@unittest.skipUnless(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames disabled.")
@ddt.data(*INVALID_USERNAMES_UNICODE)
@ddt.unpack
@ddt.data(*testutils.INVALID_USERNAMES_UNICODE)
def test_username_invalid_unicode_validation_decision(self, username):
self.assertValidationDecision(
{'username': username},
{'username': USERNAME_INVALID_CHARS_UNICODE}
{'username': accounts.USERNAME_INVALID_CHARS_UNICODE}
)
@unittest.skipIf(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames enabled.")
@ddt.data(*INVALID_USERNAMES_ASCII)
@ddt.unpack
@ddt.data(*testutils.INVALID_USERNAMES_ASCII)
def test_username_invalid_ascii_validation_decision(self, username):
self.assertValidationDecision(
{'username': username},
{"username": USERNAME_INVALID_CHARS_ASCII}
{"username": accounts.USERNAME_INVALID_CHARS_ASCII}
)
@ddt.data(
'p' * (PASSWORD_MIN_LENGTH - 1),
'p' * (PASSWORD_MAX_LENGTH + 1)
)
def test_password_less_than_min_length_validation_decision(self, password):
def test_password_empty_validation_decision(self):
self.assertValidationDecision(
{'password': ''},
{"password": accounts.PASSWORD_EMPTY_MSG}
)
def test_password_bad_min_length_validation_decision(self):
password = 'p' * (accounts.PASSWORD_MIN_LENGTH - 1)
self.assertValidationDecision(
{'password': password},
{"password": accounts.PASSWORD_BAD_MIN_LENGTH_MSG}
)
def test_password_bad_max_length_validation_decision(self):
password = 'p' * (accounts.PASSWORD_MAX_LENGTH + 1)
self.assertValidationDecision(
{'password': password},
{"password": PASSWORD_BAD_LENGTH_MSG.format(min=PASSWORD_MIN_LENGTH, max=PASSWORD_MAX_LENGTH)}
{"password": accounts.PASSWORD_BAD_MAX_LENGTH_MSG}
)
def test_password_equals_username_validation_decision(self):
self.assertValidationDecision(
{"username": "somephrase", "password": "somephrase"},
{"username": "", "password": PASSWORD_CANT_EQUAL_USERNAME_MSG}
{"username": "", "password": accounts.PASSWORD_CANT_EQUAL_USERNAME_MSG}
)
......@@ -9,6 +9,9 @@ from rest_framework.views import APIView
from openedx.core.djangoapps.user_api.accounts.api import (
get_email_validation_error,
get_email_existence_validation_error,
get_confirm_email_validation_error,
get_country_validation_error,
get_name_validation_error,
get_password_validation_error,
get_username_validation_error,
get_username_existence_validation_error
......@@ -85,38 +88,66 @@ class RegistrationValidationView(APIView):
**Available Handlers**
"name":
A handler to check the validity of the user's real name.
"username":
A handler to check the validity of usernames.
"email":
A handler to check the validity of emails.
"confirm_email":
A handler to check whether the confirmation email field matches
the email field.
"password":
A handler to check the validity of passwords; a compatibility
decision with the username is made if it exists in the input.
"country":
A handler to check whether the validity of country fields.
"""
# This end-point is available to anonymous users, so no authentication is needed.
authentication_classes = []
def name_handler(self, request):
name = request.data.get('name')
return get_name_validation_error(name)
def username_handler(self, request):
username = request.data.get('username')
invalid_username_error = get_username_validation_error(username)
username_exists_error = get_username_existence_validation_error(username)
# Existing usernames are already valid, so we prefer that error.
return username_exists_error or invalid_username_error
# We prefer seeing for invalidity first.
# Some invalid usernames (like for superusers) may exist.
return invalid_username_error or username_exists_error
def email_handler(self, request):
email = request.data.get('email')
invalid_email_error = get_email_validation_error(email)
email_exists_error = get_email_existence_validation_error(email)
# Existing emails are already valid, so we prefer that error.
return email_exists_error or invalid_email_error
# We prefer seeing for invalidity first.
# Some invalid emails (like a blank one for superusers) may exist.
return invalid_email_error or email_exists_error
def confirm_email_handler(self, request):
email = request.data.get('email', None)
confirm_email = request.data.get('confirm_email')
return get_confirm_email_validation_error(confirm_email, email)
def password_handler(self, request):
username = request.data.get('username') or None
username = request.data.get('username', None)
password = request.data.get('password')
return get_password_validation_error(password, username)
def country_handler(self, request):
country = request.data.get('country')
return get_country_validation_error(country)
validation_handlers = {
"name": name_handler,
"username": username_handler,
"email": email_handler,
"confirm_email": confirm_email_handler,
"password": password_handler,
"country": country_handler
}
def post(self, request):
......@@ -125,9 +156,12 @@ class RegistrationValidationView(APIView):
Expects request of the form
>>> {
>>> "name": "Dan the Validator",
>>> "username": "mslm",
>>> "email": "mslm@gmail.com",
>>> "password": "password123"
>>> "confirm_email": "mslm@gmail.com",
>>> "password": "password123",
>>> "country": "PK"
>>> }
where each key is the appropriate form field name and the value is
user input. One may enter individual inputs if needed. Some inputs
......
......@@ -31,15 +31,7 @@ from student.forms import get_registration_extension_form
from student.views import create_account_with_params, AccountValidationError
from util.json_request import JsonResponse
from .accounts import (
EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH,
NAME_MAX_LENGTH,
PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH,
USERNAME_MAX_LENGTH, USERNAME_MIN_LENGTH,
EMAIL_CONFLICT_MSG,
USERNAME_CONFLICT_MSG
)
from .accounts.api import check_account_exists
import accounts
from .helpers import FormDescription, require_post_params, shim_student_view
from .models import UserPreference, UserProfile
from .preferences.api import get_country_time_zones, update_email_opt_in
......@@ -91,8 +83,8 @@ class LoginSessionView(APIView):
placeholder=email_placeholder,
instructions=email_instructions,
restrictions={
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH,
"min_length": accounts.EMAIL_MIN_LENGTH,
"max_length": accounts.EMAIL_MAX_LENGTH,
}
)
......@@ -105,8 +97,8 @@ class LoginSessionView(APIView):
label=password_label,
field_type="password",
restrictions={
"min_length": PASSWORD_MIN_LENGTH,
"max_length": PASSWORD_MAX_LENGTH,
"min_length": accounts.PASSWORD_MIN_LENGTH,
"max_length": accounts.PASSWORD_MAX_LENGTH,
}
)
......@@ -336,11 +328,11 @@ class RegistrationView(APIView):
username = data.get('username')
# Handle duplicate email/username
conflicts = check_account_exists(email=email, username=username)
conflicts = accounts.api.check_account_exists(email=email, username=username)
if conflicts:
conflict_messages = {
"email": EMAIL_CONFLICT_MSG.format(email_address=email),
"username": USERNAME_CONFLICT_MSG.format(username=username),
"email": accounts.EMAIL_CONFLICT_MSG.format(email_address=email),
"username": accounts.USERNAME_CONFLICT_MSG.format(username=username),
}
errors = {
field: [{"user_message": conflict_messages[field]}]
......@@ -414,8 +406,8 @@ class RegistrationView(APIView):
placeholder=email_placeholder,
instructions=email_instructions,
restrictions={
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH,
"min_length": accounts.EMAIL_MIN_LENGTH,
"max_length": accounts.EMAIL_MAX_LENGTH,
},
required=required
)
......@@ -433,7 +425,7 @@ class RegistrationView(APIView):
# Translators: This label appears above a field on the registration form
# meant to confirm the user's email address.
email_label = _(u"Confirm Email")
error_msg = _(u"The email addresses do not match.")
error_msg = accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG
form_desc.add_field(
"confirm_email",
......@@ -472,7 +464,7 @@ class RegistrationView(APIView):
placeholder=name_placeholder,
instructions=name_instructions,
restrictions={
"max_length": NAME_MAX_LENGTH,
"max_length": accounts.NAME_MAX_LENGTH,
},
required=required
)
......@@ -508,8 +500,8 @@ class RegistrationView(APIView):
instructions=username_instructions,
placeholder=username_placeholder,
restrictions={
"min_length": USERNAME_MIN_LENGTH,
"max_length": USERNAME_MAX_LENGTH,
"min_length": accounts.USERNAME_MIN_LENGTH,
"max_length": accounts.USERNAME_MAX_LENGTH,
},
required=required
)
......@@ -533,8 +525,8 @@ class RegistrationView(APIView):
label=password_label,
field_type="password",
restrictions={
"min_length": PASSWORD_MIN_LENGTH,
"max_length": PASSWORD_MAX_LENGTH,
"min_length": accounts.PASSWORD_MIN_LENGTH,
"max_length": accounts.PASSWORD_MAX_LENGTH,
},
required=required
)
......@@ -552,6 +544,7 @@ class RegistrationView(APIView):
# Translators: This label appears above a dropdown menu on the registration
# form used to select the user's highest completed level of education.
education_level_label = _(u"Highest level of education completed")
error_msg = accounts.REQUIRED_FIELD_LEVEL_OF_EDUCATION_MSG
# The labels are marked for translation in UserProfile model definition.
options = [(name, _(label)) for name, label in UserProfile.LEVEL_OF_EDUCATION_CHOICES] # pylint: disable=translation-of-non-string
......@@ -561,7 +554,10 @@ class RegistrationView(APIView):
field_type="select",
options=options,
include_default_option=True,
required=required
required=required,
error_messages={
"required": error_msg
}
)
def _add_gender_field(self, form_desc, required=True):
......@@ -626,12 +622,16 @@ class RegistrationView(APIView):
# Translators: This label appears above a field on the registration form
# meant to hold the user's mailing address.
mailing_address_label = _(u"Mailing address")
error_msg = accounts.REQUIRED_FIELD_MAILING_ADDRESS_MSG
form_desc.add_field(
"mailing_address",
label=mailing_address_label,
field_type="textarea",
required=required
required=required,
error_messages={
"required": error_msg
}
)
def _add_goals_field(self, form_desc, required=True):
......@@ -649,12 +649,16 @@ class RegistrationView(APIView):
goals_label = _(u"Tell us why you're interested in {platform_name}").format(
platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME)
)
error_msg = accounts.REQUIRED_FIELD_GOALS_MSG
form_desc.add_field(
"goals",
label=goals_label,
field_type="textarea",
required=required
required=required,
error_messages={
"required": error_msg
}
)
def _add_city_field(self, form_desc, required=True):
......@@ -670,11 +674,15 @@ class RegistrationView(APIView):
# Translators: This label appears above a field on the registration form
# which allows the user to input the city in which they live.
city_label = _(u"City")
error_msg = accounts.REQUIRED_FIELD_CITY_MSG
form_desc.add_field(
"city",
label=city_label,
required=required
required=required,
error_messages={
"required": error_msg
}
)
def _add_state_field(self, form_desc, required=False):
......@@ -790,7 +798,7 @@ class RegistrationView(APIView):
# Translators: This label appears above a dropdown menu on the registration
# form used to select the country in which the user lives.
country_label = _(u"Country")
error_msg = _(u"Please select your Country.")
error_msg = accounts.REQUIRED_FIELD_COUNTRY_MSG
# If we set a country code, make sure it's uppercase for the sake of the form.
default_country = form_desc._field_overrides.get('country', {}).get('defaultValue')
......@@ -1025,8 +1033,8 @@ class PasswordResetView(APIView):
placeholder=email_placeholder,
instructions=email_instructions,
restrictions={
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH,
"min_length": accounts.EMAIL_MIN_LENGTH,
"max_length": accounts.EMAIL_MAX_LENGTH,
}
)
......@@ -1094,7 +1102,9 @@ class PreferenceUsersListView(generics.ListAPIView):
paginate_by_param = "page_size"
def get_queryset(self):
return User.objects.filter(preferences__key=self.kwargs["pref_key"]).prefetch_related("preferences").select_related("profile")
return User.objects.filter(
preferences__key=self.kwargs["pref_key"]
).prefetch_related("preferences").select_related("profile")
class UpdateEmailOptInPreference(APIView):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment