Implement client-side registration form validation.
Input forms that need validation will have AJAX requests performed to get validation decisions live. All but a few important and common form fields perform generic validation; these will need a back-end handler in the future in order to have them validated through AJAX requests. Information is conveyed on focus and blur for both errors and successes.
This commit is contained in:
@@ -45,7 +45,7 @@ USERNAME_INVALID_CHARS_UNICODE = _(
|
||||
|
||||
# Translators: This message is shown to users who attempt to create a new account using
|
||||
# an invalid email format.
|
||||
EMAIL_INVALID_MSG = _(u"Email '{email}' format is not valid")
|
||||
EMAIL_INVALID_MSG = _(u'"{email}" is not a valid email address.')
|
||||
|
||||
# Translators: This message is shown to users who attempt to create a new
|
||||
# account using an username/email associated with an existing account.
|
||||
@@ -60,15 +60,31 @@ USERNAME_CONFLICT_MSG = _(
|
||||
|
||||
# Translators: This message is shown to users who enter a username/email/password
|
||||
# with an inappropriate length (too short or too long).
|
||||
USERNAME_BAD_LENGTH_MSG = _(u"Username '{username}' must be between {min} and {max} characters long")
|
||||
EMAIL_BAD_LENGTH_MSG = _(u"Email '{email}' must be between {min} and {max} characters long")
|
||||
PASSWORD_BAD_LENGTH_MSG = _(u"Password must be between {min} and {max} characters long")
|
||||
USERNAME_BAD_LENGTH_MSG = _(u"Username must be between {min} and {max} characters long.").format(
|
||||
min=USERNAME_MIN_LENGTH, max=USERNAME_MAX_LENGTH
|
||||
)
|
||||
EMAIL_BAD_LENGTH_MSG = _(u"Enter a valid email address that contains at least {min} characters.").format(
|
||||
min=EMAIL_MIN_LENGTH
|
||||
)
|
||||
PASSWORD_EMPTY_MSG = _(u"Please enter a password.")
|
||||
PASSWORD_BAD_MIN_LENGTH_MSG = _(u"Password is not long enough.")
|
||||
PASSWORD_BAD_MAX_LENGTH_MSG = _(u"Password cannot be longer than {max} character.").format(max=PASSWORD_MAX_LENGTH)
|
||||
|
||||
# These strings are normally not user-facing.
|
||||
USERNAME_BAD_TYPE_MSG = u"Username must be a string"
|
||||
EMAIL_BAD_TYPE_MSG = u"Email must be a string"
|
||||
PASSWORD_BAD_TYPE_MSG = u"Password must be a string"
|
||||
USERNAME_BAD_TYPE_MSG = u"Username must be a string."
|
||||
EMAIL_BAD_TYPE_MSG = u"Email must be a string."
|
||||
PASSWORD_BAD_TYPE_MSG = u"Password must be a string."
|
||||
|
||||
# Translators: This message is shown to users who enter a password matching
|
||||
# the username they enter(ed).
|
||||
PASSWORD_CANT_EQUAL_USERNAME_MSG = _(u"Password cannot be the same as the username")
|
||||
PASSWORD_CANT_EQUAL_USERNAME_MSG = _(u"Password cannot be the same as the username.")
|
||||
|
||||
# Translators: These messages are shown to users who do not enter information
|
||||
# into the required field or enter it incorrectly.
|
||||
REQUIRED_FIELD_NAME_MSG = _(u"Please enter your Full Name.")
|
||||
REQUIRED_FIELD_CONFIRM_EMAIL_MSG = _(u"The email addresses do not match.")
|
||||
REQUIRED_FIELD_COUNTRY_MSG = _(u"Please select your Country.")
|
||||
REQUIRED_FIELD_CITY_MSG = _(u"Please enter your City.")
|
||||
REQUIRED_FIELD_GOALS_MSG = _(u"Please tell us your goals.")
|
||||
REQUIRED_FIELD_LEVEL_OF_EDUCATION_MSG = _(u"Please select your highest level of education completed.")
|
||||
REQUIRED_FIELD_MAILING_ADDRESS_MSG = _(u"Please enter your mailing address.")
|
||||
|
||||
@@ -20,37 +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,
|
||||
AccountDataBadLength, AccountDataBadType,
|
||||
AccountUsernameInvalid, AccountPasswordInvalid, AccountEmailInvalid,
|
||||
AccountUserAlreadyExists, AccountUsernameAlreadyExists, AccountEmailAlreadyExists,
|
||||
UserAPIInternalError, UserAPIRequestError, UserNotFound, UserNotAuthorized
|
||||
)
|
||||
from ..forms import PasswordResetFormNoActive
|
||||
from ..helpers import intercept_errors
|
||||
|
||||
from . import (
|
||||
EMAIL_BAD_LENGTH_MSG, PASSWORD_BAD_LENGTH_MSG, USERNAME_BAD_LENGTH_MSG,
|
||||
EMAIL_BAD_TYPE_MSG, PASSWORD_BAD_TYPE_MSG, USERNAME_BAD_TYPE_MSG,
|
||||
EMAIL_CONFLICT_MSG, USERNAME_CONFLICT_MSG,
|
||||
EMAIL_INVALID_MSG, USERNAME_INVALID_MSG,
|
||||
EMAIL_MIN_LENGTH, PASSWORD_MIN_LENGTH, USERNAME_MIN_LENGTH,
|
||||
EMAIL_MAX_LENGTH, PASSWORD_MAX_LENGTH, USERNAME_MAX_LENGTH,
|
||||
PASSWORD_CANT_EQUAL_USERNAME_MSG
|
||||
)
|
||||
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.
|
||||
|
||||
@@ -75,9 +57,9 @@ 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
|
||||
@@ -85,7 +67,7 @@ def get_account_settings(request, usernames=None, configuration=None, view=None)
|
||||
|
||||
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:
|
||||
@@ -104,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.
|
||||
|
||||
@@ -120,17 +102,17 @@ 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:
|
||||
@@ -139,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).
|
||||
@@ -189,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.
|
||||
@@ -234,26 +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
|
||||
)
|
||||
|
||||
|
||||
@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.
|
||||
@@ -287,11 +269,11 @@ 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
|
||||
@@ -314,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.
|
||||
@@ -349,17 +331,17 @@ def check_account_exists(username=None, email=None):
|
||||
|
||||
try:
|
||||
_validate_email_doesnt_exist(email)
|
||||
except AccountEmailAlreadyExists:
|
||||
except errors.AccountEmailAlreadyExists:
|
||||
conflicts.append("email")
|
||||
try:
|
||||
_validate_username_doesnt_exist(username)
|
||||
except AccountUsernameAlreadyExists:
|
||||
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,20 +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.
|
||||
|
||||
@@ -398,14 +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():
|
||||
@@ -418,10 +400,21 @@ 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_username_validation_error(username, default=''):
|
||||
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.
|
||||
|
||||
@@ -430,14 +423,10 @@ def get_username_validation_error(username, default=''):
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
try:
|
||||
_validate_username(username)
|
||||
except AccountUsernameInvalid as invalid_username_err:
|
||||
return invalid_username_err.message
|
||||
return default
|
||||
return _validate(_validate_username, errors.AccountUsernameInvalid, username)
|
||||
|
||||
|
||||
def get_email_validation_error(email, default=''):
|
||||
def get_email_validation_error(email):
|
||||
"""Get the built-in validation error message for when
|
||||
the email is invalid in some way.
|
||||
|
||||
@@ -446,14 +435,23 @@ def get_email_validation_error(email, default=''):
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
try:
|
||||
_validate_email(email)
|
||||
except AccountEmailInvalid as invalid_email_err:
|
||||
return invalid_email_err.message
|
||||
return default
|
||||
return _validate(_validate_email, errors.AccountEmailInvalid, email)
|
||||
|
||||
|
||||
def get_password_validation_error(password, username=None, default=''):
|
||||
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.
|
||||
|
||||
@@ -463,14 +461,21 @@ def get_password_validation_error(password, username=None, default=''):
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
try:
|
||||
_validate_password(password, username)
|
||||
except AccountPasswordInvalid as invalid_password_err:
|
||||
return invalid_password_err.message
|
||||
return default
|
||||
return _validate(_validate_password, errors.AccountPasswordInvalid, password, username)
|
||||
|
||||
|
||||
def get_username_existence_validation_error(username, default=''):
|
||||
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.
|
||||
|
||||
@@ -479,14 +484,10 @@ def get_username_existence_validation_error(username, default=''):
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
try:
|
||||
_validate_username_doesnt_exist(username)
|
||||
except AccountUsernameAlreadyExists as username_exists_err:
|
||||
return username_exists_err.message
|
||||
return default
|
||||
return _validate(_validate_username_doesnt_exist, errors.AccountUsernameAlreadyExists, username)
|
||||
|
||||
|
||||
def get_email_existence_validation_error(email, default=''):
|
||||
def get_email_existence_validation_error(email):
|
||||
"""Get the built-in validation error message for when
|
||||
the email has an existence conflict.
|
||||
|
||||
@@ -495,11 +496,7 @@ def get_email_existence_validation_error(email, default=''):
|
||||
:return: Validation error message.
|
||||
|
||||
"""
|
||||
try:
|
||||
_validate_email_doesnt_exist(email)
|
||||
except AccountEmailAlreadyExists as email_exists_err:
|
||||
return email_exists_err.message
|
||||
return default
|
||||
return _validate(_validate_email_doesnt_exist, errors.AccountEmailAlreadyExists, email)
|
||||
|
||||
|
||||
def _get_user_and_profile(username):
|
||||
@@ -509,13 +506,31 @@ def _get_user_and_profile(username):
|
||||
try:
|
||||
existing_user = User.objects.get(username=username)
|
||||
except ObjectDoesNotExist:
|
||||
raise UserNotFound()
|
||||
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):
|
||||
"""Validate the username.
|
||||
|
||||
@@ -526,25 +541,24 @@ def _validate_username(username):
|
||||
None
|
||||
|
||||
Raises:
|
||||
AccountUsernameInvalid
|
||||
errors.AccountUsernameInvalid
|
||||
|
||||
"""
|
||||
try:
|
||||
_validate_unicode(username)
|
||||
_validate_type(username, basestring, USERNAME_BAD_TYPE_MSG)
|
||||
_validate_type(username, basestring, accounts.USERNAME_BAD_TYPE_MSG)
|
||||
_validate_length(
|
||||
username, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, USERNAME_BAD_LENGTH_MSG.format(
|
||||
username=username,
|
||||
min=USERNAME_MIN_LENGTH,
|
||||
max=USERNAME_MAX_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 (UnicodeError, AccountDataBadType, AccountDataBadLength, ValidationError) as invalid_username_err:
|
||||
raise AccountUsernameInvalid(invalid_username_err.message)
|
||||
except (UnicodeError, errors.AccountDataBadType, errors.AccountDataBadLength, ValidationError) as username_err:
|
||||
raise errors.AccountUsernameInvalid(username_err.message)
|
||||
|
||||
|
||||
def _validate_email(email):
|
||||
@@ -557,23 +571,29 @@ def _validate_email(email):
|
||||
None
|
||||
|
||||
Raises:
|
||||
AccountEmailInvalid
|
||||
errors.AccountEmailInvalid
|
||||
|
||||
"""
|
||||
try:
|
||||
_validate_unicode(email)
|
||||
_validate_type(email, basestring, EMAIL_BAD_TYPE_MSG)
|
||||
_validate_length(
|
||||
email, EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, EMAIL_BAD_LENGTH_MSG.format(
|
||||
email=email,
|
||||
min=EMAIL_MIN_LENGTH,
|
||||
max=EMAIL_MAX_LENGTH
|
||||
)
|
||||
)
|
||||
validate_email.message = EMAIL_INVALID_MSG.format(email=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, AccountDataBadType, AccountDataBadLength, ValidationError) as invalid_email_err:
|
||||
raise AccountEmailInvalid(invalid_email_err.message)
|
||||
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):
|
||||
@@ -590,20 +610,33 @@ def _validate_password(password, username=None):
|
||||
None
|
||||
|
||||
Raises:
|
||||
AccountPasswordInvalid
|
||||
errors.AccountPasswordInvalid
|
||||
|
||||
"""
|
||||
try:
|
||||
_validate_type(password, basestring, PASSWORD_BAD_TYPE_MSG)
|
||||
_validate_length(
|
||||
password, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH, PASSWORD_BAD_LENGTH_MSG.format(
|
||||
min=PASSWORD_MIN_LENGTH,
|
||||
max=PASSWORD_MAX_LENGTH
|
||||
)
|
||||
)
|
||||
_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 (AccountDataBadType, AccountDataBadLength) as invalid_password_err:
|
||||
raise AccountPasswordInvalid(invalid_password_err.message)
|
||||
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):
|
||||
@@ -611,10 +644,10 @@ def _validate_username_doesnt_exist(username):
|
||||
|
||||
:param username: The proposed username (unicode).
|
||||
:return: None
|
||||
:raises: AccountUsernameAlreadyExists
|
||||
:raises: errors.AccountUsernameAlreadyExists
|
||||
"""
|
||||
if username is not None and User.objects.filter(username=username).exists():
|
||||
raise AccountUsernameAlreadyExists(_(USERNAME_CONFLICT_MSG).format(username=username))
|
||||
raise errors.AccountUsernameAlreadyExists(_(accounts.USERNAME_CONFLICT_MSG).format(username=username))
|
||||
|
||||
|
||||
def _validate_email_doesnt_exist(email):
|
||||
@@ -622,10 +655,10 @@ def _validate_email_doesnt_exist(email):
|
||||
|
||||
:param email: The proposed email (unicode).
|
||||
:return: None
|
||||
:raises: AccountEmailAlreadyExists
|
||||
:raises: errors.AccountEmailAlreadyExists
|
||||
"""
|
||||
if email is not None and User.objects.filter(email=email).exists():
|
||||
raise AccountEmailAlreadyExists(_(EMAIL_CONFLICT_MSG).format(email_address=email))
|
||||
raise errors.AccountEmailAlreadyExists(_(accounts.EMAIL_CONFLICT_MSG).format(email_address=email))
|
||||
|
||||
|
||||
def _validate_password_works_with_username(password, username=None):
|
||||
@@ -637,10 +670,10 @@ def _validate_password_works_with_username(password, username=None):
|
||||
:param password: The proposed password (unicode).
|
||||
:param username: The username associated with the user's account (unicode).
|
||||
:return: None
|
||||
:raises: AccountPasswordInvalid
|
||||
:raises: errors.AccountPasswordInvalid
|
||||
"""
|
||||
if password == username:
|
||||
raise AccountPasswordInvalid(PASSWORD_CANT_EQUAL_USERNAME_MSG)
|
||||
raise errors.AccountPasswordInvalid(accounts.PASSWORD_CANT_EQUAL_USERNAME_MSG)
|
||||
|
||||
|
||||
def _validate_type(data, type, err):
|
||||
@@ -651,11 +684,11 @@ def _validate_type(data, type, err):
|
||||
:param type: The type to check against.
|
||||
:param err: The error message to throw back if data is not of type.
|
||||
:return: None
|
||||
:raises: AccountDataBadType
|
||||
:raises: errors.AccountDataBadType
|
||||
|
||||
"""
|
||||
if not isinstance(data, type):
|
||||
raise AccountDataBadType(err)
|
||||
raise errors.AccountDataBadType(err)
|
||||
|
||||
|
||||
def _validate_length(data, min, max, err):
|
||||
@@ -665,12 +698,13 @@ def _validate_length(data, min, max, err):
|
||||
: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: AccountDataBadLength
|
||||
:raises: errors.AccountDataBadLength
|
||||
|
||||
"""
|
||||
if len(data) < min or len(data) > max:
|
||||
raise AccountDataBadLength(err)
|
||||
raise errors.AccountDataBadLength(err)
|
||||
|
||||
|
||||
def _validate_unicode(data, err=u"Input not valid unicode"):
|
||||
@@ -684,8 +718,8 @@ def _validate_unicode(data, err=u"Input not valid unicode"):
|
||||
"""
|
||||
try:
|
||||
if not isinstance(data, str) and not isinstance(data, unicode):
|
||||
raise UnicodeError
|
||||
raise UnicodeError(err)
|
||||
# In some cases we pass the above, but it's still inappropriate utf-8.
|
||||
str(data)
|
||||
unicode(data)
|
||||
except UnicodeError:
|
||||
raise UnicodeError(err)
|
||||
|
||||
@@ -36,7 +36,7 @@ from openedx.core.djangoapps.user_api.errors import (
|
||||
AccountRequestError
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.accounts.tests.testutils import (
|
||||
INVALID_EMAILS, INVALID_PASSWORDS, INVALID_USERNAMES
|
||||
INVALID_EMAILS, INVALID_PASSWORDS, INVALID_USERNAMES, VALID_USERNAMES_UNICODE
|
||||
)
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from student.models import PendingEmailChange
|
||||
@@ -450,14 +450,7 @@ class AccountCreationUnicodeUsernameTest(TestCase):
|
||||
PASSWORD = u'unicode-user-password'
|
||||
EMAIL = u'unicode-user-username@example.com'
|
||||
|
||||
UNICODE_USERNAMES = [
|
||||
u'Enchanté',
|
||||
u'username_with_@',
|
||||
u'username with spaces',
|
||||
u'eastern_arabic_numbers_١٢٣',
|
||||
]
|
||||
|
||||
@ddt.data(*UNICODE_USERNAMES)
|
||||
@ddt.data(*VALID_USERNAMES_UNICODE)
|
||||
def test_unicode_usernames(self, unicode_username):
|
||||
with patch.dict(settings.FEATURES, {'ENABLE_UNICODE_USERNAME': False}):
|
||||
with self.assertRaises(AccountUsernameInvalid):
|
||||
|
||||
@@ -10,6 +10,12 @@ from openedx.core.djangoapps.user_api.accounts import (
|
||||
)
|
||||
|
||||
|
||||
INVALID_NAMES = [
|
||||
None,
|
||||
'',
|
||||
u''
|
||||
]
|
||||
|
||||
INVALID_USERNAMES_ASCII = [
|
||||
'$invalid-ascii$',
|
||||
'invalid-fŕáńḱ',
|
||||
@@ -52,6 +58,24 @@ INVALID_PASSWORDS = [
|
||||
u'a' * (PASSWORD_MAX_LENGTH + 1)
|
||||
]
|
||||
|
||||
INVALID_COUNTRIES = [
|
||||
None,
|
||||
"",
|
||||
"--"
|
||||
]
|
||||
|
||||
VALID_NAMES = [
|
||||
'Validation Bot',
|
||||
u'Validation Bot'
|
||||
]
|
||||
|
||||
VALID_USERNAMES_UNICODE = [
|
||||
u'Enchanté',
|
||||
u'username_with_@',
|
||||
u'username with spaces',
|
||||
u'eastern_arabic_numbers_١٢٣',
|
||||
]
|
||||
|
||||
VALID_USERNAMES = [
|
||||
u'username',
|
||||
u'a' * USERNAME_MIN_LENGTH,
|
||||
@@ -72,3 +96,9 @@ VALID_PASSWORDS = [
|
||||
u'a' * PASSWORD_MIN_LENGTH,
|
||||
u'a' * PASSWORD_MAX_LENGTH
|
||||
]
|
||||
|
||||
VALID_COUNTRIES = [
|
||||
u'PK',
|
||||
u'Pakistan',
|
||||
u'US'
|
||||
]
|
||||
|
||||
@@ -58,6 +58,11 @@ class AccountPasswordInvalid(AccountRequestError):
|
||||
pass
|
||||
|
||||
|
||||
class AccountCountryInvalid(AccountRequestError):
|
||||
"""The requested country does not exist. """
|
||||
pass
|
||||
|
||||
|
||||
class AccountDataBadLength(AccountRequestError):
|
||||
"""The requested account data is either too short or too long. """
|
||||
pass
|
||||
|
||||
@@ -33,7 +33,7 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from ..accounts import (
|
||||
NAME_MAX_LENGTH, EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
|
||||
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH
|
||||
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, USERNAME_BAD_LENGTH_MSG
|
||||
)
|
||||
from ..accounts.api import get_account_settings
|
||||
from ..models import UserOrgTag
|
||||
@@ -1198,6 +1198,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
{"value": "none", "name": "No formal education", "default": False},
|
||||
{"value": "other", "name": "Other education", "default": False},
|
||||
],
|
||||
"errorMessages": {
|
||||
"required": "Please select your highest level of education completed."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1224,6 +1227,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
{"value": "none", "name": "No formal education TRANSLATED", "default": False},
|
||||
{"value": "other", "name": "Other education TRANSLATED", "default": False},
|
||||
],
|
||||
"errorMessages": {
|
||||
"required": "Please select your highest level of education completed."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1301,6 +1307,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
"type": "textarea",
|
||||
"required": False,
|
||||
"label": "Mailing address",
|
||||
"errorMessages": {
|
||||
"required": "Please enter your mailing address."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1313,7 +1322,10 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
"required": False,
|
||||
"label": u"Tell us why you're interested in {platform_name}".format(
|
||||
platform_name=settings.PLATFORM_NAME
|
||||
)
|
||||
),
|
||||
"errorMessages": {
|
||||
"required": "Please tell us your goals."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1325,6 +1337,9 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
"type": "text",
|
||||
"required": False,
|
||||
"label": "City",
|
||||
"errorMessages": {
|
||||
"required": "Please enter your City."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1992,8 +2007,8 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
|
||||
self.assertEqual(
|
||||
response_json,
|
||||
{
|
||||
"username": [{"user_message": "Username must be minimum of two characters long"}],
|
||||
"password": [{"user_message": "A valid password is required"}],
|
||||
u"username": [{u"user_message": USERNAME_BAD_LENGTH_MSG}],
|
||||
u"password": [{u"user_message": u"A valid password is required"}],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -10,19 +10,8 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from openedx.core.djangoapps.user_api.accounts import (
|
||||
EMAIL_BAD_LENGTH_MSG, EMAIL_INVALID_MSG,
|
||||
EMAIL_CONFLICT_MSG, EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH,
|
||||
PASSWORD_BAD_LENGTH_MSG, PASSWORD_CANT_EQUAL_USERNAME_MSG,
|
||||
PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH,
|
||||
USERNAME_BAD_LENGTH_MSG, USERNAME_INVALID_CHARS_ASCII, USERNAME_INVALID_CHARS_UNICODE,
|
||||
USERNAME_CONFLICT_MSG, USERNAME_MAX_LENGTH, USERNAME_MIN_LENGTH
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.accounts.tests.testutils import (
|
||||
VALID_EMAILS, VALID_PASSWORDS, VALID_USERNAMES,
|
||||
INVALID_EMAILS, INVALID_PASSWORDS, INVALID_USERNAMES,
|
||||
INVALID_USERNAMES_ASCII, INVALID_USERNAMES_UNICODE
|
||||
)
|
||||
from openedx.core.djangoapps.user_api import accounts
|
||||
from openedx.core.djangoapps.user_api.accounts.tests import testutils
|
||||
from openedx.core.lib.api import test_utils
|
||||
|
||||
|
||||
@@ -45,16 +34,30 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||
decision
|
||||
)
|
||||
|
||||
def assertNotValidationDecision(self, data, decision):
|
||||
self.assertNotEqual(
|
||||
self.get_validation_decision(data),
|
||||
decision
|
||||
)
|
||||
|
||||
def test_no_decision_for_empty_request(self):
|
||||
self.assertValidationDecision({}, {})
|
||||
self.assertValidationDecision(
|
||||
{},
|
||||
{}
|
||||
)
|
||||
|
||||
def test_no_decision_for_invalid_request(self):
|
||||
self.assertValidationDecision({'invalid_field': 'random_user_data'}, {})
|
||||
self.assertValidationDecision(
|
||||
{'invalid_field': 'random_user_data'},
|
||||
{}
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
['email', (email for email in VALID_EMAILS)],
|
||||
['password', (password for password in VALID_PASSWORDS)],
|
||||
['username', (username for username in VALID_USERNAMES)]
|
||||
['name', (name for name in testutils.VALID_NAMES)],
|
||||
['email', (email for email in testutils.VALID_EMAILS)],
|
||||
['password', (password for password in testutils.VALID_PASSWORDS)],
|
||||
['username', (username for username in testutils.VALID_USERNAMES)],
|
||||
['country', (country for country in testutils.VALID_COUNTRIES)]
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_positive_validation_decision(self, form_field_name, user_data):
|
||||
@@ -68,17 +71,19 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||
|
||||
@ddt.data(
|
||||
# Skip None type for invalidity checks.
|
||||
['email', (email for email in INVALID_EMAILS[1:])],
|
||||
['password', (password for password in INVALID_PASSWORDS[1:])],
|
||||
['username', (username for username in INVALID_USERNAMES[1:])]
|
||||
['name', (name for name in testutils.INVALID_NAMES[1:])],
|
||||
['email', (email for email in testutils.INVALID_EMAILS[1:])],
|
||||
['password', (password for password in testutils.INVALID_PASSWORDS[1:])],
|
||||
['username', (username for username in testutils.INVALID_USERNAMES[1:])],
|
||||
['country', (country for country in testutils.INVALID_COUNTRIES[1:])]
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_negative_validation_decision(self, form_field_name, user_data):
|
||||
"""
|
||||
Test if {0} as any item in {1} gives a negative validation decision.
|
||||
"""
|
||||
self.assertNotEqual(
|
||||
self.get_validation_decision({form_field_name: user_data}),
|
||||
self.assertNotValidationDecision(
|
||||
{form_field_name: user_data},
|
||||
{form_field_name: ''}
|
||||
)
|
||||
|
||||
@@ -101,71 +106,91 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||
'email': email
|
||||
},
|
||||
{
|
||||
"username": USERNAME_CONFLICT_MSG.format(username=user.username) if username == user.username else '',
|
||||
"email": EMAIL_CONFLICT_MSG.format(email_address=user.email) if email == user.email else ''
|
||||
"username": accounts.USERNAME_CONFLICT_MSG.format(
|
||||
username=user.username
|
||||
) if username == user.username else '',
|
||||
"email": accounts.EMAIL_CONFLICT_MSG.format(
|
||||
email_address=user.email
|
||||
) if email == user.email else ''
|
||||
}
|
||||
)
|
||||
|
||||
@ddt.data('', ('e' * EMAIL_MAX_LENGTH) + '@email.com')
|
||||
def test_email_less_than_min_length_validation_decision(self, email):
|
||||
@ddt.data('', ('e' * accounts.EMAIL_MAX_LENGTH) + '@email.com')
|
||||
def test_email_bad_length_validation_decision(self, email):
|
||||
self.assertValidationDecision(
|
||||
{'email': email},
|
||||
{'email': EMAIL_BAD_LENGTH_MSG.format(email=email, min=EMAIL_MIN_LENGTH, max=EMAIL_MAX_LENGTH)}
|
||||
{'email': accounts.EMAIL_BAD_LENGTH_MSG}
|
||||
)
|
||||
|
||||
def test_email_generically_invalid_validation_decision(self):
|
||||
email = 'email'
|
||||
self.assertValidationDecision(
|
||||
{'email': email},
|
||||
{'email': EMAIL_INVALID_MSG.format(email=email)}
|
||||
{'email': accounts.EMAIL_INVALID_MSG.format(email=email)}
|
||||
)
|
||||
|
||||
def test_confirm_email_matches_email(self):
|
||||
email = 'user@email.com'
|
||||
self.assertValidationDecision(
|
||||
{'email': email, 'confirm_email': email},
|
||||
{'email': '', 'confirm_email': ''}
|
||||
)
|
||||
|
||||
@ddt.data('', 'users@other.email')
|
||||
def test_confirm_email_doesnt_equal_email(self, confirm_email):
|
||||
self.assertValidationDecision(
|
||||
{'email': 'user@email.com', 'confirm_email': confirm_email},
|
||||
{'email': '', 'confirm_email': accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG}
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
'u' * (USERNAME_MIN_LENGTH - 1),
|
||||
'u' * (USERNAME_MAX_LENGTH + 1)
|
||||
'u' * (accounts.USERNAME_MIN_LENGTH - 1),
|
||||
'u' * (accounts.USERNAME_MAX_LENGTH + 1)
|
||||
)
|
||||
def test_username_less_than_min_length_validation_decision(self, username):
|
||||
def test_username_bad_length_validation_decision(self, username):
|
||||
self.assertValidationDecision(
|
||||
{'username': username},
|
||||
{
|
||||
'username': USERNAME_BAD_LENGTH_MSG.format(
|
||||
username=username,
|
||||
min=USERNAME_MIN_LENGTH,
|
||||
max=USERNAME_MAX_LENGTH
|
||||
)
|
||||
}
|
||||
{'username': accounts.USERNAME_BAD_LENGTH_MSG}
|
||||
)
|
||||
|
||||
@unittest.skipUnless(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames disabled.")
|
||||
@ddt.data(*INVALID_USERNAMES_UNICODE)
|
||||
@ddt.unpack
|
||||
@ddt.data(*testutils.INVALID_USERNAMES_UNICODE)
|
||||
def test_username_invalid_unicode_validation_decision(self, username):
|
||||
self.assertValidationDecision(
|
||||
{'username': username},
|
||||
{'username': USERNAME_INVALID_CHARS_UNICODE}
|
||||
{'username': accounts.USERNAME_INVALID_CHARS_UNICODE}
|
||||
)
|
||||
|
||||
@unittest.skipIf(settings.FEATURES.get("ENABLE_UNICODE_USERNAME"), "Unicode usernames enabled.")
|
||||
@ddt.data(*INVALID_USERNAMES_ASCII)
|
||||
@ddt.unpack
|
||||
@ddt.data(*testutils.INVALID_USERNAMES_ASCII)
|
||||
def test_username_invalid_ascii_validation_decision(self, username):
|
||||
self.assertValidationDecision(
|
||||
{'username': username},
|
||||
{"username": USERNAME_INVALID_CHARS_ASCII}
|
||||
{"username": accounts.USERNAME_INVALID_CHARS_ASCII}
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
'p' * (PASSWORD_MIN_LENGTH - 1),
|
||||
'p' * (PASSWORD_MAX_LENGTH + 1)
|
||||
)
|
||||
def test_password_less_than_min_length_validation_decision(self, password):
|
||||
def test_password_empty_validation_decision(self):
|
||||
self.assertValidationDecision(
|
||||
{'password': ''},
|
||||
{"password": accounts.PASSWORD_EMPTY_MSG}
|
||||
)
|
||||
|
||||
def test_password_bad_min_length_validation_decision(self):
|
||||
password = 'p' * (accounts.PASSWORD_MIN_LENGTH - 1)
|
||||
self.assertValidationDecision(
|
||||
{'password': password},
|
||||
{"password": PASSWORD_BAD_LENGTH_MSG.format(min=PASSWORD_MIN_LENGTH, max=PASSWORD_MAX_LENGTH)}
|
||||
{"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": PASSWORD_CANT_EQUAL_USERNAME_MSG}
|
||||
{"username": "", "password": accounts.PASSWORD_CANT_EQUAL_USERNAME_MSG}
|
||||
)
|
||||
|
||||
@@ -9,6 +9,9 @@ from rest_framework.views import APIView
|
||||
from openedx.core.djangoapps.user_api.accounts.api import (
|
||||
get_email_validation_error,
|
||||
get_email_existence_validation_error,
|
||||
get_confirm_email_validation_error,
|
||||
get_country_validation_error,
|
||||
get_name_validation_error,
|
||||
get_password_validation_error,
|
||||
get_username_validation_error,
|
||||
get_username_existence_validation_error
|
||||
@@ -85,38 +88,66 @@ class RegistrationValidationView(APIView):
|
||||
|
||||
**Available Handlers**
|
||||
|
||||
"name":
|
||||
A handler to check the validity of the user's real name.
|
||||
"username":
|
||||
A handler to check the validity of usernames.
|
||||
"email":
|
||||
A handler to check the validity of emails.
|
||||
"confirm_email":
|
||||
A handler to check whether the confirmation email field matches
|
||||
the email field.
|
||||
"password":
|
||||
A handler to check the validity of passwords; a compatibility
|
||||
decision with the username is made if it exists in the input.
|
||||
"country":
|
||||
A handler to check whether the validity of country fields.
|
||||
"""
|
||||
|
||||
# This end-point is available to anonymous users, so no authentication is needed.
|
||||
authentication_classes = []
|
||||
|
||||
def name_handler(self, request):
|
||||
name = request.data.get('name')
|
||||
return get_name_validation_error(name)
|
||||
|
||||
def username_handler(self, request):
|
||||
username = request.data.get('username')
|
||||
invalid_username_error = get_username_validation_error(username)
|
||||
username_exists_error = get_username_existence_validation_error(username)
|
||||
# Existing usernames are already valid, so we prefer that error.
|
||||
return username_exists_error or invalid_username_error
|
||||
# We prefer seeing for invalidity first.
|
||||
# Some invalid usernames (like for superusers) may exist.
|
||||
return invalid_username_error or username_exists_error
|
||||
|
||||
def email_handler(self, request):
|
||||
email = request.data.get('email')
|
||||
invalid_email_error = get_email_validation_error(email)
|
||||
email_exists_error = get_email_existence_validation_error(email)
|
||||
# Existing emails are already valid, so we prefer that error.
|
||||
return email_exists_error or invalid_email_error
|
||||
# We prefer seeing for invalidity first.
|
||||
# Some invalid emails (like a blank one for superusers) may exist.
|
||||
return invalid_email_error or email_exists_error
|
||||
|
||||
def confirm_email_handler(self, request):
|
||||
email = request.data.get('email', None)
|
||||
confirm_email = request.data.get('confirm_email')
|
||||
return get_confirm_email_validation_error(confirm_email, email)
|
||||
|
||||
def password_handler(self, request):
|
||||
username = request.data.get('username') or None
|
||||
username = request.data.get('username', None)
|
||||
password = request.data.get('password')
|
||||
return get_password_validation_error(password, username)
|
||||
|
||||
def country_handler(self, request):
|
||||
country = request.data.get('country')
|
||||
return get_country_validation_error(country)
|
||||
|
||||
validation_handlers = {
|
||||
"name": name_handler,
|
||||
"username": username_handler,
|
||||
"email": email_handler,
|
||||
"confirm_email": confirm_email_handler,
|
||||
"password": password_handler,
|
||||
"country": country_handler
|
||||
}
|
||||
|
||||
def post(self, request):
|
||||
@@ -125,9 +156,12 @@ class RegistrationValidationView(APIView):
|
||||
|
||||
Expects request of the form
|
||||
>>> {
|
||||
>>> "name": "Dan the Validator",
|
||||
>>> "username": "mslm",
|
||||
>>> "email": "mslm@gmail.com",
|
||||
>>> "password": "password123"
|
||||
>>> "confirm_email": "mslm@gmail.com",
|
||||
>>> "password": "password123",
|
||||
>>> "country": "PK"
|
||||
>>> }
|
||||
where each key is the appropriate form field name and the value is
|
||||
user input. One may enter individual inputs if needed. Some inputs
|
||||
|
||||
@@ -31,15 +31,7 @@ from student.forms import get_registration_extension_form
|
||||
from student.views import create_account_with_params, AccountValidationError
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
from .accounts import (
|
||||
EMAIL_MAX_LENGTH, EMAIL_MIN_LENGTH,
|
||||
NAME_MAX_LENGTH,
|
||||
PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH,
|
||||
USERNAME_MAX_LENGTH, USERNAME_MIN_LENGTH,
|
||||
EMAIL_CONFLICT_MSG,
|
||||
USERNAME_CONFLICT_MSG
|
||||
)
|
||||
from .accounts.api import check_account_exists
|
||||
import accounts
|
||||
from .helpers import FormDescription, require_post_params, shim_student_view
|
||||
from .models import UserPreference, UserProfile
|
||||
from .preferences.api import get_country_time_zones, update_email_opt_in
|
||||
@@ -91,8 +83,8 @@ class LoginSessionView(APIView):
|
||||
placeholder=email_placeholder,
|
||||
instructions=email_instructions,
|
||||
restrictions={
|
||||
"min_length": EMAIL_MIN_LENGTH,
|
||||
"max_length": EMAIL_MAX_LENGTH,
|
||||
"min_length": accounts.EMAIL_MIN_LENGTH,
|
||||
"max_length": accounts.EMAIL_MAX_LENGTH,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -105,8 +97,8 @@ class LoginSessionView(APIView):
|
||||
label=password_label,
|
||||
field_type="password",
|
||||
restrictions={
|
||||
"min_length": PASSWORD_MIN_LENGTH,
|
||||
"max_length": PASSWORD_MAX_LENGTH,
|
||||
"min_length": accounts.PASSWORD_MIN_LENGTH,
|
||||
"max_length": accounts.PASSWORD_MAX_LENGTH,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -336,11 +328,11 @@ class RegistrationView(APIView):
|
||||
username = data.get('username')
|
||||
|
||||
# Handle duplicate email/username
|
||||
conflicts = check_account_exists(email=email, username=username)
|
||||
conflicts = accounts.api.check_account_exists(email=email, username=username)
|
||||
if conflicts:
|
||||
conflict_messages = {
|
||||
"email": EMAIL_CONFLICT_MSG.format(email_address=email),
|
||||
"username": USERNAME_CONFLICT_MSG.format(username=username),
|
||||
"email": accounts.EMAIL_CONFLICT_MSG.format(email_address=email),
|
||||
"username": accounts.USERNAME_CONFLICT_MSG.format(username=username),
|
||||
}
|
||||
errors = {
|
||||
field: [{"user_message": conflict_messages[field]}]
|
||||
@@ -414,8 +406,8 @@ class RegistrationView(APIView):
|
||||
placeholder=email_placeholder,
|
||||
instructions=email_instructions,
|
||||
restrictions={
|
||||
"min_length": EMAIL_MIN_LENGTH,
|
||||
"max_length": EMAIL_MAX_LENGTH,
|
||||
"min_length": accounts.EMAIL_MIN_LENGTH,
|
||||
"max_length": accounts.EMAIL_MAX_LENGTH,
|
||||
},
|
||||
required=required
|
||||
)
|
||||
@@ -433,7 +425,7 @@ class RegistrationView(APIView):
|
||||
# Translators: This label appears above a field on the registration form
|
||||
# meant to confirm the user's email address.
|
||||
email_label = _(u"Confirm Email")
|
||||
error_msg = _(u"The email addresses do not match.")
|
||||
error_msg = accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG
|
||||
|
||||
form_desc.add_field(
|
||||
"confirm_email",
|
||||
@@ -472,7 +464,7 @@ class RegistrationView(APIView):
|
||||
placeholder=name_placeholder,
|
||||
instructions=name_instructions,
|
||||
restrictions={
|
||||
"max_length": NAME_MAX_LENGTH,
|
||||
"max_length": accounts.NAME_MAX_LENGTH,
|
||||
},
|
||||
required=required
|
||||
)
|
||||
@@ -508,8 +500,8 @@ class RegistrationView(APIView):
|
||||
instructions=username_instructions,
|
||||
placeholder=username_placeholder,
|
||||
restrictions={
|
||||
"min_length": USERNAME_MIN_LENGTH,
|
||||
"max_length": USERNAME_MAX_LENGTH,
|
||||
"min_length": accounts.USERNAME_MIN_LENGTH,
|
||||
"max_length": accounts.USERNAME_MAX_LENGTH,
|
||||
},
|
||||
required=required
|
||||
)
|
||||
@@ -533,8 +525,8 @@ class RegistrationView(APIView):
|
||||
label=password_label,
|
||||
field_type="password",
|
||||
restrictions={
|
||||
"min_length": PASSWORD_MIN_LENGTH,
|
||||
"max_length": PASSWORD_MAX_LENGTH,
|
||||
"min_length": accounts.PASSWORD_MIN_LENGTH,
|
||||
"max_length": accounts.PASSWORD_MAX_LENGTH,
|
||||
},
|
||||
required=required
|
||||
)
|
||||
@@ -552,6 +544,7 @@ class RegistrationView(APIView):
|
||||
# Translators: This label appears above a dropdown menu on the registration
|
||||
# form used to select the user's highest completed level of education.
|
||||
education_level_label = _(u"Highest level of education completed")
|
||||
error_msg = accounts.REQUIRED_FIELD_LEVEL_OF_EDUCATION_MSG
|
||||
|
||||
# The labels are marked for translation in UserProfile model definition.
|
||||
options = [(name, _(label)) for name, label in UserProfile.LEVEL_OF_EDUCATION_CHOICES] # pylint: disable=translation-of-non-string
|
||||
@@ -561,7 +554,10 @@ class RegistrationView(APIView):
|
||||
field_type="select",
|
||||
options=options,
|
||||
include_default_option=True,
|
||||
required=required
|
||||
required=required,
|
||||
error_messages={
|
||||
"required": error_msg
|
||||
}
|
||||
)
|
||||
|
||||
def _add_gender_field(self, form_desc, required=True):
|
||||
@@ -626,12 +622,16 @@ class RegistrationView(APIView):
|
||||
# Translators: This label appears above a field on the registration form
|
||||
# meant to hold the user's mailing address.
|
||||
mailing_address_label = _(u"Mailing address")
|
||||
error_msg = accounts.REQUIRED_FIELD_MAILING_ADDRESS_MSG
|
||||
|
||||
form_desc.add_field(
|
||||
"mailing_address",
|
||||
label=mailing_address_label,
|
||||
field_type="textarea",
|
||||
required=required
|
||||
required=required,
|
||||
error_messages={
|
||||
"required": error_msg
|
||||
}
|
||||
)
|
||||
|
||||
def _add_goals_field(self, form_desc, required=True):
|
||||
@@ -649,12 +649,16 @@ class RegistrationView(APIView):
|
||||
goals_label = _(u"Tell us why you're interested in {platform_name}").format(
|
||||
platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME)
|
||||
)
|
||||
error_msg = accounts.REQUIRED_FIELD_GOALS_MSG
|
||||
|
||||
form_desc.add_field(
|
||||
"goals",
|
||||
label=goals_label,
|
||||
field_type="textarea",
|
||||
required=required
|
||||
required=required,
|
||||
error_messages={
|
||||
"required": error_msg
|
||||
}
|
||||
)
|
||||
|
||||
def _add_city_field(self, form_desc, required=True):
|
||||
@@ -670,11 +674,15 @@ class RegistrationView(APIView):
|
||||
# Translators: This label appears above a field on the registration form
|
||||
# which allows the user to input the city in which they live.
|
||||
city_label = _(u"City")
|
||||
error_msg = accounts.REQUIRED_FIELD_CITY_MSG
|
||||
|
||||
form_desc.add_field(
|
||||
"city",
|
||||
label=city_label,
|
||||
required=required
|
||||
required=required,
|
||||
error_messages={
|
||||
"required": error_msg
|
||||
}
|
||||
)
|
||||
|
||||
def _add_state_field(self, form_desc, required=False):
|
||||
@@ -790,7 +798,7 @@ class RegistrationView(APIView):
|
||||
# Translators: This label appears above a dropdown menu on the registration
|
||||
# form used to select the country in which the user lives.
|
||||
country_label = _(u"Country")
|
||||
error_msg = _(u"Please select your Country.")
|
||||
error_msg = accounts.REQUIRED_FIELD_COUNTRY_MSG
|
||||
|
||||
# If we set a country code, make sure it's uppercase for the sake of the form.
|
||||
default_country = form_desc._field_overrides.get('country', {}).get('defaultValue')
|
||||
@@ -1025,8 +1033,8 @@ class PasswordResetView(APIView):
|
||||
placeholder=email_placeholder,
|
||||
instructions=email_instructions,
|
||||
restrictions={
|
||||
"min_length": EMAIL_MIN_LENGTH,
|
||||
"max_length": EMAIL_MAX_LENGTH,
|
||||
"min_length": accounts.EMAIL_MIN_LENGTH,
|
||||
"max_length": accounts.EMAIL_MAX_LENGTH,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1094,7 +1102,9 @@ class PreferenceUsersListView(generics.ListAPIView):
|
||||
paginate_by_param = "page_size"
|
||||
|
||||
def get_queryset(self):
|
||||
return User.objects.filter(preferences__key=self.kwargs["pref_key"]).prefetch_related("preferences").select_related("profile")
|
||||
return User.objects.filter(
|
||||
preferences__key=self.kwargs["pref_key"]
|
||||
).prefetch_related("preferences").select_related("profile")
|
||||
|
||||
|
||||
class UpdateEmailOptInPreference(APIView):
|
||||
|
||||
Reference in New Issue
Block a user