Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
4c41b777
Commit
4c41b777
authored
Aug 02, 2017
by
Uman Shahzad
Committed by
GitHub
Aug 02, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #15375 from open-craft/uman/ent-334
[ENT-334] Add client-side registration form validation.
parents
78708e41
4b47d4af
Hide whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
1694 additions
and
483 deletions
+1694
-483
common/djangoapps/student/forms.py
+5
-17
common/djangoapps/student/tests/test_create_account.py
+6
-4
common/djangoapps/student/tests/test_long_username_email.py
+3
-1
common/static/common/js/utils/edx.utils.validate.js
+13
-6
common/test/acceptance/tests/lms/test_lms.py
+1
-4
conf/locale/en/LC_MESSAGES/django.po
+102
-54
conf/locale/en/LC_MESSAGES/djangojs.po
+6
-2
lms/djangoapps/student_account/test/test_views.py
+0
-19
lms/static/js/spec/student_account/register_spec.js
+144
-33
lms/static/js/student_account/tos_modal.js
+1
-0
lms/static/js/student_account/views/FormView.js
+58
-32
lms/static/js/student_account/views/RegisterView.js
+266
-33
lms/static/sass/views/_login-register.scss
+25
-6
lms/templates/student_account/form_field.underscore
+72
-41
openedx/core/djangoapps/user_api/accounts/__init__.py
+63
-0
openedx/core/djangoapps/user_api/accounts/api.py
+319
-130
openedx/core/djangoapps/user_api/accounts/tests/test_api.py
+22
-50
openedx/core/djangoapps/user_api/accounts/tests/testutils.py
+104
-0
openedx/core/djangoapps/user_api/errors.py
+25
-0
openedx/core/djangoapps/user_api/tests/test_views.py
+19
-5
openedx/core/djangoapps/user_api/urls.py
+21
-3
openedx/core/djangoapps/user_api/validation/__init__.py
+0
-0
openedx/core/djangoapps/user_api/validation/tests/__init__.py
+0
-0
openedx/core/djangoapps/user_api/validation/tests/test_views.py
+196
-0
openedx/core/djangoapps/user_api/validation/views.py
+180
-0
openedx/core/djangoapps/user_api/views.py
+42
-42
openedx/core/lib/api/test_utils.py
+1
-1
No files found.
common/djangoapps/student/forms.py
View file @
4c41b777
...
...
@@ -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
,
}
)
...
...
common/djangoapps/student/tests/test_create_account.py
View file @
4c41b777
...
...
@@ -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"
...
...
common/djangoapps/student/tests/test_long_username_email.py
View file @
4c41b777
...
...
@@ -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
):
...
...
common/static/common/js/utils/edx.utils.validate.js
View file @
4c41b777
...
...
@@ -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
;
});
...
...
common/test/acceptance/tests/lms/test_lms.py
View file @
4c41b777
...
...
@@ -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
)
...
...
conf/locale/en/LC_MESSAGES/django.po
View file @
4c41b777
...
...
@@ -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-0
1 21:22
+0000\n"
"PO-Revision-Date: 2017-08-0
1 21:22:49.685108
\n"
"POT-Creation-Date: 2017-08-0
2 12:48
+0000\n"
"PO-Revision-Date: 2017-08-0
2 12:48:04.261143
\n"
"Last-Translator: \n"
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
"MIME-Version: 1.0\n"
...
...
@@ -331,31 +331,6 @@ msgid "User profile"
msgstr ""
#: common/djangoapps/student/forms.py
msgid "Username must be minimum of two characters long"
msgstr ""
#: common/djangoapps/student/forms.py
#, python-format
msgid "Username cannot be more than %(limit_value)s characters long"
msgstr ""
#. Translators: This message is shown when the Unicode usernames are NOT
#. allowed
#: common/djangoapps/student/forms.py
msgid ""
"Usernames can only contain Roman letters, western numerals (0-9), "
"underscores (_), and hyphens (-)."
msgstr ""
#. Translators: This message is shown only when the Unicode usernames are
#. allowed
#: common/djangoapps/student/forms.py
msgid ""
"Usernames can only contain letters, numerals, underscore (_), numbers and "
"@/./+/-/_ characters."
msgstr ""
#: common/djangoapps/student/forms.py
msgid ""
"That e-mail address doesn't have an associated user account. Are you sure "
"you've registered?"
...
...
@@ -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
...
...
@@ -9506,10 +9554,6 @@ msgid "Company"
msgstr ""
#: openedx/core/djangoapps/user_api/views.py
msgid "Please select your Country."
msgstr ""
#: openedx/core/djangoapps/user_api/views.py
msgid "Review the Honor Code"
msgstr ""
...
...
@@ -19611,6 +19655,10 @@ msgid ""
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 succes
s
fully added."
msgstr ""
#: wiki/views/article.py
...
...
conf/locale/en/LC_MESSAGES/djangojs.po
View file @
4c41b777
...
...
@@ -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-0
1 21:22
+0000\n"
"PO-Revision-Date: 2017-08-0
1 21:22:50.33999
3\n"
"POT-Creation-Date: 2017-08-0
2 12:47
+0000\n"
"PO-Revision-Date: 2017-08-0
2 12:48:04.56744
3\n"
"Last-Translator: \n"
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
"MIME-Version: 1.0\n"
...
...
@@ -4219,6 +4219,10 @@ msgid "We couldn't create your account."
msgstr ""
#: lms/static/js/student_account/views/RegisterView.js
msgid "(required)"
msgstr ""
#: lms/static/js/student_account/views/RegisterView.js
msgid "You've successfully signed into %(currentProvider)s."
msgstr ""
...
...
lms/djangoapps/student_account/test/test_views.py
View file @
4c41b777
...
...
@@ -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'
]
...
...
lms/static/js/spec/student_account/register_spec.js
View file @
4c41b777
...
...
@@ -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
...
...
lms/static/js/student_account/tos_modal.js
View file @
4c41b777
...
...
@@ -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'
),
...
...
lms/static/js/student_account/views/FormView.js
View file @
4c41b777
...
...
@@ -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
;
}
});
});
...
...
lms/static/js/student_account/views/RegisterView.js
View file @
4c41b777
...
...
@@ -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 @@
});
},
submitForm
:
function
(
event
)
{
// eslint-disable-line no-unused-vars
var
elements
=
this
.
$form
[
0
].
elements
,
$el
,
i
;
// As per requirements, disable positive validation for submission.
this
.
positiveValidationEnabled
=
false
;
for
(
i
=
0
;
i
<
elements
.
length
;
i
++
)
{
$el
=
$
(
elements
[
i
]);
// 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
),
$form
=
this
.
$form
,
$label
,
$emailElement
,
$confirmEmailElement
,
email
=
''
,
confirmEmail
=
''
;
$emailElement
=
$form
.
find
(
'input[name=email]'
);
$confirmEmailElement
=
$form
.
find
(
'input[name=confirm_email]'
);
if
(
$confirmEmailElement
.
length
)
{
email
=
$emailElement
.
val
();
confirmEmail
=
$confirmEmailElement
.
val
();
$label
=
$form
.
find
(
'label[for='
+
$confirmEmailElement
.
attr
(
'id'
)
+
']'
);
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'
);
$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
);
}
});
});
...
...
lms/static/sass/views/_login-register.scss
View file @
4c41b777
...
...
@@ -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
;
}
...
...
lms/templates/student_account/form_field.underscore
View file @
4c41b777
<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' ) { %>
...
...
openedx/core/djangoapps/user_api/accounts/__init__.py
View file @
4c41b777
...
...
@@ -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."
)
openedx/core/djangoapps/user_api/accounts/api.py
View file @
4c41b777
# -*- 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 sam
e
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 th
e
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_email
(
email
):
"""Validate the format of the email address.
Arguments:
email (unicode): The proposed email.
def
_validate_password
(
password
,
username
):
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"
)
try
:
_validate_type
(
password
,
basestring
,
accounts
.
PASSWORD_BAD_TYPE_MSG
)
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
)
==
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
)
if
len
(
password
)
>
PASSWORD_MAX_LENGTH
:
raise
AccountPasswordInvalid
(
u"Password must be at most {max} characters long"
.
format
(
max
=
PASSWORD_MAX_LENGTH
)
)
_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
AccountPasswordInvalid
(
u"Password cannot be the same as the username"
)
raise
errors
.
AccountPasswordInvalid
(
accounts
.
PASSWORD_CANT_EQUAL_USERNAME_MSG
)
def
_validate_email
(
email
):
"""Validate the format of the email address.
def
_validate_type
(
data
,
type
,
err
):
"""Checks whether the input data is of type. If not,
throws a generic error message.
Arguments:
email (unicode): The proposed email.
: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
Returns:
None
"""
if
not
isinstance
(
data
,
type
):
raise
errors
.
AccountDataBadType
(
err
)
Raises:
AccountEmailInvalid
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
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
(
data
)
<
min
or
len
(
data
)
>
max
:
raise
errors
.
AccountDataBadLength
(
err
)
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
)
)
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
:
validate_email
(
email
)
except
ValidationError
:
raise
AccountEmailInvalid
(
u"Email '{email}' format is not valid"
.
format
(
email
=
email
)
)
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
)
openedx/core/djangoapps/user_api/accounts/tests/test_api.py
View file @
4c41b777
...
...
@@ -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
):
...
...
openedx/core/djangoapps/user_api/accounts/tests/testutils.py
0 → 100644
View file @
4c41b777
# -*- 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'
]
openedx/core/djangoapps/user_api/errors.py
View file @
4c41b777
...
...
@@ -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,
...
...
openedx/core/djangoapps/user_api/tests/test_views.py
View file @
4c41b777
...
...
@@ -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"
}],
}
)
...
...
openedx/core/djangoapps/user_api/urls.py
View file @
4c41b777
...
...
@@ -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
(),
...
...
@@ -44,6 +57,11 @@ urlpatterns = patterns(
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
(),
name
=
'preferences_api'
...
...
openedx/core/djangoapps/user_api/validation/__init__.py
0 → 100644
View file @
4c41b777
openedx/core/djangoapps/user_api/validation/tests/__init__.py
0 → 100644
View file @
4c41b777
openedx/core/djangoapps/user_api/validation/tests/test_views.py
0 → 100644
View file @
4c41b777
# -*- 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
}
)
openedx/core/djangoapps/user_api/validation/views.py
0 → 100644
View file @
4c41b777
# -*- 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
})
openedx/core/djangoapps/user_api/views.py
View file @
4c41b777
...
...
@@ -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
):
...
...
openedx/core/lib/api/test_utils.py
View file @
4c41b777
...
...
@@ -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 re
ps
onse"""
"""Make a request with the given args and return the parsed JSON re
sp
onse"""
resp
=
self
.
request_with_auth
(
"get"
,
*
args
,
**
kwargs
)
self
.
assertHttpOK
(
resp
)
self
.
assertTrue
(
resp
[
"Content-Type"
]
.
startswith
(
"application/json"
))
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment