Merge pull request #15375 from open-craft/uman/ent-334
[ENT-334] Add client-side registration form validation.
This commit is contained in:
@@ -23,18 +23,6 @@ from student.models import CourseEnrollmentAllowed
|
||||
from util.password_policy_validators import validate_password_strength
|
||||
|
||||
|
||||
USERNAME_TOO_SHORT_MSG = _("Username must be minimum of two characters long")
|
||||
USERNAME_TOO_LONG_MSG = _("Username cannot be more than %(limit_value)s characters long")
|
||||
|
||||
# Translators: This message is shown when the Unicode usernames are NOT allowed
|
||||
USERNAME_INVALID_CHARS_ASCII = _("Usernames can only contain Roman letters, western numerals (0-9), "
|
||||
"underscores (_), and hyphens (-).")
|
||||
|
||||
# Translators: This message is shown only when the Unicode usernames are allowed
|
||||
USERNAME_INVALID_CHARS_UNICODE = _("Usernames can only contain letters, numerals, underscore (_), numbers "
|
||||
"and @/./+/-/_ characters.")
|
||||
|
||||
|
||||
class PasswordResetFormNoActive(PasswordResetForm):
|
||||
error_messages = {
|
||||
'unknown': _("That e-mail address doesn't have an associated "
|
||||
@@ -127,12 +115,12 @@ def validate_username(username):
|
||||
|
||||
username_re = slug_re
|
||||
flags = None
|
||||
message = USERNAME_INVALID_CHARS_ASCII
|
||||
message = accounts_settings.USERNAME_INVALID_CHARS_ASCII
|
||||
|
||||
if settings.FEATURES.get("ENABLE_UNICODE_USERNAME"):
|
||||
username_re = r"^{regex}$".format(regex=settings.USERNAME_REGEX_PARTIAL)
|
||||
flags = re.UNICODE
|
||||
message = USERNAME_INVALID_CHARS_UNICODE
|
||||
message = accounts_settings.USERNAME_INVALID_CHARS_UNICODE
|
||||
|
||||
validator = RegexValidator(
|
||||
regex=username_re,
|
||||
@@ -156,9 +144,9 @@ class UsernameField(forms.CharField):
|
||||
min_length=accounts_settings.USERNAME_MIN_LENGTH,
|
||||
max_length=accounts_settings.USERNAME_MAX_LENGTH,
|
||||
error_messages={
|
||||
"required": USERNAME_TOO_SHORT_MSG,
|
||||
"min_length": USERNAME_TOO_SHORT_MSG,
|
||||
"max_length": USERNAME_TOO_LONG_MSG,
|
||||
"required": accounts_settings.USERNAME_BAD_LENGTH_MSG,
|
||||
"min_length": accounts_settings.USERNAME_BAD_LENGTH_MSG,
|
||||
"max_length": accounts_settings.USERNAME_BAD_LENGTH_MSG,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ 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_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.forms import USERNAME_INVALID_CHARS_ASCII, USERNAME_INVALID_CHARS_UNICODE
|
||||
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><%- content %></li>'),
|
||||
|
||||
msg: {
|
||||
email: gettext("The email address you've provided isn't formatted correctly."),
|
||||
@@ -107,7 +107,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 +124,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) {
|
||||
@@ -132,16 +132,21 @@
|
||||
label,
|
||||
context,
|
||||
content,
|
||||
customMsg;
|
||||
customMsg,
|
||||
liveValidationMsg;
|
||||
|
||||
_.each(tests, function(value, key) {
|
||||
if (!value) {
|
||||
label = _fn.validate.getLabel($el.attr('id'));
|
||||
customMsg = $el.data('errormsg-' + key) || false;
|
||||
liveValidationMsg =
|
||||
$('#' + $el.attr('id') + '-validation-error-msg').text() || false;
|
||||
|
||||
// If the field has a custom error msg attached, use it
|
||||
if (customMsg) {
|
||||
content = customMsg;
|
||||
} else if (liveValidationMsg) {
|
||||
content = liveValidationMsg;
|
||||
} else {
|
||||
context = {field: label};
|
||||
|
||||
@@ -154,7 +159,9 @@
|
||||
content = _.sprintf(_fn.validate.msg[key], context);
|
||||
}
|
||||
|
||||
txt.push(_fn.validate.template({content: content}));
|
||||
txt.push(_fn.validate.template({
|
||||
content: content
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -173,7 +180,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)
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1a\n"
|
||||
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
|
||||
"POT-Creation-Date: 2017-08-01 21:22+0000\n"
|
||||
"PO-Revision-Date: 2017-08-01 21:22:49.685108\n"
|
||||
"POT-Creation-Date: 2017-08-02 12:48+0000\n"
|
||||
"PO-Revision-Date: 2017-08-02 12:48:04.261143\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -330,31 +330,6 @@ msgstr ""
|
||||
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 "
|
||||
@@ -9307,6 +9282,101 @@ msgstr ""
|
||||
msgid "Theming Administration"
|
||||
msgstr ""
|
||||
|
||||
#: 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 "\"{email}\" is not a valid email address."
|
||||
msgstr ""
|
||||
|
||||
#: openedx/core/djangoapps/user_api/accounts/__init__.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"It looks like {email_address} belongs to an existing account. Try again with"
|
||||
" a different email address."
|
||||
msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#. 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/__init__.py
|
||||
msgid "Please enter your City."
|
||||
msgstr ""
|
||||
|
||||
#: openedx/core/djangoapps/user_api/accounts/__init__.py
|
||||
msgid "Please tell us your goals."
|
||||
msgstr ""
|
||||
|
||||
#: openedx/core/djangoapps/user_api/accounts/__init__.py
|
||||
msgid "Please select your highest level of education completed."
|
||||
msgstr ""
|
||||
|
||||
#: openedx/core/djangoapps/user_api/accounts/__init__.py
|
||||
msgid "Please enter your mailing address."
|
||||
msgstr ""
|
||||
|
||||
#: openedx/core/djangoapps/user_api/accounts/api.py
|
||||
#, python-brace-format
|
||||
msgid "The '{field_name}' field cannot be edited."
|
||||
@@ -9392,24 +9462,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 +9475,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
|
||||
@@ -9505,10 +9553,6 @@ msgstr ""
|
||||
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 ""
|
||||
@@ -19610,6 +19654,10 @@ msgid ""
|
||||
"assistance."
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/index.html
|
||||
msgid "Archived Courses"
|
||||
msgstr ""
|
||||
|
||||
#: cms/templates/index.html
|
||||
msgid "Libraries"
|
||||
msgstr ""
|
||||
@@ -21665,7 +21713,7 @@ msgid "Your changes were saved."
|
||||
msgstr ""
|
||||
|
||||
#: wiki/views/article.py
|
||||
msgid "A new revision of the article was succesfully added."
|
||||
msgid "A new revision of the article was successfully added."
|
||||
msgstr ""
|
||||
|
||||
#: wiki/views/article.py
|
||||
|
||||
@@ -26,8 +26,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1a\n"
|
||||
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
|
||||
"POT-Creation-Date: 2017-08-01 21:22+0000\n"
|
||||
"PO-Revision-Date: 2017-08-01 21:22:50.339993\n"
|
||||
"POT-Creation-Date: 2017-08-02 12:47+0000\n"
|
||||
"PO-Revision-Date: 2017-08-02 12:48:04.567443\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -4218,6 +4218,10 @@ msgstr ""
|
||||
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 ""
|
||||
|
||||
@@ -36,7 +36,6 @@ from openedx.core.djangoapps.oauth_dispatch.tests import factories as dot_factor
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
|
||||
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme_context
|
||||
from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH
|
||||
from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
@@ -62,24 +61,6 @@ class StudentAccountUpdateTest(CacheIsolationTestCase, UrlResetMixin):
|
||||
NEW_EMAIL = u"walt@savewalterwhite.com"
|
||||
|
||||
INVALID_ATTEMPTS = 100
|
||||
|
||||
INVALID_EMAILS = [
|
||||
None,
|
||||
u"",
|
||||
u"a",
|
||||
"no_domain",
|
||||
"no+domain",
|
||||
"@",
|
||||
"@domain.com",
|
||||
"test@no_extension",
|
||||
|
||||
# Long email -- subtract the length of the @domain
|
||||
# except for one character (so we exceed the max length limit)
|
||||
u"{user}@example.com".format(
|
||||
user=(u'e' * (EMAIL_MAX_LENGTH - 11))
|
||||
)
|
||||
]
|
||||
|
||||
INVALID_KEY = u"123abc"
|
||||
|
||||
URLCONF_MODULES = ['student_accounts.urls']
|
||||
|
||||
@@ -30,6 +30,17 @@
|
||||
confirm_email: 'xsy@edx.org',
|
||||
honor_code: true
|
||||
},
|
||||
$email = null,
|
||||
$name = null,
|
||||
$username = null,
|
||||
$password = null,
|
||||
$levelOfEducation = null,
|
||||
$gender = null,
|
||||
$yearOfBirth = null,
|
||||
$mailingAddress = null,
|
||||
$goals = null,
|
||||
$confirmEmail = null,
|
||||
$honorCode = null,
|
||||
THIRD_PARTY_AUTH = {
|
||||
currentProvider: null,
|
||||
providers: [
|
||||
@@ -49,9 +60,26 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
VALIDATION_DECISIONS_POSITIVE = {
|
||||
validation_decisions: {
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirm_email: ''
|
||||
}
|
||||
},
|
||||
VALIDATION_DECISIONS_NEGATIVE = {
|
||||
validation_decisions: {
|
||||
email: 'Error.',
|
||||
username: 'Error.',
|
||||
password: 'Error.',
|
||||
confirm_email: 'Error'
|
||||
}
|
||||
},
|
||||
FORM_DESCRIPTION = {
|
||||
method: 'post',
|
||||
submit_url: '/user_api/v1/account/registration/',
|
||||
validation_url: '/api/user/v1/validation/registration',
|
||||
fields: [
|
||||
{
|
||||
placeholder: 'username@domain.com',
|
||||
@@ -110,10 +138,10 @@
|
||||
defaultValue: '',
|
||||
type: 'select',
|
||||
options: [
|
||||
{value: '', name: '--'},
|
||||
{value: 'p', name: 'Doctorate'},
|
||||
{value: 'm', name: "Master's or professional degree"},
|
||||
{value: 'b', name: "Bachelor's degree"}
|
||||
{value: '', name: '--'},
|
||||
{value: 'p', name: 'Doctorate'},
|
||||
{value: 'm', name: "Master's or professional degree"},
|
||||
{value: 'b', name: "Bachelor's degree"}
|
||||
],
|
||||
required: false,
|
||||
instructions: 'Select your education level.',
|
||||
@@ -126,10 +154,10 @@
|
||||
defaultValue: '',
|
||||
type: 'select',
|
||||
options: [
|
||||
{value: '', name: '--'},
|
||||
{value: 'm', name: 'Male'},
|
||||
{value: 'f', name: 'Female'},
|
||||
{value: 'o', name: 'Other'}
|
||||
{value: '', name: '--'},
|
||||
{value: 'm', name: 'Male'},
|
||||
{value: 'f', name: 'Female'},
|
||||
{value: 'o', name: 'Other'}
|
||||
],
|
||||
required: false,
|
||||
instructions: 'Select your gender.',
|
||||
@@ -142,10 +170,10 @@
|
||||
defaultValue: '',
|
||||
type: 'select',
|
||||
options: [
|
||||
{value: '', name: '--'},
|
||||
{value: 1900, name: '1900'},
|
||||
{value: 1950, name: '1950'},
|
||||
{value: 2014, name: '2014'}
|
||||
{value: '', name: '--'},
|
||||
{value: 1900, name: '1900'},
|
||||
{value: 1950, name: '1950'},
|
||||
{value: 2014, name: '2014'}
|
||||
],
|
||||
required: false,
|
||||
instructions: 'Select your year of birth.',
|
||||
@@ -185,7 +213,6 @@
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var createRegisterView = function(that) {
|
||||
// Initialize the register model
|
||||
model = new RegisterModel({}, {
|
||||
@@ -209,6 +236,43 @@
|
||||
view.on('auth-complete', function() {
|
||||
authComplete = true;
|
||||
});
|
||||
|
||||
// Target each form field.
|
||||
$email = $('#register-email');
|
||||
$confirmEmail = $('#register-confirm_email');
|
||||
$name = $('#register-name');
|
||||
$username = $('#register-username');
|
||||
$password = $('#register-password');
|
||||
$levelOfEducation = $('#register-level_of_education');
|
||||
$gender = $('#register-gender');
|
||||
$yearOfBirth = $('#register-year_of_birth');
|
||||
$mailingAddress = $('#register-mailing_address');
|
||||
$goals = $('#register-goals');
|
||||
$honorCode = $('#register-honor_code');
|
||||
};
|
||||
|
||||
var fillData = function() {
|
||||
$email.val(USER_DATA.email);
|
||||
$confirmEmail.val(USER_DATA.email);
|
||||
$name.val(USER_DATA.name);
|
||||
$username.val(USER_DATA.username);
|
||||
$password.val(USER_DATA.password);
|
||||
$levelOfEducation.val(USER_DATA.level_of_education);
|
||||
$gender.val(USER_DATA.gender);
|
||||
$yearOfBirth.val(USER_DATA.year_of_birth);
|
||||
$mailingAddress.val(USER_DATA.mailing_address);
|
||||
$goals.val(USER_DATA.goals);
|
||||
// Check the honor code checkbox
|
||||
$honorCode.prop('checked', USER_DATA.honor_code);
|
||||
};
|
||||
|
||||
var liveValidate = function($el, validationSuccess) {
|
||||
$el.focus();
|
||||
if (!_.isUndefined(validationSuccess) && !validationSuccess) {
|
||||
model.trigger('validation', $el, VALIDATION_DECISIONS_NEGATIVE);
|
||||
} else {
|
||||
model.trigger('validation', $el, VALIDATION_DECISIONS_POSITIVE);
|
||||
}
|
||||
};
|
||||
|
||||
var submitForm = function(validationSuccess) {
|
||||
@@ -216,19 +280,7 @@
|
||||
var clickEvent = $.Event('click');
|
||||
|
||||
// Simulate manual entry of registration form data
|
||||
$('#register-email').val(USER_DATA.email);
|
||||
$('#register-confirm_email').val(USER_DATA.email);
|
||||
$('#register-name').val(USER_DATA.name);
|
||||
$('#register-username').val(USER_DATA.username);
|
||||
$('#register-password').val(USER_DATA.password);
|
||||
$('#register-level_of_education').val(USER_DATA.level_of_education);
|
||||
$('#register-gender').val(USER_DATA.gender);
|
||||
$('#register-year_of_birth').val(USER_DATA.year_of_birth);
|
||||
$('#register-mailing_address').val(USER_DATA.mailing_address);
|
||||
$('#register-goals').val(USER_DATA.goals);
|
||||
|
||||
// Check the honor code checkbox
|
||||
$('#register-honor_code').prop('checked', USER_DATA.honor_code);
|
||||
fillData();
|
||||
|
||||
// If validationSuccess isn't passed, we avoid
|
||||
// spying on `view.validate` twice
|
||||
@@ -238,6 +290,10 @@
|
||||
isValid: validationSuccess,
|
||||
message: 'Submission was validated.'
|
||||
});
|
||||
// Successful validation means there's no need to use AJAX calls from liveValidate,
|
||||
if (validationSuccess) {
|
||||
spyOn(view, 'liveValidate').and.callFake(function() {});
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the email address
|
||||
@@ -284,6 +340,7 @@
|
||||
if (param === '?course_id') {
|
||||
return encodeURIComponent(COURSE_ID);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Attempt to register
|
||||
@@ -308,17 +365,17 @@
|
||||
expect($('.button-oa2-facebook')).toBeVisible();
|
||||
});
|
||||
|
||||
it('validates registration form fields', function() {
|
||||
it('validates registration form fields on form submission', function() {
|
||||
createRegisterView(this);
|
||||
|
||||
// Submit the form, with successful validation
|
||||
submitForm(true);
|
||||
|
||||
// Verify that validation of form fields occurred
|
||||
expect(view.validate).toHaveBeenCalledWith($('#register-email')[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($('#register-name')[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($('#register-username')[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($('#register-password')[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($email[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($name[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($username[0]);
|
||||
expect(view.validate).toHaveBeenCalledWith($password[0]);
|
||||
|
||||
// Verify that no submission errors are visible
|
||||
expect(view.$formFeedback.find('.' + view.formErrorsJsHook).length).toEqual(0);
|
||||
@@ -327,7 +384,34 @@
|
||||
expect(view.$submitButton).toHaveAttr('disabled');
|
||||
});
|
||||
|
||||
it('displays registration form validation errors', function() {
|
||||
it('live validates registration form fields', function() {
|
||||
var requiredValidationFields = [$email, $confirmEmail, $username, $password],
|
||||
i,
|
||||
$el;
|
||||
createRegisterView(this);
|
||||
|
||||
for (i = 0; i < requiredValidationFields.length; ++i) {
|
||||
$el = requiredValidationFields[i];
|
||||
|
||||
// Perform successful live validations.
|
||||
liveValidate($el);
|
||||
|
||||
// Confirm success.
|
||||
expect($el).toHaveClass('success');
|
||||
|
||||
// Confirm that since we've blurred from each input, required text doesn't show.
|
||||
expect(view.getRequiredTextLabel($el)).toHaveClass('hidden');
|
||||
|
||||
// Confirm fa-check shows.
|
||||
expect(view.getIcon($el)).toHaveClass('fa-check');
|
||||
expect(view.getIcon($el)).toBeVisible();
|
||||
|
||||
// Confirm the error tip is empty.
|
||||
expect(view.getErrorTip($el).val().length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('displays registration form validation errors on form submission', function() {
|
||||
createRegisterView(this);
|
||||
|
||||
// Submit the form, with failed validation
|
||||
@@ -343,7 +427,34 @@
|
||||
expect(view.$submitButton).not.toHaveAttr('disabled');
|
||||
});
|
||||
|
||||
it('displays an error if the server returns an error while registering', function() {
|
||||
it('displays live registration form validation errors', function() {
|
||||
var requiredValidationFields = [$email, $confirmEmail, $username, $password],
|
||||
i,
|
||||
$el;
|
||||
createRegisterView(this);
|
||||
|
||||
for (i = 0; i < requiredValidationFields.length; ++i) {
|
||||
$el = requiredValidationFields[i];
|
||||
|
||||
// Perform invalid live validations.
|
||||
liveValidate($el, false);
|
||||
|
||||
// Confirm error.
|
||||
expect($el).toHaveClass('error');
|
||||
|
||||
// Confirm that since we've blurred from each input, required text still shows for errors.
|
||||
expect(view.getRequiredTextLabel($el)).not.toHaveClass('hidden');
|
||||
|
||||
// Confirm fa-times shows.
|
||||
expect(view.getIcon($el)).toHaveClass('fa-exclamation');
|
||||
expect(view.getIcon($el)).toBeVisible();
|
||||
|
||||
// Confirm the error tip shows an error message.
|
||||
expect(view.getErrorTip($el).val()).not.toBeEmpty();
|
||||
}
|
||||
});
|
||||
|
||||
it('displays an error on form submission if the server returns an error', function() {
|
||||
createRegisterView(this);
|
||||
|
||||
// Submit the form, with successful validation
|
||||
|
||||
@@ -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,8 +177,15 @@
|
||||
},
|
||||
|
||||
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.scrollToFormFeedback();
|
||||
this.toggleDisableButton(false);
|
||||
},
|
||||
|
||||
@@ -200,7 +194,6 @@
|
||||
*/
|
||||
renderErrors: function(title, errorMessages) {
|
||||
this.clearFormErrors();
|
||||
|
||||
this.renderFormFeedback(this.formErrorsTpl, {
|
||||
jsHook: this.formErrorsJsHook,
|
||||
title: title,
|
||||
@@ -211,14 +204,6 @@
|
||||
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');
|
||||
|
||||
// Focus on the feedback container to ensure screen readers see the messages.
|
||||
this.$formFeedback.focus();
|
||||
},
|
||||
|
||||
/* Allows extended views to add non-form attributes
|
||||
@@ -244,6 +229,7 @@
|
||||
this.clearFormErrors();
|
||||
} else {
|
||||
this.renderErrors(this.defaultFormErrorsTitle, this.errors);
|
||||
this.scrollToFormFeedback();
|
||||
this.toggleDisableButton(false);
|
||||
}
|
||||
|
||||
@@ -257,6 +243,10 @@
|
||||
return true;
|
||||
},
|
||||
|
||||
resetValidationVariables: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
clearFormErrors: function() {
|
||||
var query = '.' + this.formErrorsJsHook;
|
||||
this.clearFormFeedbackItems(query);
|
||||
@@ -283,8 +273,44 @@
|
||||
}
|
||||
},
|
||||
|
||||
scrollToFormFeedback: function() {
|
||||
var self = this;
|
||||
// Scroll to feedback container
|
||||
$('html,body').animate({
|
||||
scrollTop: this.$formFeedback.offset().top
|
||||
}, 'slow', function() {
|
||||
self.resetValidationVariables();
|
||||
});
|
||||
|
||||
// Focus on the feedback container to ensure screen readers see the messages.
|
||||
this.$formFeedback.focus();
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,29 +4,48 @@
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'edx-ui-toolkit/js/utils/string-utils',
|
||||
'js/student_account/views/FormView',
|
||||
'text!templates/student_account/form_status.underscore'
|
||||
],
|
||||
function($, _, gettext, FormView, formStatusTpl) {
|
||||
function(
|
||||
$, _, gettext,
|
||||
StringUtils,
|
||||
FormView,
|
||||
formStatusTpl
|
||||
) {
|
||||
return FormView.extend({
|
||||
el: '#register-form',
|
||||
|
||||
tpl: '#register-tpl',
|
||||
|
||||
validationUrl: '/api/user/v1/validation/registration',
|
||||
events: {
|
||||
'click .js-register': 'submitForm',
|
||||
'click .login-provider': 'thirdPartyAuth'
|
||||
'click .login-provider': 'thirdPartyAuth',
|
||||
'click input[required][type="checkbox"]': 'liveValidateHandler',
|
||||
'blur input[required], textarea[required], select[required]': 'liveValidateHandler',
|
||||
'focus input[required], textarea[required], select[required]': 'handleRequiredInputFocus'
|
||||
},
|
||||
|
||||
liveValidationFields: [
|
||||
'name',
|
||||
'username',
|
||||
'password',
|
||||
'email',
|
||||
'confirm_email',
|
||||
'country',
|
||||
'honor_code',
|
||||
'terms_of_service'
|
||||
],
|
||||
formType: 'register',
|
||||
|
||||
formStatusTpl: formStatusTpl,
|
||||
|
||||
authWarningJsHook: 'js-auth-warning',
|
||||
|
||||
defaultFormErrorsTitle: gettext('We couldn\'t create your account.'),
|
||||
|
||||
submitButton: '.js-register',
|
||||
positiveValidationIcon: 'fa-check',
|
||||
negativeValidationIcon: 'fa-exclamation',
|
||||
successfulValidationDisplaySeconds: 3,
|
||||
// These are reset to true on form submission.
|
||||
positiveValidationEnabled: true,
|
||||
negativeValidationEnabled: true,
|
||||
|
||||
preRender: function(data) {
|
||||
this.providers = data.thirdPartyAuth.providers || [];
|
||||
@@ -41,6 +60,7 @@
|
||||
this.autoRegisterWelcomeMessage = data.thirdPartyAuth.autoRegisterWelcomeMessage || '';
|
||||
|
||||
this.listenTo(this.model, 'sync', this.saveSuccess);
|
||||
this.listenTo(this.model, 'validation', this.renderLiveValidations);
|
||||
},
|
||||
|
||||
render: function(html) {
|
||||
@@ -79,6 +99,144 @@
|
||||
return this;
|
||||
},
|
||||
|
||||
hideRequiredMessageExceptOnError: function($el) {
|
||||
// We only handle blur if not in an error state.
|
||||
if (!$el.hasClass('error')) {
|
||||
this.hideRequiredMessage($el);
|
||||
}
|
||||
},
|
||||
|
||||
hideRequiredMessage: function($el) {
|
||||
this.doOnInputLabel($el, function($label) {
|
||||
$label.addClass('hidden');
|
||||
});
|
||||
},
|
||||
|
||||
doOnInputLabel: function($el, action) {
|
||||
var $label = this.getRequiredTextLabel($el);
|
||||
action($label);
|
||||
},
|
||||
|
||||
handleRequiredInputFocus: function(event) {
|
||||
var $el = $(event.currentTarget);
|
||||
// Avoid rendering for required checkboxes.
|
||||
if ($el.attr('type') !== 'checkbox') {
|
||||
this.renderRequiredMessage($el);
|
||||
}
|
||||
if ($el.hasClass('error')) {
|
||||
this.doOnInputLabel($el, function($label) {
|
||||
$label.addClass('error');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderRequiredMessage: function($el) {
|
||||
this.doOnInputLabel($el, function($label) {
|
||||
$label.removeClass('hidden').text(gettext('(required)'));
|
||||
});
|
||||
},
|
||||
|
||||
getRequiredTextLabel: function($el) {
|
||||
return $('#' + $el.attr('id') + '-required-label');
|
||||
},
|
||||
|
||||
renderLiveValidations: function($el, decisions) {
|
||||
var $label = this.getLabel($el),
|
||||
$requiredTextLabel = this.getRequiredTextLabel($el),
|
||||
$icon = this.getIcon($el),
|
||||
$errorTip = this.getErrorTip($el),
|
||||
name = $el.attr('name'),
|
||||
type = $el.attr('type'),
|
||||
isCheckbox = type === 'checkbox',
|
||||
hasError = decisions.validation_decisions[name] !== '',
|
||||
error = isCheckbox ? '' : decisions.validation_decisions[name];
|
||||
|
||||
if (hasError && this.negativeValidationEnabled) {
|
||||
this.renderLiveValidationError($el, $label, $requiredTextLabel, $icon, $errorTip, error);
|
||||
} else if (this.positiveValidationEnabled) {
|
||||
this.renderLiveValidationSuccess($el, $label, $requiredTextLabel, $icon, $errorTip);
|
||||
}
|
||||
},
|
||||
|
||||
getLabel: function($el) {
|
||||
return this.$form.find('label[for=' + $el.attr('id') + ']');
|
||||
},
|
||||
|
||||
getIcon: function($el) {
|
||||
return $('#' + $el.attr('id') + '-validation-icon');
|
||||
},
|
||||
|
||||
getErrorTip: function($el) {
|
||||
return $('#' + $el.attr('id') + '-validation-error-msg');
|
||||
},
|
||||
|
||||
getFieldTimeout: function($el) {
|
||||
return $('#' + $el.attr('id')).attr('timeout-id') || null;
|
||||
},
|
||||
|
||||
setFieldTimeout: function($el, time, action) {
|
||||
$el.attr('timeout-id', setTimeout(action, time));
|
||||
},
|
||||
|
||||
clearFieldTimeout: function($el) {
|
||||
var timeout = this.getFieldTimeout($el);
|
||||
if (timeout) {
|
||||
clearTimeout(this.getFieldTimeout($el));
|
||||
$el.removeAttr('timeout-id');
|
||||
}
|
||||
},
|
||||
|
||||
renderLiveValidationError: function($el, $label, $req, $icon, $tip, error) {
|
||||
this.removeLiveValidationIndicators(
|
||||
$el, $label, $req, $icon,
|
||||
'success', this.positiveValidationIcon
|
||||
);
|
||||
this.addLiveValidationIndicators(
|
||||
$el, $label, $req, $icon, $tip,
|
||||
'error', this.negativeValidationIcon, error
|
||||
);
|
||||
this.renderRequiredMessage($el);
|
||||
},
|
||||
|
||||
renderLiveValidationSuccess: function($el, $label, $req, $icon, $tip) {
|
||||
var self = this,
|
||||
validationFadeTime = this.successfulValidationDisplaySeconds * 1000;
|
||||
this.removeLiveValidationIndicators(
|
||||
$el, $label, $req, $icon,
|
||||
'error', this.negativeValidationIcon
|
||||
);
|
||||
this.addLiveValidationIndicators(
|
||||
$el, $label, $req, $icon, $tip,
|
||||
'success', this.positiveValidationIcon, ''
|
||||
);
|
||||
this.hideRequiredMessage($el);
|
||||
|
||||
// Hide success indicators after some time.
|
||||
this.clearFieldTimeout($el);
|
||||
this.setFieldTimeout($el, validationFadeTime, function() {
|
||||
self.removeLiveValidationIndicators(
|
||||
$el, $label, $req, $icon,
|
||||
'success', self.positiveValidationIcon
|
||||
);
|
||||
self.clearFieldTimeout($el);
|
||||
});
|
||||
},
|
||||
|
||||
addLiveValidationIndicators: function($el, $label, $req, $icon, $tip, indicator, icon, msg) {
|
||||
$el.addClass(indicator);
|
||||
$label.addClass(indicator);
|
||||
$req.addClass(indicator);
|
||||
$icon.addClass(indicator + ' ' + icon);
|
||||
$tip.text(msg);
|
||||
},
|
||||
|
||||
removeLiveValidationIndicators: function($el, $label, $req, $icon, indicator, icon) {
|
||||
$el.removeClass(indicator);
|
||||
$label.removeClass(indicator);
|
||||
$req.removeClass(indicator);
|
||||
$icon.removeClass(indicator + ' ' + icon);
|
||||
},
|
||||
|
||||
thirdPartyAuth: function(event) {
|
||||
var providerUrl = $(event.currentTarget).data('provider-url') || '';
|
||||
|
||||
@@ -100,12 +258,17 @@
|
||||
function(errorList) {
|
||||
return _.map(
|
||||
errorList,
|
||||
function(errorItem) { return '<li>' + errorItem.user_message + '</li>'; }
|
||||
function(errorItem) {
|
||||
return StringUtils.interpolate('<li>{error}</li>', {
|
||||
error: errorItem.user_message
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
this.renderErrors(this.defaultFormErrorsTitle, this.errors);
|
||||
this.scrollToFormFeedback();
|
||||
this.toggleDisableButton(false);
|
||||
},
|
||||
|
||||
@@ -116,6 +279,11 @@
|
||||
}
|
||||
},
|
||||
|
||||
resetValidationVariables: function() {
|
||||
this.positiveValidationEnabled = true;
|
||||
this.negativeValidationEnabled = true;
|
||||
},
|
||||
|
||||
renderAuthWarning: function() {
|
||||
var msgPart1 = gettext('You\'ve successfully signed into %(currentProvider)s.'),
|
||||
msgPart2 = gettext(
|
||||
@@ -132,35 +300,100 @@
|
||||
});
|
||||
},
|
||||
|
||||
getFormData: function() {
|
||||
var obj = FormView.prototype.getFormData.apply(this, arguments),
|
||||
$form = this.$form,
|
||||
$label,
|
||||
$emailElement,
|
||||
$confirmEmailElement,
|
||||
email = '',
|
||||
confirmEmail = '';
|
||||
submitForm: function(event) { // eslint-disable-line no-unused-vars
|
||||
var elements = this.$form[0].elements,
|
||||
$el,
|
||||
i;
|
||||
|
||||
$emailElement = $form.find('input[name=email]');
|
||||
$confirmEmailElement = $form.find('input[name=confirm_email]');
|
||||
// As per requirements, disable positive validation for submission.
|
||||
this.positiveValidationEnabled = false;
|
||||
|
||||
if ($confirmEmailElement.length) {
|
||||
email = $emailElement.val();
|
||||
confirmEmail = $confirmEmailElement.val();
|
||||
$label = $form.find('label[for=' + $confirmEmailElement.attr('id') + ']');
|
||||
for (i = 0; i < elements.length; i++) {
|
||||
$el = $(elements[i]);
|
||||
|
||||
if (confirmEmail !== '' && email !== confirmEmail) {
|
||||
this.errors.push('<li>' + $confirmEmailElement.data('errormsg-required') + '</li>');
|
||||
$confirmEmailElement.addClass('error');
|
||||
$label.addClass('error');
|
||||
} else if (confirmEmail !== '') {
|
||||
obj.confirm_email = confirmEmail;
|
||||
$confirmEmailElement.removeClass('error');
|
||||
$label.removeClass('error');
|
||||
// Simulate live validation.
|
||||
if ($el.attr('required')) {
|
||||
$el.blur();
|
||||
}
|
||||
}
|
||||
|
||||
FormView.prototype.submitForm.apply(this, arguments);
|
||||
},
|
||||
|
||||
getFormData: function() {
|
||||
var obj = FormView.prototype.getFormData.apply(this, arguments),
|
||||
$emailElement = this.$form.find('input[name=email]'),
|
||||
$confirmEmail = this.$form.find('input[name=confirm_email]');
|
||||
|
||||
if ($confirmEmail.length) {
|
||||
if (!$confirmEmail.val() || ($emailElement.val() !== $confirmEmail.val())) {
|
||||
this.errors.push(StringUtils.interpolate('<li>{error}</li>', {
|
||||
error: $confirmEmail.data('errormsg-required')
|
||||
}));
|
||||
}
|
||||
obj.confirm_email = $confirmEmail.val();
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
liveValidateHandler: function(event) {
|
||||
var $el = $(event.currentTarget);
|
||||
// Until we get a back-end that can handle all available
|
||||
// registration fields, we do some generic validation here.
|
||||
if (this.inLiveValidationFields($el)) {
|
||||
if ($el.attr('type') === 'checkbox') {
|
||||
this.liveValidateCheckbox($el);
|
||||
} else {
|
||||
this.liveValidate($el);
|
||||
}
|
||||
} else {
|
||||
this.genericLiveValidateHandler($el);
|
||||
}
|
||||
// On blur, we do exactly as the function name says, no matter which input.
|
||||
this.hideRequiredMessageExceptOnError($el);
|
||||
},
|
||||
|
||||
liveValidate: function($el) {
|
||||
var data = {},
|
||||
field,
|
||||
i;
|
||||
for (i = 0; i < this.liveValidationFields.length; ++i) {
|
||||
field = this.liveValidationFields[i];
|
||||
data[field] = $('#register-' + field).val();
|
||||
}
|
||||
FormView.prototype.liveValidate(
|
||||
$el, this.validationUrl, 'json', data, 'POST', this.model
|
||||
);
|
||||
},
|
||||
|
||||
liveValidateCheckbox: function($checkbox) {
|
||||
var validationDecisions = {validation_decisions: {}},
|
||||
decisions = validationDecisions.validation_decisions,
|
||||
name = $checkbox.attr('name'),
|
||||
checked = $checkbox.is(':checked'),
|
||||
error = $checkbox.data('errormsg-required');
|
||||
decisions[name] = checked ? '' : error;
|
||||
this.renderLiveValidations($checkbox, validationDecisions);
|
||||
},
|
||||
|
||||
genericLiveValidateHandler: function($el) {
|
||||
var elementType = $el.attr('type');
|
||||
if (elementType === 'checkbox') {
|
||||
// We are already validating checkboxes in a generic way.
|
||||
this.liveValidateCheckbox($el);
|
||||
} else {
|
||||
this.genericLiveValidate($el);
|
||||
}
|
||||
},
|
||||
|
||||
genericLiveValidate: function($el) {
|
||||
var validationDecisions = {validation_decisions: {}},
|
||||
decisions = validationDecisions.validation_decisions,
|
||||
name = $el.attr('name'),
|
||||
error = $el.data('errormsg-required');
|
||||
decisions[name] = $el.val() ? '' : error;
|
||||
this.renderLiveValidations($el, validationDecisions);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<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' ) { %>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
Account constants
|
||||
"""
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
# The minimum and maximum length for the name ("full name") account field
|
||||
NAME_MIN_LENGTH = 2
|
||||
NAME_MAX_LENGTH = 255
|
||||
@@ -25,3 +28,63 @@ ALL_USERS_VISIBILITY = 'all_users'
|
||||
|
||||
# Indicates the user's preference that all their account information be private.
|
||||
PRIVATE_VISIBILITY = 'private'
|
||||
|
||||
# Translators: This message is shown when the Unicode usernames are NOT allowed.
|
||||
# It is shown to users who attempt to create a new account using invalid characters
|
||||
# in the username.
|
||||
USERNAME_INVALID_CHARS_ASCII = _(
|
||||
u"Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-)."
|
||||
)
|
||||
|
||||
# Translators: This message is shown only when the Unicode usernames are allowed.
|
||||
# It is shown to users who attempt to create a new account using invalid characters
|
||||
# in the username.
|
||||
USERNAME_INVALID_CHARS_UNICODE = _(
|
||||
u"Usernames can only contain letters, numerals, and @/./+/-/_ characters."
|
||||
)
|
||||
|
||||
# Translators: This message is shown to users who attempt to create a new account using
|
||||
# an invalid email format.
|
||||
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.
|
||||
EMAIL_CONFLICT_MSG = _(
|
||||
u"It looks like {email_address} belongs to an existing account. "
|
||||
u"Try again with a different email address."
|
||||
)
|
||||
USERNAME_CONFLICT_MSG = _(
|
||||
u"It looks like {username} belongs to an existing account. "
|
||||
u"Try again with a different username."
|
||||
)
|
||||
|
||||
# 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 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."
|
||||
|
||||
# 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.")
|
||||
|
||||
# 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.")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Programmatic integration point for User API Accounts sub-application
|
||||
"""
|
||||
@@ -19,30 +20,19 @@ from util.model_utils import emit_setting_changed_event
|
||||
|
||||
from openedx.core.lib.api.view_utils import add_serializer_errors
|
||||
|
||||
from ..errors import (
|
||||
AccountUpdateError, AccountValidationError, AccountUsernameInvalid, AccountPasswordInvalid,
|
||||
AccountEmailInvalid, AccountUserAlreadyExists,
|
||||
UserAPIInternalError, UserAPIRequestError, UserNotFound, UserNotAuthorized
|
||||
)
|
||||
from ..forms import PasswordResetFormNoActive
|
||||
from ..helpers import intercept_errors
|
||||
|
||||
from . import (
|
||||
EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
|
||||
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH
|
||||
)
|
||||
from .serializers import (
|
||||
AccountLegacyProfileSerializer, AccountUserSerializer,
|
||||
UserReadOnlySerializer, _visible_fields # pylint: disable=invalid-name
|
||||
)
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_api import errors, accounts, forms, helpers
|
||||
|
||||
|
||||
# Public access point for this function.
|
||||
visible_fields = _visible_fields
|
||||
|
||||
|
||||
@intercept_errors(UserAPIInternalError, ignore_errors=[UserAPIRequestError])
|
||||
@helpers.intercept_errors(errors.UserAPIInternalError, ignore_errors=[errors.UserAPIRequestError])
|
||||
def get_account_settings(request, usernames=None, configuration=None, view=None):
|
||||
"""Returns account information for a user serialized as JSON.
|
||||
|
||||
@@ -67,16 +57,17 @@ def get_account_settings(request, usernames=None, configuration=None, view=None)
|
||||
A list of users account details.
|
||||
|
||||
Raises:
|
||||
UserNotFound: no user with username `username` exists (or `request.user.username` if
|
||||
errors.UserNotFound: no user with username `username` exists (or `request.user.username` if
|
||||
`username` is not specified)
|
||||
UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
errors.UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
|
||||
"""
|
||||
requesting_user = request.user
|
||||
usernames = usernames or [requesting_user.username]
|
||||
|
||||
requested_users = User.objects.select_related('profile').filter(username__in=usernames)
|
||||
if not requested_users:
|
||||
raise UserNotFound()
|
||||
raise errors.UserNotFound()
|
||||
|
||||
serialized_users = []
|
||||
for user in requested_users:
|
||||
@@ -95,7 +86,7 @@ def get_account_settings(request, usernames=None, configuration=None, view=None)
|
||||
return serialized_users
|
||||
|
||||
|
||||
@intercept_errors(UserAPIInternalError, ignore_errors=[UserAPIRequestError])
|
||||
@helpers.intercept_errors(errors.UserAPIInternalError, ignore_errors=[errors.UserAPIRequestError])
|
||||
def update_account_settings(requesting_user, update, username=None):
|
||||
"""Update user account information.
|
||||
|
||||
@@ -111,17 +102,18 @@ def update_account_settings(requesting_user, update, username=None):
|
||||
`requesting_user.username` is assumed.
|
||||
|
||||
Raises:
|
||||
UserNotFound: no user with username `username` exists (or `requesting_user.username` if
|
||||
errors.UserNotFound: no user with username `username` exists (or `requesting_user.username` if
|
||||
`username` is not specified)
|
||||
UserNotAuthorized: the requesting_user does not have access to change the account
|
||||
errors.UserNotAuthorized: the requesting_user does not have access to change the account
|
||||
associated with `username`
|
||||
AccountValidationError: the update was not attempted because validation errors were found with
|
||||
errors.AccountValidationError: the update was not attempted because validation errors were found with
|
||||
the supplied update
|
||||
AccountUpdateError: the update could not be completed. Note that if multiple fields are updated at the same
|
||||
time, some parts of the update may have been successful, even if an AccountUpdateError is returned;
|
||||
in particular, the user account (not including e-mail address) may have successfully been updated,
|
||||
errors.AccountUpdateError: the update could not be completed. Note that if multiple fields are updated at the
|
||||
same time, some parts of the update may have been successful, even if an errors.AccountUpdateError is
|
||||
returned; in particular, the user account (not including e-mail address) may have successfully been updated,
|
||||
but then the e-mail change request, which is processed last, may throw an error.
|
||||
UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
errors.UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
|
||||
"""
|
||||
if username is None:
|
||||
username = requesting_user.username
|
||||
@@ -129,7 +121,7 @@ def update_account_settings(requesting_user, update, username=None):
|
||||
existing_user, existing_user_profile = _get_user_and_profile(username)
|
||||
|
||||
if requesting_user.username != username:
|
||||
raise UserNotAuthorized()
|
||||
raise errors.UserNotAuthorized()
|
||||
|
||||
# If user has requested to change email, we must call the multi-step process to handle this.
|
||||
# It is not handled by the serializer (which considers email to be read-only).
|
||||
@@ -179,7 +171,7 @@ def update_account_settings(requesting_user, update, username=None):
|
||||
|
||||
# If we have encountered any validation errors, return them to the user.
|
||||
if field_errors:
|
||||
raise AccountValidationError(field_errors)
|
||||
raise errors.AccountValidationError(field_errors)
|
||||
|
||||
try:
|
||||
# If everything validated, go ahead and save the serializers.
|
||||
@@ -224,40 +216,26 @@ def update_account_settings(requesting_user, update, username=None):
|
||||
existing_user_profile.save()
|
||||
|
||||
except PreferenceValidationError as err:
|
||||
raise AccountValidationError(err.preference_errors)
|
||||
raise errors.AccountValidationError(err.preference_errors)
|
||||
except Exception as err:
|
||||
raise AccountUpdateError(
|
||||
raise errors.AccountUpdateError(
|
||||
u"Error thrown when saving account updates: '{}'".format(err.message)
|
||||
)
|
||||
|
||||
# And try to send the email change request if necessary.
|
||||
if changing_email:
|
||||
if not settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']:
|
||||
raise AccountUpdateError(u"Email address changes have been disabled by the site operators.")
|
||||
raise errors.AccountUpdateError(u"Email address changes have been disabled by the site operators.")
|
||||
try:
|
||||
student_views.do_email_change_request(existing_user, new_email)
|
||||
except ValueError as err:
|
||||
raise AccountUpdateError(
|
||||
raise errors.AccountUpdateError(
|
||||
u"Error thrown from do_email_change_request: '{}'".format(err.message),
|
||||
user_message=err.message
|
||||
)
|
||||
|
||||
|
||||
def _get_user_and_profile(username):
|
||||
"""
|
||||
Helper method to return the legacy user and profile objects based on username.
|
||||
"""
|
||||
try:
|
||||
existing_user = User.objects.get(username=username)
|
||||
except ObjectDoesNotExist:
|
||||
raise UserNotFound()
|
||||
|
||||
existing_user_profile, _ = UserProfile.objects.get_or_create(user=existing_user)
|
||||
|
||||
return existing_user, existing_user_profile
|
||||
|
||||
|
||||
@intercept_errors(UserAPIInternalError, ignore_errors=[UserAPIRequestError])
|
||||
@helpers.intercept_errors(errors.UserAPIInternalError, ignore_errors=[errors.UserAPIRequestError])
|
||||
@transaction.atomic
|
||||
def create_account(username, password, email):
|
||||
"""Create a new user account.
|
||||
@@ -291,11 +269,12 @@ def create_account(username, password, email):
|
||||
unicode: an activation key for the account.
|
||||
|
||||
Raises:
|
||||
AccountUserAlreadyExists
|
||||
AccountUsernameInvalid
|
||||
AccountEmailInvalid
|
||||
AccountPasswordInvalid
|
||||
UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
errors.AccountUserAlreadyExists
|
||||
errors.AccountUsernameInvalid
|
||||
errors.AccountEmailInvalid
|
||||
errors.AccountPasswordInvalid
|
||||
errors.UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
|
||||
"""
|
||||
# Check if ALLOW_PUBLIC_ACCOUNT_CREATION flag turned off to restrict user account creation
|
||||
if not configuration_helpers.get_value(
|
||||
@@ -317,7 +296,7 @@ def create_account(username, password, email):
|
||||
try:
|
||||
user.save()
|
||||
except IntegrityError:
|
||||
raise AccountUserAlreadyExists
|
||||
raise errors.AccountUserAlreadyExists
|
||||
|
||||
# Create a registration to track the activation process
|
||||
# This implicitly saves the registration.
|
||||
@@ -350,16 +329,19 @@ def check_account_exists(username=None, email=None):
|
||||
"""
|
||||
conflicts = []
|
||||
|
||||
if email is not None and User.objects.filter(email=email).exists():
|
||||
try:
|
||||
_validate_email_doesnt_exist(email)
|
||||
except errors.AccountEmailAlreadyExists:
|
||||
conflicts.append("email")
|
||||
|
||||
if username is not None and User.objects.filter(username=username).exists():
|
||||
try:
|
||||
_validate_username_doesnt_exist(username)
|
||||
except errors.AccountUsernameAlreadyExists:
|
||||
conflicts.append("username")
|
||||
|
||||
return conflicts
|
||||
|
||||
|
||||
@intercept_errors(UserAPIInternalError, ignore_errors=[UserAPIRequestError])
|
||||
@helpers.intercept_errors(errors.UserAPIInternalError, ignore_errors=[errors.UserAPIRequestError])
|
||||
def activate_account(activation_key):
|
||||
"""Activate a user's account.
|
||||
|
||||
@@ -370,19 +352,20 @@ def activate_account(activation_key):
|
||||
None
|
||||
|
||||
Raises:
|
||||
UserNotAuthorized
|
||||
UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
errors.UserNotAuthorized
|
||||
errors.UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
|
||||
"""
|
||||
try:
|
||||
registration = Registration.objects.get(activation_key=activation_key)
|
||||
except Registration.DoesNotExist:
|
||||
raise UserNotAuthorized
|
||||
raise errors.UserNotAuthorized
|
||||
else:
|
||||
# This implicitly saves the registration
|
||||
registration.activate()
|
||||
|
||||
|
||||
@intercept_errors(UserAPIInternalError, ignore_errors=[UserAPIRequestError])
|
||||
@helpers.intercept_errors(errors.UserAPIInternalError, ignore_errors=[errors.UserAPIRequestError])
|
||||
def request_password_change(email, orig_host, is_secure):
|
||||
"""Email a single-use link for performing a password reset.
|
||||
|
||||
@@ -397,13 +380,14 @@ def request_password_change(email, orig_host, is_secure):
|
||||
None
|
||||
|
||||
Raises:
|
||||
UserNotFound
|
||||
errors.UserNotFound
|
||||
AccountRequestError
|
||||
UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
errors.UserAPIInternalError: the operation failed due to an unexpected error.
|
||||
|
||||
"""
|
||||
# Binding data to a form requires that the data be passed as a dictionary
|
||||
# to the Form class constructor.
|
||||
form = PasswordResetFormNoActive({'email': email})
|
||||
form = forms.PasswordResetFormNoActive({'email': email})
|
||||
|
||||
# Validate that a user exists with the given email address.
|
||||
if form.is_valid():
|
||||
@@ -416,7 +400,135 @@ def request_password_change(email, orig_host, is_secure):
|
||||
)
|
||||
else:
|
||||
# No user with the provided email address exists.
|
||||
raise UserNotFound
|
||||
raise errors.UserNotFound
|
||||
|
||||
|
||||
def get_name_validation_error(name):
|
||||
"""Get the built-in validation error message for when
|
||||
the user's real name is invalid in some way (we wonder how).
|
||||
|
||||
:param name: The proposed user's real name.
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
return '' if name else accounts.REQUIRED_FIELD_NAME_MSG
|
||||
|
||||
|
||||
def get_username_validation_error(username):
|
||||
"""Get the built-in validation error message for when
|
||||
the username is invalid in some way.
|
||||
|
||||
:param username: The proposed username (unicode).
|
||||
:param default: The message to default to in case of no error.
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
return _validate(_validate_username, errors.AccountUsernameInvalid, username)
|
||||
|
||||
|
||||
def get_email_validation_error(email):
|
||||
"""Get the built-in validation error message for when
|
||||
the email is invalid in some way.
|
||||
|
||||
:param email: The proposed email (unicode).
|
||||
:param default: The message to default to in case of no error.
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
return _validate(_validate_email, errors.AccountEmailInvalid, email)
|
||||
|
||||
|
||||
def get_confirm_email_validation_error(confirm_email, email):
|
||||
"""Get the built-in validation error message for when
|
||||
the confirmation email is invalid in some way.
|
||||
|
||||
:param confirm_email: The proposed confirmation email (unicode).
|
||||
:param email: The email to match (unicode).
|
||||
:param default: THe message to default to in case of no error.
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
return _validate(_validate_confirm_email, errors.AccountEmailInvalid, confirm_email, email)
|
||||
|
||||
|
||||
def get_password_validation_error(password, username=None):
|
||||
"""Get the built-in validation error message for when
|
||||
the password is invalid in some way.
|
||||
|
||||
:param password: The proposed password (unicode).
|
||||
:param username: The username associated with the user's account (unicode).
|
||||
:param default: The message to default to in case of no error.
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
return _validate(_validate_password, errors.AccountPasswordInvalid, password, username)
|
||||
|
||||
|
||||
def get_country_validation_error(country):
|
||||
"""Get the built-in validation error message for when
|
||||
the country is invalid in some way.
|
||||
|
||||
:param country: The proposed country.
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
return _validate(_validate_country, errors.AccountCountryInvalid, country)
|
||||
|
||||
|
||||
def get_username_existence_validation_error(username):
|
||||
"""Get the built-in validation error message for when
|
||||
the username has an existence conflict.
|
||||
|
||||
:param username: The proposed username (unicode).
|
||||
:param default: The message to default to in case of no error.
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
return _validate(_validate_username_doesnt_exist, errors.AccountUsernameAlreadyExists, username)
|
||||
|
||||
|
||||
def get_email_existence_validation_error(email):
|
||||
"""Get the built-in validation error message for when
|
||||
the email has an existence conflict.
|
||||
|
||||
:param email: The proposed email (unicode).
|
||||
:param default: The message to default to in case of no error.
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
return _validate(_validate_email_doesnt_exist, errors.AccountEmailAlreadyExists, email)
|
||||
|
||||
|
||||
def _get_user_and_profile(username):
|
||||
"""
|
||||
Helper method to return the legacy user and profile objects based on username.
|
||||
"""
|
||||
try:
|
||||
existing_user = User.objects.get(username=username)
|
||||
except ObjectDoesNotExist:
|
||||
raise errors.UserNotFound()
|
||||
|
||||
existing_user_profile, _ = UserProfile.objects.get_or_create(user=existing_user)
|
||||
|
||||
return existing_user, existing_user_profile
|
||||
|
||||
|
||||
def _validate(validation_func, err, *args):
|
||||
"""Generic validation function that returns default on
|
||||
no errors, but the message associated with the err class
|
||||
otherwise. Passes all other arguments into the validation function.
|
||||
|
||||
:param validation_func: The function used to perform validation.
|
||||
:param err: The error class to catch.
|
||||
:param args: The arguments to pass into the validation function.
|
||||
:return: Validation error message, or empty string if no error.
|
||||
|
||||
"""
|
||||
try:
|
||||
validation_func(*args)
|
||||
except err as validation_err:
|
||||
return validation_err.message
|
||||
return ''
|
||||
|
||||
|
||||
def _validate_username(username):
|
||||
@@ -429,36 +541,62 @@ def _validate_username(username):
|
||||
None
|
||||
|
||||
Raises:
|
||||
AccountUsernameInvalid
|
||||
errors.AccountUsernameInvalid
|
||||
|
||||
"""
|
||||
if not isinstance(username, basestring):
|
||||
raise AccountUsernameInvalid(u"Username must be a string")
|
||||
|
||||
if len(username) < USERNAME_MIN_LENGTH:
|
||||
raise AccountUsernameInvalid(
|
||||
u"Username '{username}' must be at least {min} characters long".format(
|
||||
username=username,
|
||||
min=USERNAME_MIN_LENGTH
|
||||
)
|
||||
)
|
||||
if len(username) > USERNAME_MAX_LENGTH:
|
||||
raise AccountUsernameInvalid(
|
||||
u"Username '{username}' must be at most {max} characters long".format(
|
||||
username=username,
|
||||
max=USERNAME_MAX_LENGTH
|
||||
)
|
||||
)
|
||||
try:
|
||||
_validate_unicode(username)
|
||||
_validate_type(username, basestring, accounts.USERNAME_BAD_TYPE_MSG)
|
||||
_validate_length(
|
||||
username,
|
||||
accounts.USERNAME_MIN_LENGTH,
|
||||
accounts.USERNAME_MAX_LENGTH,
|
||||
accounts.USERNAME_BAD_LENGTH_MSG
|
||||
)
|
||||
with override_language('en'):
|
||||
# `validate_username` provides a proper localized message, however the API needs only the English
|
||||
# message by convention.
|
||||
student_forms.validate_username(username)
|
||||
except ValidationError as error:
|
||||
raise AccountUsernameInvalid(error.message)
|
||||
except (UnicodeError, errors.AccountDataBadType, errors.AccountDataBadLength, ValidationError) as username_err:
|
||||
raise errors.AccountUsernameInvalid(username_err.message)
|
||||
|
||||
|
||||
def _validate_password(password, username):
|
||||
def _validate_email(email):
|
||||
"""Validate the format of the email address.
|
||||
|
||||
Arguments:
|
||||
email (unicode): The proposed email.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
errors.AccountEmailInvalid
|
||||
|
||||
"""
|
||||
try:
|
||||
_validate_unicode(email)
|
||||
_validate_type(email, basestring, accounts.EMAIL_BAD_TYPE_MSG)
|
||||
_validate_length(email, accounts.EMAIL_MIN_LENGTH, accounts.EMAIL_MAX_LENGTH, accounts.EMAIL_BAD_LENGTH_MSG)
|
||||
validate_email.message = accounts.EMAIL_INVALID_MSG.format(email=email)
|
||||
validate_email(email)
|
||||
except (UnicodeError, errors.AccountDataBadType, errors.AccountDataBadLength, ValidationError) as invalid_email_err:
|
||||
raise errors.AccountEmailInvalid(invalid_email_err.message)
|
||||
|
||||
|
||||
def _validate_confirm_email(confirm_email, email):
|
||||
"""Validate the confirmation email field.
|
||||
|
||||
:param confirm_email: The proposed confirmation email. (unicode)
|
||||
:param email: The email to match. (unicode)
|
||||
:return: None
|
||||
|
||||
"""
|
||||
if not confirm_email or confirm_email != email:
|
||||
raise errors.AccountEmailInvalid(accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG)
|
||||
|
||||
|
||||
def _validate_password(password, username=None):
|
||||
"""Validate the format of the user's password.
|
||||
|
||||
Passwords cannot be the same as the username of the account,
|
||||
@@ -472,65 +610,116 @@ def _validate_password(password, username):
|
||||
None
|
||||
|
||||
Raises:
|
||||
AccountPasswordInvalid
|
||||
errors.AccountPasswordInvalid
|
||||
|
||||
"""
|
||||
if not isinstance(password, basestring):
|
||||
raise AccountPasswordInvalid(u"Password must be a string")
|
||||
|
||||
if len(password) < PASSWORD_MIN_LENGTH:
|
||||
raise AccountPasswordInvalid(
|
||||
u"Password must be at least {min} characters long".format(
|
||||
min=PASSWORD_MIN_LENGTH
|
||||
)
|
||||
)
|
||||
|
||||
if len(password) > PASSWORD_MAX_LENGTH:
|
||||
raise AccountPasswordInvalid(
|
||||
u"Password must be at most {max} characters long".format(
|
||||
max=PASSWORD_MAX_LENGTH
|
||||
)
|
||||
)
|
||||
|
||||
if password == username:
|
||||
raise AccountPasswordInvalid(u"Password cannot be the same as the username")
|
||||
|
||||
|
||||
def _validate_email(email):
|
||||
"""Validate the format of the email address.
|
||||
|
||||
Arguments:
|
||||
email (unicode): The proposed email.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
AccountEmailInvalid
|
||||
|
||||
"""
|
||||
if not isinstance(email, basestring):
|
||||
raise AccountEmailInvalid(u"Email must be a string")
|
||||
|
||||
if len(email) < EMAIL_MIN_LENGTH:
|
||||
raise AccountEmailInvalid(
|
||||
u"Email '{email}' must be at least {min} characters long".format(
|
||||
email=email,
|
||||
min=EMAIL_MIN_LENGTH
|
||||
)
|
||||
)
|
||||
|
||||
if len(email) > EMAIL_MAX_LENGTH:
|
||||
raise AccountEmailInvalid(
|
||||
u"Email '{email}' must be at most {max} characters long".format(
|
||||
email=email,
|
||||
max=EMAIL_MAX_LENGTH
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
validate_email(email)
|
||||
except ValidationError:
|
||||
raise AccountEmailInvalid(
|
||||
u"Email '{email}' format is not valid".format(email=email)
|
||||
)
|
||||
_validate_type(password, basestring, accounts.PASSWORD_BAD_TYPE_MSG)
|
||||
|
||||
if len(password) == 0:
|
||||
raise errors.AccountPasswordInvalid(accounts.PASSWORD_EMPTY_MSG)
|
||||
elif len(password) < accounts.PASSWORD_MIN_LENGTH:
|
||||
raise errors.AccountPasswordInvalid(accounts.PASSWORD_BAD_MIN_LENGTH_MSG)
|
||||
elif len(password) > accounts.PASSWORD_MAX_LENGTH:
|
||||
raise errors.AccountPasswordInvalid(accounts.PASSWORD_BAD_MAX_LENGTH_MSG)
|
||||
|
||||
_validate_password_works_with_username(password, username)
|
||||
except (errors.AccountDataBadType, errors.AccountDataBadLength) as invalid_password_err:
|
||||
raise errors.AccountPasswordInvalid(invalid_password_err.message)
|
||||
|
||||
|
||||
def _validate_country(country):
|
||||
"""Validate the country selection.
|
||||
|
||||
:param country: The proposed country.
|
||||
:return: None
|
||||
|
||||
"""
|
||||
if country == '' or country == '--':
|
||||
raise errors.AccountCountryInvalid(accounts.REQUIRED_FIELD_COUNTRY_MSG)
|
||||
|
||||
|
||||
def _validate_username_doesnt_exist(username):
|
||||
"""Validate that the username is not associated with an existing user.
|
||||
|
||||
:param username: The proposed username (unicode).
|
||||
:return: None
|
||||
:raises: errors.AccountUsernameAlreadyExists
|
||||
"""
|
||||
if username is not None and User.objects.filter(username=username).exists():
|
||||
raise errors.AccountUsernameAlreadyExists(_(accounts.USERNAME_CONFLICT_MSG).format(username=username))
|
||||
|
||||
|
||||
def _validate_email_doesnt_exist(email):
|
||||
"""Validate that the email is not associated with an existing user.
|
||||
|
||||
:param email: The proposed email (unicode).
|
||||
:return: None
|
||||
:raises: errors.AccountEmailAlreadyExists
|
||||
"""
|
||||
if email is not None and User.objects.filter(email=email).exists():
|
||||
raise errors.AccountEmailAlreadyExists(_(accounts.EMAIL_CONFLICT_MSG).format(email_address=email))
|
||||
|
||||
|
||||
def _validate_password_works_with_username(password, username=None):
|
||||
"""Run validation checks on whether the password and username
|
||||
go well together.
|
||||
|
||||
An example check is to see whether they are the same.
|
||||
|
||||
:param password: The proposed password (unicode).
|
||||
:param username: The username associated with the user's account (unicode).
|
||||
:return: None
|
||||
:raises: errors.AccountPasswordInvalid
|
||||
"""
|
||||
if password == username:
|
||||
raise errors.AccountPasswordInvalid(accounts.PASSWORD_CANT_EQUAL_USERNAME_MSG)
|
||||
|
||||
|
||||
def _validate_type(data, type, err):
|
||||
"""Checks whether the input data is of type. If not,
|
||||
throws a generic error message.
|
||||
|
||||
:param data: The data to check.
|
||||
:param type: The type to check against.
|
||||
:param err: The error message to throw back if data is not of type.
|
||||
:return: None
|
||||
:raises: errors.AccountDataBadType
|
||||
|
||||
"""
|
||||
if not isinstance(data, type):
|
||||
raise errors.AccountDataBadType(err)
|
||||
|
||||
|
||||
def _validate_length(data, min, max, err):
|
||||
"""Validate that the data's length is less than or equal to max,
|
||||
and greater than or equal to min.
|
||||
|
||||
:param data: The data to do the test on.
|
||||
:param min: The minimum allowed length.
|
||||
:param max: The maximum allowed length.
|
||||
:param err: The error message to throw back if data's length is below min or above max.
|
||||
:return: None
|
||||
:raises: errors.AccountDataBadLength
|
||||
|
||||
"""
|
||||
if len(data) < min or len(data) > max:
|
||||
raise errors.AccountDataBadLength(err)
|
||||
|
||||
|
||||
def _validate_unicode(data, err=u"Input not valid unicode"):
|
||||
"""Checks whether the input data is valid unicode or not.
|
||||
|
||||
:param data: The data to check for unicode validity.
|
||||
:param err: The error message to throw back if unicode is invalid.
|
||||
:return: None
|
||||
:raises: UnicodeError
|
||||
|
||||
"""
|
||||
try:
|
||||
if not isinstance(data, str) and not isinstance(data, unicode):
|
||||
raise UnicodeError(err)
|
||||
# In some cases we pass the above, but it's still inappropriate utf-8.
|
||||
unicode(data)
|
||||
except UnicodeError:
|
||||
raise UnicodeError(err)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Unit tests for behavior that is specific to the api methods (vs. the view methods).
|
||||
Most of the functionality is covered in test_views.py.
|
||||
"""
|
||||
|
||||
import re
|
||||
import ddt
|
||||
from dateutil.parser import parse as parse_datetime
|
||||
@@ -17,17 +18,29 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import mail
|
||||
from django.test.client import RequestFactory
|
||||
from openedx.core.djangoapps.user_api.accounts import (
|
||||
USERNAME_MAX_LENGTH,
|
||||
PRIVATE_VISIBILITY
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.accounts.api import (
|
||||
get_account_settings,
|
||||
update_account_settings,
|
||||
create_account,
|
||||
activate_account,
|
||||
request_password_change
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.errors import (
|
||||
UserNotFound, UserNotAuthorized,
|
||||
AccountUpdateError, AccountValidationError, AccountUserAlreadyExists,
|
||||
AccountUsernameInvalid, AccountEmailInvalid, AccountPasswordInvalid,
|
||||
AccountRequestError
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.accounts.tests.testutils import (
|
||||
INVALID_EMAILS, INVALID_PASSWORDS, INVALID_USERNAMES, VALID_USERNAMES_UNICODE
|
||||
)
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from student.models import PendingEmailChange
|
||||
from student.tests.tests import UserSettingsEventTestMixin
|
||||
from ...errors import (
|
||||
UserNotFound, UserNotAuthorized, AccountUpdateError, AccountValidationError,
|
||||
AccountUserAlreadyExists, AccountUsernameInvalid, AccountEmailInvalid, AccountPasswordInvalid, AccountRequestError
|
||||
)
|
||||
from ..api import (
|
||||
get_account_settings, update_account_settings, create_account, activate_account, request_password_change
|
||||
)
|
||||
from .. import USERNAME_MAX_LENGTH, EMAIL_MAX_LENGTH, PASSWORD_MAX_LENGTH, PRIVATE_VISIBILITY
|
||||
|
||||
|
||||
def mock_render_to_string(template_name, context):
|
||||
@@ -310,40 +323,6 @@ class AccountCreationActivationAndPasswordChangeTest(TestCase):
|
||||
ORIG_HOST = 'example.com'
|
||||
IS_SECURE = False
|
||||
|
||||
INVALID_USERNAMES = [
|
||||
None,
|
||||
u'',
|
||||
u'a',
|
||||
u'a' * (USERNAME_MAX_LENGTH + 1),
|
||||
u'invalid_symbol_@',
|
||||
u'invalid-unicode_fŕáńḱ',
|
||||
]
|
||||
|
||||
INVALID_EMAILS = [
|
||||
None,
|
||||
u'',
|
||||
u'a',
|
||||
'no_domain',
|
||||
'no+domain',
|
||||
'@',
|
||||
'@domain.com',
|
||||
'test@no_extension',
|
||||
u'fŕáńḱ@example.com',
|
||||
|
||||
# Long email -- subtract the length of the @domain
|
||||
# except for one character (so we exceed the max length limit)
|
||||
u'{user}@example.com'.format(
|
||||
user=(u'e' * (EMAIL_MAX_LENGTH - 11))
|
||||
)
|
||||
]
|
||||
|
||||
INVALID_PASSWORDS = [
|
||||
None,
|
||||
u'',
|
||||
u'a',
|
||||
u'a' * (PASSWORD_MAX_LENGTH + 1)
|
||||
]
|
||||
|
||||
@skip_unless_lms
|
||||
def test_activate_account(self):
|
||||
# Create the account, which is initially inactive
|
||||
@@ -471,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):
|
||||
|
||||
104
openedx/core/djangoapps/user_api/accounts/tests/testutils.py
Normal file
104
openedx/core/djangoapps/user_api/accounts/tests/testutils.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Utility functions, constants, etc. for testing.
|
||||
"""
|
||||
|
||||
from openedx.core.djangoapps.user_api.accounts import (
|
||||
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH,
|
||||
EMAIL_MAX_LENGTH,
|
||||
PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH
|
||||
)
|
||||
|
||||
|
||||
INVALID_NAMES = [
|
||||
None,
|
||||
'',
|
||||
u''
|
||||
]
|
||||
|
||||
INVALID_USERNAMES_ASCII = [
|
||||
'$invalid-ascii$',
|
||||
'invalid-fŕáńḱ',
|
||||
'@invalid-ascii@'
|
||||
]
|
||||
|
||||
INVALID_USERNAMES_UNICODE = [
|
||||
u'invalid-unicode_fŕáńḱ',
|
||||
]
|
||||
|
||||
INVALID_USERNAMES = [
|
||||
None,
|
||||
u'',
|
||||
u'a',
|
||||
u'a' * (USERNAME_MAX_LENGTH + 1),
|
||||
] + INVALID_USERNAMES_ASCII + INVALID_USERNAMES_UNICODE
|
||||
|
||||
INVALID_EMAILS = [
|
||||
None,
|
||||
u'',
|
||||
u'a',
|
||||
'no_domain',
|
||||
'no+domain',
|
||||
'@',
|
||||
'@domain.com',
|
||||
'test@no_extension',
|
||||
u'fŕáńḱ@example.com',
|
||||
|
||||
# Long email -- subtract the length of the @domain
|
||||
# except for one character (so we exceed the max length limit)
|
||||
u'{user}@example.com'.format(
|
||||
user=(u'e' * (EMAIL_MAX_LENGTH - 11))
|
||||
)
|
||||
]
|
||||
|
||||
INVALID_PASSWORDS = [
|
||||
None,
|
||||
u'',
|
||||
u'a',
|
||||
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,
|
||||
u'a' * USERNAME_MAX_LENGTH,
|
||||
u'-' * USERNAME_MIN_LENGTH,
|
||||
u'-' * USERNAME_MAX_LENGTH,
|
||||
u'_username_',
|
||||
u'-username-',
|
||||
u'-_username_-'
|
||||
]
|
||||
|
||||
VALID_EMAILS = [
|
||||
'has@domain.com'
|
||||
]
|
||||
|
||||
VALID_PASSWORDS = [
|
||||
u'password', # :)
|
||||
u'a' * PASSWORD_MIN_LENGTH,
|
||||
u'a' * PASSWORD_MAX_LENGTH
|
||||
]
|
||||
|
||||
VALID_COUNTRIES = [
|
||||
u'PK',
|
||||
u'Pakistan',
|
||||
u'US'
|
||||
]
|
||||
@@ -33,6 +33,16 @@ class AccountUserAlreadyExists(AccountRequestError):
|
||||
pass
|
||||
|
||||
|
||||
class AccountUsernameAlreadyExists(AccountRequestError):
|
||||
"""User with the same username already exists. """
|
||||
pass
|
||||
|
||||
|
||||
class AccountEmailAlreadyExists(AccountRequestError):
|
||||
"""User with the same email already exists. """
|
||||
pass
|
||||
|
||||
|
||||
class AccountUsernameInvalid(AccountRequestError):
|
||||
"""The requested username is not in a valid format. """
|
||||
pass
|
||||
@@ -48,6 +58,21 @@ 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
|
||||
|
||||
|
||||
class AccountDataBadType(AccountRequestError):
|
||||
"""The requested account data is of the wrong type. """
|
||||
pass
|
||||
|
||||
|
||||
class AccountUpdateError(AccountRequestError):
|
||||
"""
|
||||
An update to the account failed. More detailed information is present in developer_message,
|
||||
|
||||
@@ -31,10 +31,9 @@ from third_party_auth.tests.utils import (
|
||||
from .test_helpers import TestCaseForm
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from ..helpers import FormDescription
|
||||
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
|
||||
@@ -1199,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."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1225,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."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1302,6 +1307,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
"type": "textarea",
|
||||
"required": False,
|
||||
"label": "Mailing address",
|
||||
"errorMessages": {
|
||||
"required": "Please enter your mailing address."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1314,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."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1326,6 +1337,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
"type": "text",
|
||||
"required": False,
|
||||
"label": "City",
|
||||
"errorMessages": {
|
||||
"required": "Please enter your City."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1993,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"}],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from ..profile_images.views import ProfileImageView
|
||||
from .accounts.views import AccountDeactivationView, AccountViewSet
|
||||
from .preferences.views import PreferencesDetailView, PreferencesView
|
||||
from .verification_api.views import PhotoVerificationStatusView
|
||||
from .validation.views import RegistrationValidationView
|
||||
|
||||
ME = AccountViewSet.as_view({
|
||||
'get': 'get',
|
||||
@@ -25,9 +26,21 @@ ACCOUNT_DETAIL = AccountViewSet.as_view({
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^v1/me$', ME, name='own_username_api'),
|
||||
url(r'^v1/accounts/{}$'.format(settings.USERNAME_PATTERN), ACCOUNT_DETAIL, name='accounts_api'),
|
||||
url(r'^v1/accounts$', ACCOUNT_LIST, name='accounts_detail_api'),
|
||||
url(
|
||||
r'^v1/me$',
|
||||
ME,
|
||||
name='own_username_api'
|
||||
),
|
||||
url(
|
||||
r'^v1/accounts$',
|
||||
ACCOUNT_LIST,
|
||||
name='accounts_detail_api'
|
||||
),
|
||||
url(
|
||||
r'^v1/accounts/{}$'.format(settings.USERNAME_PATTERN),
|
||||
ACCOUNT_DETAIL,
|
||||
name='accounts_api'
|
||||
),
|
||||
url(
|
||||
r'^v1/accounts/{}/image$'.format(settings.USERNAME_PATTERN),
|
||||
ProfileImageView.as_view(),
|
||||
@@ -43,6 +56,11 @@ urlpatterns = patterns(
|
||||
PhotoVerificationStatusView.as_view(),
|
||||
name='verification_status'
|
||||
),
|
||||
url(
|
||||
r'^v1/validation/registration$',
|
||||
RegistrationValidationView.as_view(),
|
||||
name='registration_validation'
|
||||
),
|
||||
url(
|
||||
r'^v1/preferences/{}$'.format(settings.USERNAME_PATTERN),
|
||||
PreferencesView.as_view(),
|
||||
|
||||
196
openedx/core/djangoapps/user_api/validation/tests/test_views.py
Normal file
196
openedx/core/djangoapps/user_api/validation/tests/test_views.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests for an API endpoint for client-side user data validation.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||
"""
|
||||
Tests for validity of user data in registration forms.
|
||||
"""
|
||||
|
||||
endpoint_name = 'registration_validation'
|
||||
path = reverse(endpoint_name)
|
||||
|
||||
def get_validation_decision(self, data):
|
||||
response = self.client.post(self.path, data)
|
||||
return response.data.get('validation_decisions', {})
|
||||
|
||||
def assertValidationDecision(self, data, decision):
|
||||
self.assertEqual(
|
||||
self.get_validation_decision(data),
|
||||
decision
|
||||
)
|
||||
|
||||
def assertNotValidationDecision(self, data, decision):
|
||||
self.assertNotEqual(
|
||||
self.get_validation_decision(data),
|
||||
decision
|
||||
)
|
||||
|
||||
def test_no_decision_for_empty_request(self):
|
||||
self.assertValidationDecision(
|
||||
{},
|
||||
{}
|
||||
)
|
||||
|
||||
def test_no_decision_for_invalid_request(self):
|
||||
self.assertValidationDecision(
|
||||
{'invalid_field': 'random_user_data'},
|
||||
{}
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
['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):
|
||||
"""
|
||||
Test if {0} as any item in {1} gives a positive validation decision.
|
||||
"""
|
||||
self.assertValidationDecision(
|
||||
{form_field_name: user_data},
|
||||
{form_field_name: ''}
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
# Skip None type for invalidity checks.
|
||||
['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.assertNotValidationDecision(
|
||||
{form_field_name: user_data},
|
||||
{form_field_name: ''}
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
['username', 'username@email.com'], # No conflict
|
||||
['user', 'username@email.com'], # Username conflict
|
||||
['username', 'user@email.com'], # Email conflict
|
||||
['user', 'user@email.com'] # Both conflict
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_existence_conflict(self, username, email):
|
||||
"""
|
||||
Test if username '{0}' and email '{1}' have conflicts with
|
||||
username 'user' and email 'user@email.com'.
|
||||
"""
|
||||
user = User.objects.create_user(username='user', email='user@email.com')
|
||||
self.assertValidationDecision(
|
||||
{
|
||||
'username': username,
|
||||
'email': email
|
||||
},
|
||||
{
|
||||
"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' * accounts.EMAIL_MAX_LENGTH) + '@email.com')
|
||||
def test_email_bad_length_validation_decision(self, email):
|
||||
self.assertValidationDecision(
|
||||
{'email': email},
|
||||
{'email': accounts.EMAIL_BAD_LENGTH_MSG}
|
||||
)
|
||||
|
||||
def test_email_generically_invalid_validation_decision(self):
|
||||
email = 'email'
|
||||
self.assertValidationDecision(
|
||||
{'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' * (accounts.USERNAME_MIN_LENGTH - 1),
|
||||
'u' * (accounts.USERNAME_MAX_LENGTH + 1)
|
||||
)
|
||||
def test_username_bad_length_validation_decision(self, username):
|
||||
self.assertValidationDecision(
|
||||
{'username': username},
|
||||
{'username': accounts.USERNAME_BAD_LENGTH_MSG}
|
||||
)
|
||||
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames disabled.")
|
||||
@ddt.data(*testutils.INVALID_USERNAMES_UNICODE)
|
||||
def test_username_invalid_unicode_validation_decision(self, username):
|
||||
self.assertValidationDecision(
|
||||
{'username': username},
|
||||
{'username': accounts.USERNAME_INVALID_CHARS_UNICODE}
|
||||
)
|
||||
|
||||
@unittest.skipIf(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames enabled.")
|
||||
@ddt.data(*testutils.INVALID_USERNAMES_ASCII)
|
||||
def test_username_invalid_ascii_validation_decision(self, username):
|
||||
self.assertValidationDecision(
|
||||
{'username': username},
|
||||
{"username": accounts.USERNAME_INVALID_CHARS_ASCII}
|
||||
)
|
||||
|
||||
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": accounts.PASSWORD_BAD_MAX_LENGTH_MSG}
|
||||
)
|
||||
|
||||
def test_password_equals_username_validation_decision(self):
|
||||
self.assertValidationDecision(
|
||||
{"username": "somephrase", "password": "somephrase"},
|
||||
{"username": "", "password": accounts.PASSWORD_CANT_EQUAL_USERNAME_MSG}
|
||||
)
|
||||
180
openedx/core/djangoapps/user_api/validation/views.py
Normal file
180
openedx/core/djangoapps/user_api/validation/views.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
An API for client-side validation of (potential) user data.
|
||||
"""
|
||||
|
||||
from rest_framework.response import Response
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class RegistrationValidationView(APIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
|
||||
Get validation information about user data during registration.
|
||||
Client-side may request validation for any number of form fields,
|
||||
and the API will return a conclusion from its analysis for each
|
||||
input (i.e. valid or not valid, or a custom, detailed message).
|
||||
|
||||
**Example Requests and Responses**
|
||||
|
||||
- Checks the validity of the username and email inputs separately.
|
||||
POST /api/user/v1/validation/registration/
|
||||
>>> {
|
||||
>>> "username": "hi_im_new",
|
||||
>>> "email": "newguy101@edx.org"
|
||||
>>> }
|
||||
RESPONSE
|
||||
>>> {
|
||||
>>> "validation_decisions": {
|
||||
>>> "username": "",
|
||||
>>> "email": ""
|
||||
>>> }
|
||||
>>> }
|
||||
Empty strings indicate that there was no problem with the input.
|
||||
|
||||
- Checks the validity of the password field (its validity depends
|
||||
upon both the username and password fields, so we need both). If
|
||||
only password is input, we don't check for password/username
|
||||
compatibility issues.
|
||||
POST /api/user/v1/validation/registration/
|
||||
>>> {
|
||||
>>> "username": "myname",
|
||||
>>> "password": "myname"
|
||||
>>> }
|
||||
RESPONSE
|
||||
>>> {
|
||||
>>> "validation_decisions": {
|
||||
>>> "username": "",
|
||||
>>> "password": "Password cannot be the same as the username"
|
||||
>>> }
|
||||
>>> }
|
||||
|
||||
- Checks the validity of the username, email, and password fields
|
||||
separately, and also tells whether an account exists. The password
|
||||
field's validity depends upon both the username and password, and
|
||||
the account's existence depends upon both the username and email.
|
||||
POST /api/user/v1/validation/registration/
|
||||
>>> {
|
||||
>>> "username": "hi_im_new",
|
||||
>>> "email": "cto@edx.org",
|
||||
>>> "password": "p"
|
||||
>>> }
|
||||
RESPONSE
|
||||
>>> {
|
||||
>>> "validation_decisions": {
|
||||
>>> "username": "",
|
||||
>>> "email": "It looks like cto@edx.org belongs to an existing account. Try again with a different email address.",
|
||||
>>> "password": "Password must be at least 2 characters long",
|
||||
>>> }
|
||||
>>> }
|
||||
In this example, username is valid and (we assume) there is
|
||||
a preexisting account with that email. The password also seems
|
||||
to contain the username.
|
||||
|
||||
Note that a validation decision is returned *for all* inputs, whether
|
||||
positive or negative.
|
||||
|
||||
**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)
|
||||
# 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)
|
||||
# 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', 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):
|
||||
"""
|
||||
POST /api/user/v1/validation/registration/
|
||||
|
||||
Expects request of the form
|
||||
>>> {
|
||||
>>> "name": "Dan the Validator",
|
||||
>>> "username": "mslm",
|
||||
>>> "email": "mslm@gmail.com",
|
||||
>>> "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
|
||||
can get extra verification checks if entered along with others,
|
||||
like when the password may not equal the username.
|
||||
"""
|
||||
validation_decisions = {}
|
||||
for form_field_key in self.validation_handlers:
|
||||
# For every field requiring validation from the client,
|
||||
# request a decision for it from the appropriate handler.
|
||||
if form_field_key in request.data:
|
||||
handler = self.validation_handlers[form_field_key]
|
||||
validation_decisions.update({
|
||||
form_field_key: handler(self, request)
|
||||
})
|
||||
return Response({"validation_decisions": validation_decisions})
|
||||
@@ -23,6 +23,7 @@ import third_party_auth
|
||||
from django_comment_common.models import Role
|
||||
from edxmako.shortcuts import marketing_link
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.user_api.accounts.api import check_account_exists
|
||||
from openedx.core.lib.api.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission
|
||||
from openedx.features.enterprise_support.api import enterprise_customer_for_request
|
||||
@@ -31,16 +32,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
|
||||
)
|
||||
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
|
||||
@@ -92,8 +84,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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -106,8 +98,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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -340,18 +332,8 @@ class RegistrationView(APIView):
|
||||
conflicts = check_account_exists(email=email, username=username)
|
||||
if conflicts:
|
||||
conflict_messages = {
|
||||
"email": _(
|
||||
# Translators: This message is shown to users who attempt to create a new
|
||||
# account using an email address associated with an existing account.
|
||||
u"It looks like {email_address} belongs to an existing account. "
|
||||
u"Try again with a different email address."
|
||||
).format(email_address=email),
|
||||
"username": _(
|
||||
# Translators: This message is shown to users who attempt to create a new
|
||||
# account using a username associated with an existing account.
|
||||
u"It looks like {username} belongs to an existing account. "
|
||||
u"Try again with a different username."
|
||||
).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]}]
|
||||
@@ -425,8 +407,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
|
||||
)
|
||||
@@ -444,7 +426,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",
|
||||
@@ -483,7 +465,7 @@ class RegistrationView(APIView):
|
||||
placeholder=name_placeholder,
|
||||
instructions=name_instructions,
|
||||
restrictions={
|
||||
"max_length": NAME_MAX_LENGTH,
|
||||
"max_length": accounts.NAME_MAX_LENGTH,
|
||||
},
|
||||
required=required
|
||||
)
|
||||
@@ -519,8 +501,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
|
||||
)
|
||||
@@ -544,8 +526,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
|
||||
)
|
||||
@@ -563,6 +545,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
|
||||
@@ -572,7 +555,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):
|
||||
@@ -637,12 +623,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):
|
||||
@@ -660,12 +650,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):
|
||||
@@ -681,11 +675,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):
|
||||
@@ -801,7 +799,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')
|
||||
@@ -1036,8 +1034,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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1105,7 +1103,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):
|
||||
|
||||
@@ -28,7 +28,7 @@ class ApiTestCase(TestCase):
|
||||
return getattr(self.client, method)(*args, HTTP_X_EDX_API_KEY=TEST_API_KEY, **kwargs)
|
||||
|
||||
def get_json(self, *args, **kwargs):
|
||||
"""Make a request with the given args and return the parsed JSON repsonse"""
|
||||
"""Make a request with the given args and return the parsed JSON response"""
|
||||
resp = self.request_with_auth("get", *args, **kwargs)
|
||||
self.assertHttpOK(resp)
|
||||
self.assertTrue(resp["Content-Type"].startswith("application/json"))
|
||||
|
||||
Reference in New Issue
Block a user