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
6013c529
Commit
6013c529
authored
Oct 20, 2014
by
Renzo Lucioni
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #5615 from edx/renzo/account-password-reset
Add password reset request handling to the account page
parents
3cfe586b
ff587c5c
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
419 additions
and
121 deletions
+419
-121
common/djangoapps/user_api/api/account.py
+40
-0
common/djangoapps/user_api/tests/test_account_api.py
+85
-48
lms/djangoapps/student_account/test/test_views.py
+147
-25
lms/djangoapps/student_account/urls.py
+1
-0
lms/djangoapps/student_account/views.py
+66
-11
lms/static/js/spec/student_account/account.js
+35
-30
lms/static/js/student_account/account.js
+35
-2
lms/templates/student_account/account.underscore
+8
-3
lms/templates/student_account/index.html
+1
-1
lms/templates/student_profile/index.html
+1
-1
No files found.
common/djangoapps/user_api/api/account.py
View file @
6013c529
...
@@ -5,8 +5,11 @@ address, but does NOT include user profile information (i.e., demographic
...
@@ -5,8 +5,11 @@ address, but does NOT include user profile information (i.e., demographic
information and preferences).
information and preferences).
"""
"""
from
django.conf
import
settings
from
django.db
import
transaction
,
IntegrityError
from
django.db
import
transaction
,
IntegrityError
from
django.core.validators
import
validate_email
,
validate_slug
,
ValidationError
from
django.core.validators
import
validate_email
,
validate_slug
,
ValidationError
from
django.contrib.auth.forms
import
PasswordResetForm
from
user_api.models
import
User
,
UserProfile
,
Registration
,
PendingEmailChange
from
user_api.models
import
User
,
UserProfile
,
Registration
,
PendingEmailChange
from
user_api.helpers
import
intercept_errors
from
user_api.helpers
import
intercept_errors
...
@@ -300,6 +303,43 @@ def confirm_email_change(activation_key):
...
@@ -300,6 +303,43 @@ def confirm_email_change(activation_key):
return
(
old_email
,
new_email
)
return
(
old_email
,
new_email
)
@intercept_errors
(
AccountInternalError
,
ignore_errors
=
[
AccountRequestError
])
def
request_password_change
(
email
,
orig_host
,
is_secure
):
"""Email a single-use link for performing a password reset.
Users must confirm the password change before we update their information.
Args:
email (string): An email address
orig_host (string): An originating host, extracted from a request with get_host
is_secure (Boolean): Whether the request was made with HTTPS
Returns:
None
Raises:
AccountUserNotFound
AccountRequestError
"""
# Binding data to a form requires that the data be passed as a dictionary
# to the Form class constructor.
form
=
PasswordResetForm
({
'email'
:
email
})
# Validate that an active user exists with the given email address.
if
form
.
is_valid
():
# Generate a single-use link for performing a password reset
# and email it to the user.
form
.
save
(
from_email
=
settings
.
DEFAULT_FROM_EMAIL
,
domain_override
=
orig_host
,
use_https
=
is_secure
)
else
:
# No active user with the provided email address exists.
raise
AccountUserNotFound
def
_validate_username
(
username
):
def
_validate_username
(
username
):
"""Validate the username.
"""Validate the username.
...
...
common/djangoapps/user_api/tests/test_account_api.py
View file @
6013c529
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
""" Tests for the account API. """
""" Tests for the account API. """
import
unittest
import
re
from
unittest
import
skipUnless
from
nose.tools
import
raises
from
nose.tools
import
raises
from
mock
import
patch
import
ddt
import
ddt
from
dateutil.parser
import
parse
as
parse_datetime
from
dateutil.parser
import
parse
as
parse_datetime
from
django.co
nf
import
settings
from
django.co
re
import
mail
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.conf
import
settings
from
user_api.api
import
account
as
account_api
from
user_api.api
import
account
as
account_api
from
user_api.models
import
UserProfile
from
user_api.models
import
UserProfile
...
@@ -14,43 +19,46 @@ from user_api.models import UserProfile
...
@@ -14,43 +19,46 @@ from user_api.models import UserProfile
@ddt.ddt
@ddt.ddt
class
AccountApiTest
(
TestCase
):
class
AccountApiTest
(
TestCase
):
USERNAME
=
u"frank-underwood"
USERNAME
=
u'frank-underwood'
PASSWORD
=
u"ṕáśśẃőŕd"
PASSWORD
=
u'ṕáśśẃőŕd'
EMAIL
=
u"frank+underwood@example.com"
EMAIL
=
u'frank+underwood@example.com'
ORIG_HOST
=
'example.com'
IS_SECURE
=
False
INVALID_USERNAMES
=
[
INVALID_USERNAMES
=
[
None
,
None
,
u
""
,
u
''
,
u
"a"
,
u
'a'
,
u
"a"
*
(
account_api
.
USERNAME_MAX_LENGTH
+
1
),
u
'a'
*
(
account_api
.
USERNAME_MAX_LENGTH
+
1
),
u
"invalid_symbol_@"
,
u
'invalid_symbol_@'
,
u
"invalid-unicode_fŕáńḱ"
,
u
'invalid-unicode_fŕáńḱ'
,
]
]
INVALID_EMAILS
=
[
INVALID_EMAILS
=
[
None
,
None
,
u
""
,
u
''
,
u
"a"
,
u
'a'
,
"no_domain"
,
'no_domain'
,
"no+domain"
,
'no+domain'
,
"@"
,
'@'
,
"@domain.com"
,
'@domain.com'
,
"test@no_extension"
,
'test@no_extension'
,
u
"fŕáńḱ@example.com"
,
u
'fŕáńḱ@example.com'
,
u
"frank@éxáḿṕĺé.ćőḿ"
,
u
'frank@éxáḿṕĺé.ćőḿ'
,
# Long email -- subtract the length of the @domain
# Long email -- subtract the length of the @domain
# except for one character (so we exceed the max length limit)
# except for one character (so we exceed the max length limit)
u
"{user}@example.com"
.
format
(
u
'{user}@example.com'
.
format
(
user
=
(
u'e'
*
(
account_api
.
EMAIL_MAX_LENGTH
-
11
))
user
=
(
u'e'
*
(
account_api
.
EMAIL_MAX_LENGTH
-
11
))
)
)
]
]
INVALID_PASSWORDS
=
[
INVALID_PASSWORDS
=
[
None
,
None
,
u
""
,
u
''
,
u
"a"
,
u
'a'
,
u
"a"
*
(
account_api
.
PASSWORD_MAX_LENGTH
+
1
)
u
'a'
*
(
account_api
.
PASSWORD_MAX_LENGTH
+
1
)
]
]
def
test_activate_account
(
self
):
def
test_activate_account
(
self
):
...
@@ -72,7 +80,7 @@ class AccountApiTest(TestCase):
...
@@ -72,7 +80,7 @@ class AccountApiTest(TestCase):
# Request an email change
# Request an email change
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
activation_key
=
account_api
.
request_email_change
(
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"new+email@example.com"
,
self
.
PASSWORD
self
.
USERNAME
,
u
'new+email@example.com'
,
self
.
PASSWORD
)
)
# Verify that the email has not yet changed
# Verify that the email has not yet changed
...
@@ -82,24 +90,23 @@ class AccountApiTest(TestCase):
...
@@ -82,24 +90,23 @@ class AccountApiTest(TestCase):
# Confirm the change, using the activation code
# Confirm the change, using the activation code
old_email
,
new_email
=
account_api
.
confirm_email_change
(
activation_key
)
old_email
,
new_email
=
account_api
.
confirm_email_change
(
activation_key
)
self
.
assertEqual
(
old_email
,
self
.
EMAIL
)
self
.
assertEqual
(
old_email
,
self
.
EMAIL
)
self
.
assertEqual
(
new_email
,
u
"new+email@example.com"
)
self
.
assertEqual
(
new_email
,
u
'new+email@example.com'
)
# Verify that the email is changed
# Verify that the email is changed
account
=
account_api
.
account_info
(
self
.
USERNAME
)
account
=
account_api
.
account_info
(
self
.
USERNAME
)
self
.
assertEqual
(
account
[
'email'
],
u
"new+email@example.com"
)
self
.
assertEqual
(
account
[
'email'
],
u
'new+email@example.com'
)
def
test_confirm_email_change_repeat
(
self
):
def
test_confirm_email_change_repeat
(
self
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
activation_key
=
account_api
.
request_email_change
(
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"new+email@example.com"
,
self
.
PASSWORD
self
.
USERNAME
,
u
'new+email@example.com'
,
self
.
PASSWORD
)
)
# Confirm the change once
# Confirm the change once
account_api
.
confirm_email_change
(
activation_key
)
account_api
.
confirm_email_change
(
activation_key
)
# Confirm the change again
# Confirm the change again. The activation code should be
# The activation code should be single-use
# single-use, so this should raise an error.
# so this should raise an error.
with
self
.
assertRaises
(
account_api
.
AccountNotAuthorized
):
with
self
.
assertRaises
(
account_api
.
AccountNotAuthorized
):
account_api
.
confirm_email_change
(
activation_key
)
account_api
.
confirm_email_change
(
activation_key
)
...
@@ -110,11 +117,11 @@ class AccountApiTest(TestCase):
...
@@ -110,11 +117,11 @@ class AccountApiTest(TestCase):
# Email uniqueness constraints were introduced in a database migration,
# Email uniqueness constraints were introduced in a database migration,
# which we disable in the unit tests to improve the speed of the test suite.
# which we disable in the unit tests to improve the speed of the test suite.
@
unittest.
skipUnless
(
settings
.
SOUTH_TESTS_MIGRATE
,
"South migrations required"
)
@skipUnless
(
settings
.
SOUTH_TESTS_MIGRATE
,
"South migrations required"
)
def
test_create_account_duplicate_email
(
self
):
def
test_create_account_duplicate_email
(
self
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
with
self
.
assertRaises
(
account_api
.
AccountUserAlreadyExists
):
with
self
.
assertRaises
(
account_api
.
AccountUserAlreadyExists
):
account_api
.
create_account
(
"different_user"
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
create_account
(
'different_user'
,
self
.
PASSWORD
,
self
.
EMAIL
)
def
test_username_too_long
(
self
):
def
test_username_too_long
(
self
):
long_username
=
'e'
*
(
account_api
.
USERNAME_MAX_LENGTH
+
1
)
long_username
=
'e'
*
(
account_api
.
USERNAME_MAX_LENGTH
+
1
)
...
@@ -122,7 +129,7 @@ class AccountApiTest(TestCase):
...
@@ -122,7 +129,7 @@ class AccountApiTest(TestCase):
account_api
.
create_account
(
long_username
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
create_account
(
long_username
,
self
.
PASSWORD
,
self
.
EMAIL
)
def
test_account_info_no_user
(
self
):
def
test_account_info_no_user
(
self
):
self
.
assertIs
(
account_api
.
account_info
(
"does_not_exist"
),
None
)
self
.
assertIs
(
account_api
.
account_info
(
'does_not_exist'
),
None
)
@raises
(
account_api
.
AccountEmailInvalid
)
@raises
(
account_api
.
AccountEmailInvalid
)
@ddt.data
(
*
INVALID_EMAILS
)
@ddt.data
(
*
INVALID_EMAILS
)
...
@@ -146,11 +153,11 @@ class AccountApiTest(TestCase):
...
@@ -146,11 +153,11 @@ class AccountApiTest(TestCase):
@raises
(
account_api
.
AccountNotAuthorized
)
@raises
(
account_api
.
AccountNotAuthorized
)
def
test_activate_account_invalid_key
(
self
):
def
test_activate_account_invalid_key
(
self
):
account_api
.
activate_account
(
u
"invalid"
)
account_api
.
activate_account
(
u
'invalid'
)
@raises
(
account_api
.
AccountUserNotFound
)
@raises
(
account_api
.
AccountUserNotFound
)
def
test_request_email_change_no_user
(
self
):
def
test_request_email_change_no_user
(
self
):
account_api
.
request_email_change
(
u
"no_such_user"
,
self
.
EMAIL
,
self
.
PASSWORD
)
account_api
.
request_email_change
(
u
'no_such_user'
,
self
.
EMAIL
,
self
.
PASSWORD
)
@ddt.data
(
*
INVALID_EMAILS
)
@ddt.data
(
*
INVALID_EMAILS
)
def
test_request_email_change_invalid_email
(
self
,
invalid_email
):
def
test_request_email_change_invalid_email
(
self
,
invalid_email
):
...
@@ -165,22 +172,22 @@ class AccountApiTest(TestCase):
...
@@ -165,22 +172,22 @@ class AccountApiTest(TestCase):
# Create two accounts, both activated
# Create two accounts, both activated
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
activate_account
(
activation_key
)
account_api
.
activate_account
(
activation_key
)
activation_key
=
account_api
.
create_account
(
u
"another_user"
,
u"password"
,
u"another+user@example.com"
)
activation_key
=
account_api
.
create_account
(
u
'another_user'
,
u'password'
,
u'another+user@example.com'
)
account_api
.
activate_account
(
activation_key
)
account_api
.
activate_account
(
activation_key
)
# Try to change the first user's email to the same as the second user's
# Try to change the first user's email to the same as the second user's
with
self
.
assertRaises
(
account_api
.
AccountEmailAlreadyExists
):
with
self
.
assertRaises
(
account_api
.
AccountEmailAlreadyExists
):
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"another+user@example.com"
,
self
.
PASSWORD
)
account_api
.
request_email_change
(
self
.
USERNAME
,
u
'another+user@example.com'
,
self
.
PASSWORD
)
def
test_request_email_change_duplicates_unactivated_account
(
self
):
def
test_request_email_change_duplicates_unactivated_account
(
self
):
# Create two accounts, but the second account is inactive
# Create two accounts, but the second account is inactive
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
activate_account
(
activation_key
)
account_api
.
activate_account
(
activation_key
)
account_api
.
create_account
(
u
"another_user"
,
u"password"
,
u"another+user@example.com"
)
account_api
.
create_account
(
u
'another_user'
,
u'password'
,
u'another+user@example.com'
)
# Try to change the first user's email to the same as the second user's
# Try to change the first user's email to the same as the second user's
# Since the second user has not yet activated, this should succeed.
# Since the second user has not yet activated, this should succeed.
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"another+user@example.com"
,
self
.
PASSWORD
)
account_api
.
request_email_change
(
self
.
USERNAME
,
u
'another+user@example.com'
,
self
.
PASSWORD
)
def
test_request_email_change_same_address
(
self
):
def
test_request_email_change_same_address
(
self
):
# Create and activate the account
# Create and activate the account
...
@@ -196,14 +203,14 @@ class AccountApiTest(TestCase):
...
@@ -196,14 +203,14 @@ class AccountApiTest(TestCase):
# Use the wrong password
# Use the wrong password
with
self
.
assertRaises
(
account_api
.
AccountNotAuthorized
):
with
self
.
assertRaises
(
account_api
.
AccountNotAuthorized
):
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"new+email@example.com"
,
u"wrong password"
)
account_api
.
request_email_change
(
self
.
USERNAME
,
u
'new+email@example.com'
,
u'wrong password'
)
def
test_confirm_email_change_invalid_activation_key
(
self
):
def
test_confirm_email_change_invalid_activation_key
(
self
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"new+email@example.com"
,
self
.
PASSWORD
)
account_api
.
request_email_change
(
self
.
USERNAME
,
u
'new+email@example.com'
,
self
.
PASSWORD
)
with
self
.
assertRaises
(
account_api
.
AccountNotAuthorized
):
with
self
.
assertRaises
(
account_api
.
AccountNotAuthorized
):
account_api
.
confirm_email_change
(
u
"invalid"
)
account_api
.
confirm_email_change
(
u
'invalid'
)
def
test_confirm_email_change_no_request_pending
(
self
):
def
test_confirm_email_change_no_request_pending
(
self
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
...
@@ -213,11 +220,11 @@ class AccountApiTest(TestCase):
...
@@ -213,11 +220,11 @@ class AccountApiTest(TestCase):
# Request a change
# Request a change
activation_key
=
account_api
.
request_email_change
(
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"new+email@example.com"
,
self
.
PASSWORD
self
.
USERNAME
,
u
'new+email@example.com'
,
self
.
PASSWORD
)
)
# Another use takes the email before we confirm the change
# Another use takes the email before we confirm the change
account_api
.
create_account
(
u
"other_user"
,
u"password"
,
u"new+email@example.com"
)
account_api
.
create_account
(
u
'other_user'
,
u'password'
,
u'new+email@example.com'
)
# When we try to confirm our change, we get an error because the email is taken
# When we try to confirm our change, we get an error because the email is taken
with
self
.
assertRaises
(
account_api
.
AccountEmailAlreadyExists
):
with
self
.
assertRaises
(
account_api
.
AccountEmailAlreadyExists
):
...
@@ -229,7 +236,7 @@ class AccountApiTest(TestCase):
...
@@ -229,7 +236,7 @@ class AccountApiTest(TestCase):
def
test_confirm_email_no_user_profile
(
self
):
def
test_confirm_email_no_user_profile
(
self
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
activation_key
=
account_api
.
request_email_change
(
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"new+email@example.com"
,
self
.
PASSWORD
self
.
USERNAME
,
u
'new+email@example.com'
,
self
.
PASSWORD
)
)
# This should never happen, but just in case...
# This should never happen, but just in case...
...
@@ -243,7 +250,7 @@ class AccountApiTest(TestCase):
...
@@ -243,7 +250,7 @@ class AccountApiTest(TestCase):
# Change the email once
# Change the email once
activation_key
=
account_api
.
request_email_change
(
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"new+email@example.com"
,
self
.
PASSWORD
self
.
USERNAME
,
u
'new+email@example.com'
,
self
.
PASSWORD
)
)
account_api
.
confirm_email_change
(
activation_key
)
account_api
.
confirm_email_change
(
activation_key
)
...
@@ -256,7 +263,7 @@ class AccountApiTest(TestCase):
...
@@ -256,7 +263,7 @@ class AccountApiTest(TestCase):
# Change the email again
# Change the email again
activation_key
=
account_api
.
request_email_change
(
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
u
"another_new+email@example.com"
,
self
.
PASSWORD
self
.
USERNAME
,
u
'another_new+email@example.com'
,
self
.
PASSWORD
)
)
account_api
.
confirm_email_change
(
activation_key
)
account_api
.
confirm_email_change
(
activation_key
)
...
@@ -264,9 +271,39 @@ class AccountApiTest(TestCase):
...
@@ -264,9 +271,39 @@ class AccountApiTest(TestCase):
meta
=
UserProfile
.
objects
.
get
(
user__username
=
self
.
USERNAME
)
.
get_meta
()
meta
=
UserProfile
.
objects
.
get
(
user__username
=
self
.
USERNAME
)
.
get_meta
()
self
.
assertEqual
(
len
(
meta
[
'old_emails'
]),
2
)
self
.
assertEqual
(
len
(
meta
[
'old_emails'
]),
2
)
email
,
timestamp
=
meta
[
'old_emails'
][
1
]
email
,
timestamp
=
meta
[
'old_emails'
][
1
]
self
.
assertEqual
(
email
,
"new+email@example.com"
)
self
.
assertEqual
(
email
,
'new+email@example.com'
)
self
.
_assert_is_datetime
(
timestamp
)
self
.
_assert_is_datetime
(
timestamp
)
@skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in LMS'
)
def
test_request_password_change
(
self
):
# Create and activate an account
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
activate_account
(
activation_key
)
# Request a password change
account_api
.
request_password_change
(
self
.
EMAIL
,
self
.
ORIG_HOST
,
self
.
IS_SECURE
)
# Verify that one email message has been sent
self
.
assertEqual
(
len
(
mail
.
outbox
),
1
)
# Verify that the body of the message contains something that looks
# like an activation link
email_body
=
mail
.
outbox
[
0
]
.
body
result
=
re
.
search
(
'(?P<url>https?://[^
\
s]+)'
,
email_body
)
self
.
assertIsNot
(
result
,
None
)
@raises
(
account_api
.
AccountUserNotFound
)
@ddt.data
(
True
,
False
)
def
test_request_password_change_invalid_user
(
self
,
create_inactive_account
):
if
create_inactive_account
:
# Create an account, but do not activate it
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
request_password_change
(
self
.
EMAIL
,
self
.
ORIG_HOST
,
self
.
IS_SECURE
)
# Verify that no email messages have been sent
self
.
assertEqual
(
len
(
mail
.
outbox
),
0
)
def
_assert_is_datetime
(
self
,
timestamp
):
def
_assert_is_datetime
(
self
,
timestamp
):
if
not
timestamp
:
if
not
timestamp
:
return
False
return
False
...
...
lms/djangoapps/student_account/test/test_views.py
View file @
6013c529
...
@@ -2,7 +2,9 @@
...
@@ -2,7 +2,9 @@
""" Tests for student account views. """
""" Tests for student account views. """
import
re
import
re
from
unittest
import
skipUnless
from
urllib
import
urlencode
from
urllib
import
urlencode
from
mock
import
patch
from
mock
import
patch
import
ddt
import
ddt
from
django.test
import
TestCase
from
django.test
import
TestCase
...
@@ -13,6 +15,7 @@ from django.core import mail
...
@@ -13,6 +15,7 @@ from django.core import mail
from
util.testing
import
UrlResetMixin
from
util.testing
import
UrlResetMixin
from
user_api.api
import
account
as
account_api
from
user_api.api
import
account
as
account_api
from
user_api.api
import
profile
as
profile_api
from
user_api.api
import
profile
as
profile_api
from
util.bad_request_rate_limiter
import
BadRequestRateLimiter
@ddt.ddt
@ddt.ddt
...
@@ -21,10 +24,13 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
...
@@ -21,10 +24,13 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
USERNAME
=
u"heisenberg"
USERNAME
=
u"heisenberg"
ALTERNATE_USERNAME
=
u"walt"
ALTERNATE_USERNAME
=
u"walt"
PASSWORD
=
u"ḅḷüëṡḳÿ"
OLD_PASSWORD
=
u"ḅḷüëṡḳÿ"
NEW_PASSWORD
=
u"🄱🄸🄶🄱🄻🅄🄴"
OLD_EMAIL
=
u"walter@graymattertech.com"
OLD_EMAIL
=
u"walter@graymattertech.com"
NEW_EMAIL
=
u"walt@savewalterwhite.com"
NEW_EMAIL
=
u"walt@savewalterwhite.com"
INVALID_ATTEMPTS
=
100
INVALID_EMAILS
=
[
INVALID_EMAILS
=
[
None
,
None
,
u""
,
u""
,
...
@@ -49,19 +55,19 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
...
@@ -49,19 +55,19 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
super
(
StudentAccountViewTest
,
self
)
.
setUp
()
super
(
StudentAccountViewTest
,
self
)
.
setUp
()
# Create/activate a new account
# Create/activate a new account
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
OLD_EMAIL
)
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
OLD_
PASSWORD
,
self
.
OLD_EMAIL
)
account_api
.
activate_account
(
activation_key
)
account_api
.
activate_account
(
activation_key
)
# Login
# Login
result
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
PASSWORD
)
result
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
OLD_
PASSWORD
)
self
.
assertTrue
(
result
)
self
.
assertTrue
(
result
)
def
test_index
(
self
):
def
test_index
(
self
):
response
=
self
.
client
.
get
(
reverse
(
'account_index'
))
response
=
self
.
client
.
get
(
reverse
(
'account_index'
))
self
.
assertContains
(
response
,
"Student Account"
)
self
.
assertContains
(
response
,
"Student Account"
)
def
test_
change_email
(
self
):
def
test_
email_change
(
self
):
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
self
.
PASSWORD
)
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
self
.
OLD_
PASSWORD
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
# Verify that the email associated with the account remains unchanged
# Verify that the email associated with the account remains unchanged
...
@@ -73,8 +79,8 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
...
@@ -73,8 +79,8 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self
.
_assert_email
(
self
.
_assert_email
(
mail
.
outbox
[
0
],
mail
.
outbox
[
0
],
[
self
.
NEW_EMAIL
],
[
self
.
NEW_EMAIL
],
u
'Email Change Request'
,
u
"Email Change Request"
,
u
'There was recently a request to change the email address'
u
"There was recently a request to change the email address"
)
)
# Retrieve the activation key from the email
# Retrieve the activation key from the email
...
@@ -96,48 +102,48 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
...
@@ -96,48 +102,48 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self
.
_assert_email
(
self
.
_assert_email
(
mail
.
outbox
[
1
],
mail
.
outbox
[
1
],
[
self
.
OLD_EMAIL
,
self
.
NEW_EMAIL
],
[
self
.
OLD_EMAIL
,
self
.
NEW_EMAIL
],
u
'Email Change Successful'
,
u
"Email Change Successful"
,
u
'You successfully changed the email address'
u
"You successfully changed the email address"
)
)
def
test_email_change_wrong_password
(
self
):
def
test_email_change_wrong_password
(
self
):
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
"wrong password"
)
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
"wrong password"
)
self
.
assertEqual
(
response
.
status_code
,
401
)
self
.
assertEqual
(
response
.
status_code
,
401
)
def
test_email_change_request_
internal_erro
r
(
self
):
def
test_email_change_request_
no_use
r
(
self
):
# Patch account API to raise an internal error when an email change is requested
# Patch account API to raise an internal error when an email change is requested
with
patch
(
'student_account.views.account_api.request_email_change'
)
as
mock_call
:
with
patch
(
'student_account.views.account_api.request_email_change'
)
as
mock_call
:
mock_call
.
side_effect
=
account_api
.
AccountUserNotFound
mock_call
.
side_effect
=
account_api
.
AccountUserNotFound
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
self
.
PASSWORD
)
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
self
.
OLD_
PASSWORD
)
self
.
assertEquals
(
response
.
status_code
,
5
00
)
self
.
assertEquals
(
response
.
status_code
,
4
00
)
def
test_email_change_request_email_taken_by_active_account
(
self
):
def
test_email_change_request_email_taken_by_active_account
(
self
):
# Create/activate a second user with the new email
# Create/activate a second user with the new email
activation_key
=
account_api
.
create_account
(
self
.
ALTERNATE_USERNAME
,
self
.
PASSWORD
,
self
.
NEW_EMAIL
)
activation_key
=
account_api
.
create_account
(
self
.
ALTERNATE_USERNAME
,
self
.
OLD_
PASSWORD
,
self
.
NEW_EMAIL
)
account_api
.
activate_account
(
activation_key
)
account_api
.
activate_account
(
activation_key
)
# Request to change the original user's email to the email now used by the second user
# Request to change the original user's email to the email now used by the second user
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
self
.
PASSWORD
)
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
self
.
OLD_
PASSWORD
)
self
.
assertEquals
(
response
.
status_code
,
409
)
self
.
assertEquals
(
response
.
status_code
,
409
)
def
test_email_change_request_email_taken_by_inactive_account
(
self
):
def
test_email_change_request_email_taken_by_inactive_account
(
self
):
# Create a second user with the new email, but don't active them
# Create a second user with the new email, but don't active them
account_api
.
create_account
(
self
.
ALTERNATE_USERNAME
,
self
.
PASSWORD
,
self
.
NEW_EMAIL
)
account_api
.
create_account
(
self
.
ALTERNATE_USERNAME
,
self
.
OLD_
PASSWORD
,
self
.
NEW_EMAIL
)
# Request to change the original user's email to the email used by the inactive user
# Request to change the original user's email to the email used by the inactive user
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
self
.
PASSWORD
)
response
=
self
.
_change_email
(
self
.
NEW_EMAIL
,
self
.
OLD_
PASSWORD
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
@ddt.data
(
*
INVALID_EMAILS
)
@ddt.data
(
*
INVALID_EMAILS
)
def
test_email_change_request_email_invalid
(
self
,
invalid_email
):
def
test_email_change_request_email_invalid
(
self
,
invalid_email
):
# Request to change the user's email to an invalid address
# Request to change the user's email to an invalid address
response
=
self
.
_change_email
(
invalid_email
,
self
.
PASSWORD
)
response
=
self
.
_change_email
(
invalid_email
,
self
.
OLD_
PASSWORD
)
self
.
assertEquals
(
response
.
status_code
,
400
)
self
.
assertEquals
(
response
.
status_code
,
400
)
def
test_email_change_confirmation
(
self
):
def
test_email_change_confirmation
(
self
):
# Get an email change activation key
# Get an email change activation key
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
self
.
NEW_EMAIL
,
self
.
PASSWORD
)
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
self
.
NEW_EMAIL
,
self
.
OLD_
PASSWORD
)
# Follow the link sent in the confirmation email
# Follow the link sent in the confirmation email
response
=
self
.
client
.
get
(
reverse
(
'email_change_confirm'
,
kwargs
=
{
'key'
:
activation_key
}))
response
=
self
.
client
.
get
(
reverse
(
'email_change_confirm'
,
kwargs
=
{
'key'
:
activation_key
}))
...
@@ -158,10 +164,10 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
...
@@ -158,10 +164,10 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
def
test_email_change_confirmation_email_already_exists
(
self
):
def
test_email_change_confirmation_email_already_exists
(
self
):
# Get an email change activation key
# Get an email change activation key
email_activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
self
.
NEW_EMAIL
,
self
.
PASSWORD
)
email_activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
self
.
NEW_EMAIL
,
self
.
OLD_
PASSWORD
)
# Create/activate a second user with the new email
# Create/activate a second user with the new email
account_activation_key
=
account_api
.
create_account
(
self
.
ALTERNATE_USERNAME
,
self
.
PASSWORD
,
self
.
NEW_EMAIL
)
account_activation_key
=
account_api
.
create_account
(
self
.
ALTERNATE_USERNAME
,
self
.
OLD_
PASSWORD
,
self
.
NEW_EMAIL
)
account_api
.
activate_account
(
account_activation_key
)
account_api
.
activate_account
(
account_activation_key
)
# Follow the link sent to the original user
# Follow the link sent to the original user
...
@@ -174,7 +180,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
...
@@ -174,7 +180,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
def
test_email_change_confirmation_internal_error
(
self
):
def
test_email_change_confirmation_internal_error
(
self
):
# Get an email change activation key
# Get an email change activation key
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
self
.
NEW_EMAIL
,
self
.
PASSWORD
)
activation_key
=
account_api
.
request_email_change
(
self
.
USERNAME
,
self
.
NEW_EMAIL
,
self
.
OLD_
PASSWORD
)
# Patch account API to return an internal error
# Patch account API to return an internal error
with
patch
(
'student_account.views.account_api.confirm_email_change'
)
as
mock_call
:
with
patch
(
'student_account.views.account_api.confirm_email_change'
)
as
mock_call
:
...
@@ -183,14 +189,120 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
...
@@ -183,14 +189,120 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self
.
assertContains
(
response
,
"Something went wrong"
)
self
.
assertContains
(
response
,
"Something went wrong"
)
def
test_
change_email
_request_missing_email_param
(
self
):
def
test_
email_change
_request_missing_email_param
(
self
):
response
=
self
.
_change_email
(
None
,
self
.
PASSWORD
)
response
=
self
.
_change_email
(
None
,
self
.
OLD_
PASSWORD
)
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_
change_email
_request_missing_password_param
(
self
):
def
test_
email_change
_request_missing_password_param
(
self
):
response
=
self
.
_change_email
(
self
.
OLD_EMAIL
,
None
)
response
=
self
.
_change_email
(
self
.
OLD_EMAIL
,
None
)
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertEqual
(
response
.
status_code
,
400
)
@skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in LMS'
)
def
test_password_change
(
self
):
# Request a password change while logged in, simulating
# use of the password reset link from the account page
response
=
self
.
_change_password
()
self
.
assertEqual
(
response
.
status_code
,
200
)
# Check that an email was sent
self
.
assertEqual
(
len
(
mail
.
outbox
),
1
)
# Retrieve the activation link from the email body
email_body
=
mail
.
outbox
[
0
]
.
body
result
=
re
.
search
(
'(?P<url>https?://[^
\
s]+)'
,
email_body
)
self
.
assertIsNot
(
result
,
None
)
activation_link
=
result
.
group
(
'url'
)
# Visit the activation link
response
=
self
.
client
.
get
(
activation_link
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Submit a new password and follow the redirect to the success page
response
=
self
.
client
.
post
(
activation_link
,
# These keys are from the form on the current password reset confirmation page.
{
'new_password1'
:
self
.
NEW_PASSWORD
,
'new_password2'
:
self
.
NEW_PASSWORD
},
follow
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertContains
(
response
,
"Your password has been set."
)
# Log the user out to clear session data
self
.
client
.
logout
()
# Verify that the new password can be used to log in
result
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
NEW_PASSWORD
)
self
.
assertTrue
(
result
)
# Try reusing the activation link to change the password again
response
=
self
.
client
.
post
(
activation_link
,
{
'new_password1'
:
self
.
OLD_PASSWORD
,
'new_password2'
:
self
.
OLD_PASSWORD
},
follow
=
True
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertContains
(
response
,
"The password reset link was invalid, possibly because the link has already been used."
)
self
.
client
.
logout
()
# Verify that the old password cannot be used to log in
result
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
OLD_PASSWORD
)
self
.
assertFalse
(
result
)
# Verify that the new password continues to be valid
result
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
NEW_PASSWORD
)
self
.
assertTrue
(
result
)
@ddt.data
(
True
,
False
)
def
test_password_change_logged_out
(
self
,
send_email
):
# Log the user out
self
.
client
.
logout
()
# Request a password change while logged out, simulating
# use of the password reset link from the login page
if
send_email
:
response
=
self
.
_change_password
(
email
=
self
.
OLD_EMAIL
)
self
.
assertEqual
(
response
.
status_code
,
200
)
else
:
# Don't send an email in the POST data, simulating
# its (potentially accidental) omission in the POST
# data sent from the login page
response
=
self
.
_change_password
()
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_password_change_inactive_user
(
self
):
# Log out the user created during test setup
self
.
client
.
logout
()
# Create a second user, but do not activate it
account_api
.
create_account
(
self
.
ALTERNATE_USERNAME
,
self
.
OLD_PASSWORD
,
self
.
NEW_EMAIL
)
# Send the view the email address tied to the inactive user
response
=
self
.
_change_password
(
email
=
self
.
NEW_EMAIL
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_password_change_no_user
(
self
):
# Log out the user created during test setup
self
.
client
.
logout
()
# Send the view an email address not tied to any user
response
=
self
.
_change_password
(
email
=
self
.
NEW_EMAIL
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_password_change_rate_limited
(
self
):
# Log out the user created during test setup, to prevent the view from
# selecting the logged-in user's email address over the email provided
# in the POST data
self
.
client
.
logout
()
# Make many consecutive bad requests in an attempt to trigger the rate limiter
for
attempt
in
xrange
(
self
.
INVALID_ATTEMPTS
):
self
.
_change_password
(
email
=
self
.
NEW_EMAIL
)
response
=
self
.
_change_password
(
email
=
self
.
NEW_EMAIL
)
self
.
assertEqual
(
response
.
status_code
,
403
)
@ddt.data
(
@ddt.data
(
(
'get'
,
'account_index'
,
[]),
(
'get'
,
'account_index'
,
[]),
(
'post'
,
'email_change_request'
,
[]),
(
'post'
,
'email_change_request'
,
[]),
...
@@ -210,7 +322,8 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
...
@@ -210,7 +322,8 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
@ddt.data
(
@ddt.data
(
(
'get'
,
'account_index'
,
[]),
(
'get'
,
'account_index'
,
[]),
(
'post'
,
'email_change_request'
,
[]),
(
'post'
,
'email_change_request'
,
[]),
(
'get'
,
'email_change_confirm'
,
[
123
])
(
'get'
,
'email_change_confirm'
,
[
123
]),
(
'post'
,
'password_change_request'
,
[]),
)
)
@ddt.unpack
@ddt.unpack
def
test_require_http_method
(
self
,
correct_method
,
url_name
,
args
):
def
test_require_http_method
(
self
,
correct_method
,
url_name
,
args
):
...
@@ -238,3 +351,12 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
...
@@ -238,3 +351,12 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
data
[
'password'
]
=
password
.
encode
(
'utf-8'
)
data
[
'password'
]
=
password
.
encode
(
'utf-8'
)
return
self
.
client
.
post
(
path
=
reverse
(
'email_change_request'
),
data
=
data
)
return
self
.
client
.
post
(
path
=
reverse
(
'email_change_request'
),
data
=
data
)
def
_change_password
(
self
,
email
=
None
):
"""Request to change the user's password. """
data
=
{}
if
email
:
data
[
'email'
]
=
email
return
self
.
client
.
post
(
path
=
reverse
(
'password_change_request'
),
data
=
data
)
lms/djangoapps/student_account/urls.py
View file @
6013c529
...
@@ -5,4 +5,5 @@ urlpatterns = patterns(
...
@@ -5,4 +5,5 @@ urlpatterns = patterns(
url
(
r'^$'
,
'index'
,
name
=
'account_index'
),
url
(
r'^$'
,
'index'
,
name
=
'account_index'
),
url
(
r'^email$'
,
'email_change_request_handler'
,
name
=
'email_change_request'
),
url
(
r'^email$'
,
'email_change_request_handler'
,
name
=
'email_change_request'
),
url
(
r'^email/confirmation/(?P<key>[^/]*)$'
,
'email_change_confirmation_handler'
,
name
=
'email_change_confirm'
),
url
(
r'^email/confirmation/(?P<key>[^/]*)$'
,
'email_change_confirmation_handler'
,
name
=
'email_change_confirm'
),
url
(
r'^password$'
,
'password_change_request_handler'
,
name
=
'password_change_request'
),
)
)
lms/djangoapps/student_account/views.py
View file @
6013c529
""" Views for a student's account information. """
""" Views for a student's account information. """
import
logging
from
django.conf
import
settings
from
django.conf
import
settings
from
django.http
import
(
from
django.http
import
(
QueryDict
,
HttpResponse
,
HttpResponse
,
HttpResponseBadRequest
,
HttpResponse
BadRequest
,
HttpResponseServerError
HttpResponse
Forbidden
)
)
from
django.core.mail
import
send_mail
from
django.core.mail
import
send_mail
from
django_future.csrf
import
ensure_csrf_cookie
from
django_future.csrf
import
ensure_csrf_cookie
...
@@ -14,6 +16,10 @@ from microsite_configuration import microsite
...
@@ -14,6 +16,10 @@ from microsite_configuration import microsite
from
user_api.api
import
account
as
account_api
from
user_api.api
import
account
as
account_api
from
user_api.api
import
profile
as
profile_api
from
user_api.api
import
profile
as
profile_api
from
util.bad_request_rate_limiter
import
BadRequestRateLimiter
AUDIT_LOG
=
logging
.
getLogger
(
"audit"
)
@login_required
@login_required
...
@@ -47,18 +53,20 @@ def index(request):
...
@@ -47,18 +53,20 @@ def index(request):
def
email_change_request_handler
(
request
):
def
email_change_request_handler
(
request
):
"""Handle a request to change the user's email address.
"""Handle a request to change the user's email address.
Sends an email to the newly specified address containing a link
to a confirmation page.
Args:
Args:
request (HttpRequest)
request (HttpRequest)
Returns:
Returns:
HttpResponse: 200 if the confirmation email was sent successfully
HttpResponse: 200 if the confirmation email was sent successfully
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 400 if the format of the new email is incorrect
HttpResponse: 400 if the format of the new email is incorrect, or if
an email change is requested for a user which does not exist
HttpResponse: 401 if the provided password (in the form) is incorrect
HttpResponse: 401 if the provided password (in the form) is incorrect
HttpResponse: 405 if using an unsupported HTTP method
HttpResponse: 405 if using an unsupported HTTP method
HttpResponse: 409 if the provided email is already in use
HttpResponse: 409 if the provided email is already in use
HttpResponse: 500 if the user to which the email change will be applied
does not exist
Example usage:
Example usage:
...
@@ -78,12 +86,10 @@ def email_change_request_handler(request):
...
@@ -78,12 +86,10 @@ def email_change_request_handler(request):
try
:
try
:
key
=
account_api
.
request_email_change
(
username
,
new_email
,
password
)
key
=
account_api
.
request_email_change
(
username
,
new_email
,
password
)
except
account_api
.
AccountUserNotFound
:
except
(
account_api
.
AccountEmailInvalid
,
account_api
.
AccountUserNotFound
)
:
return
HttpResponse
ServerError
()
return
HttpResponse
BadRequest
()
except
account_api
.
AccountEmailAlreadyExists
:
except
account_api
.
AccountEmailAlreadyExists
:
return
HttpResponse
(
status
=
409
)
return
HttpResponse
(
status
=
409
)
except
account_api
.
AccountEmailInvalid
:
return
HttpResponseBadRequest
()
except
account_api
.
AccountNotAuthorized
:
except
account_api
.
AccountNotAuthorized
:
return
HttpResponse
(
status
=
401
)
return
HttpResponse
(
status
=
401
)
...
@@ -105,7 +111,6 @@ def email_change_request_handler(request):
...
@@ -105,7 +111,6 @@ def email_change_request_handler(request):
# Send a confirmation email to the new address containing the activation key
# Send a confirmation email to the new address containing the activation key
send_mail
(
subject
,
message
,
from_address
,
[
new_email
])
send_mail
(
subject
,
message
,
from_address
,
[
new_email
])
# Send a 200 response code to the client to indicate that the email was sent successfully.
return
HttpResponse
(
status
=
200
)
return
HttpResponse
(
status
=
200
)
...
@@ -130,7 +135,7 @@ def email_change_confirmation_handler(request, key):
...
@@ -130,7 +135,7 @@ def email_change_confirmation_handler(request, key):
Example usage:
Example usage:
GET /account/email
_change_confirm
/{key}
GET /account/email
/confirmation
/{key}
"""
"""
try
:
try
:
...
@@ -179,3 +184,53 @@ def email_change_confirmation_handler(request, key):
...
@@ -179,3 +184,53 @@ def email_change_confirmation_handler(request, key):
'disable_courseware_js'
:
True
,
'disable_courseware_js'
:
True
,
}
}
)
)
@require_http_methods
([
'POST'
])
def
password_change_request_handler
(
request
):
"""Handle password change requests originating from the account page.
Uses the Account API to email the user a link to the password reset page.
Note:
The next step in the password reset process (confirmation) is currently handled
by student.views.password_reset_confirm_wrapper, a custom wrapper around Django's
password reset confirmation view.
Args:
request (HttpRequest)
Returns:
HttpResponse: 200 if the email was sent successfully
HttpResponse: 400 if there is no 'email' POST parameter, or if no user with
the provided email exists
HttpResponse: 403 if the client has been rate limited
HttpResponse: 405 if using an unsupported HTTP method
Example usage:
POST /account/password
"""
limiter
=
BadRequestRateLimiter
()
if
limiter
.
is_rate_limit_exceeded
(
request
):
AUDIT_LOG
.
warning
(
"Password reset rate limit exceeded"
)
return
HttpResponseForbidden
()
user
=
request
.
user
# Prefer logged-in user's email
email
=
user
.
email
if
user
.
is_authenticated
()
else
request
.
POST
.
get
(
'email'
)
if
email
:
try
:
account_api
.
request_password_change
(
email
,
request
.
get_host
(),
request
.
is_secure
())
except
account_api
.
AccountUserNotFound
:
AUDIT_LOG
.
info
(
"Invalid password reset attempt"
)
# Increment the rate limit counter
limiter
.
tick_bad_request_counter
(
request
)
return
HttpResponseBadRequest
(
"No active user with the provided email address exists."
)
return
HttpResponse
(
status
=
200
)
else
:
return
HttpResponseBadRequest
(
"No email address provided."
)
lms/static/js/spec/student_account/account.js
View file @
6013c529
...
@@ -97,6 +97,11 @@ define(['js/student_account/account'],
...
@@ -97,6 +97,11 @@ define(['js/student_account/account'],
view
.
submit
(
fakeEvent
);
view
.
submit
(
fakeEvent
);
};
};
var
requestPasswordChange
=
function
()
{
var
fakeEvent
=
{
preventDefault
:
function
()
{}};
view
.
click
(
fakeEvent
);
};
var
assertAjax
=
function
(
url
,
method
,
data
)
{
var
assertAjax
=
function
(
url
,
method
,
data
)
{
expect
(
$
.
ajax
).
toHaveBeenCalled
();
expect
(
$
.
ajax
).
toHaveBeenCalled
();
var
ajaxArgs
=
$
.
ajax
.
mostRecentCall
.
args
[
0
];
var
ajaxArgs
=
$
.
ajax
.
mostRecentCall
.
args
[
0
];
...
@@ -106,31 +111,13 @@ define(['js/student_account/account'],
...
@@ -106,31 +111,13 @@ define(['js/student_account/account'],
expect
(
ajaxArgs
.
headers
.
hasOwnProperty
(
"X-CSRFToken"
)).
toBe
(
true
);
expect
(
ajaxArgs
.
headers
.
hasOwnProperty
(
"X-CSRFToken"
)).
toBe
(
true
);
};
};
var
assert
EmailStatus
=
function
(
succe
ss
,
expectedStatus
)
{
var
assert
Status
=
function
(
selection
,
success
,
errorCla
ss
,
expectedStatus
)
{
if
(
!
success
)
{
if
(
!
success
)
{
expect
(
view
.
$emailStatus
).
toHaveClass
(
"validation-error"
);
expect
(
selection
).
toHaveClass
(
errorClass
);
}
else
{
}
else
{
expect
(
view
.
$emailStatus
).
not
.
toHaveClass
(
"validation-error"
);
expect
(
selection
).
not
.
toHaveClass
(
errorClass
);
}
}
expect
(
view
.
$emailStatus
.
text
()).
toEqual
(
expectedStatus
);
expect
(
selection
.
text
()).
toEqual
(
expectedStatus
);
};
var
assertPasswordStatus
=
function
(
success
,
expectedStatus
)
{
if
(
!
success
)
{
expect
(
view
.
$passwordStatus
).
toHaveClass
(
"validation-error"
);
}
else
{
expect
(
view
.
$passwordStatus
).
not
.
toHaveClass
(
"validation-error"
);
}
expect
(
view
.
$passwordStatus
.
text
()).
toEqual
(
expectedStatus
);
};
var
assertRequestStatus
=
function
(
success
,
expectedStatus
)
{
if
(
!
success
)
{
expect
(
view
.
$requestStatus
).
toHaveClass
(
"error"
);
}
else
{
expect
(
view
.
$requestStatus
).
not
.
toHaveClass
(
"error"
);
}
expect
(
view
.
$requestStatus
.
text
()).
toEqual
(
expectedStatus
);
};
};
beforeEach
(
function
()
{
beforeEach
(
function
()
{
...
@@ -139,7 +126,7 @@ define(['js/student_account/account'],
...
@@ -139,7 +126,7 @@ define(['js/student_account/account'],
view
=
new
edx
.
student
.
account
.
AccountView
().
render
();
view
=
new
edx
.
student
.
account
.
AccountView
().
render
();
// Stub Ajax cals to return success/failure
// Stub Ajax cal
l
s to return success/failure
spyOn
(
$
,
"ajax"
).
andCallFake
(
function
()
{
spyOn
(
$
,
"ajax"
).
andCallFake
(
function
()
{
return
$
.
Deferred
(
function
(
defer
)
{
return
$
.
Deferred
(
function
(
defer
)
{
if
(
ajaxSuccess
)
{
if
(
ajaxSuccess
)
{
...
@@ -157,39 +144,57 @@ define(['js/student_account/account'],
...
@@ -157,39 +144,57 @@ define(['js/student_account/account'],
email
:
"bob@example.com"
,
email
:
"bob@example.com"
,
password
:
"password"
password
:
"password"
});
});
assert
RequestStatus
(
true
,
"Please check your email to confirm the change"
);
assert
Status
(
view
.
$requestStatus
,
true
,
"error"
,
"Please check your email to confirm the change"
);
});
});
it
(
"displays email validation errors"
,
function
()
{
it
(
"displays email validation errors"
,
function
()
{
// Invalid email should display an error
// Invalid email should display an error
requestEmailChange
(
"invalid"
,
"password"
);
requestEmailChange
(
"invalid"
,
"password"
);
assert
EmailStatus
(
false
,
"Please enter a valid email address"
);
assert
Status
(
view
.
$emailStatus
,
false
,
"validation-error"
,
"Please enter a valid email address"
);
// Once the error is fixed, the status should return to normal
// Once the error is fixed, the status should return to normal
requestEmailChange
(
"bob@example.com"
,
"password"
);
requestEmailChange
(
"bob@example.com"
,
"password"
);
assert
EmailStatus
(
true
,
""
);
assert
Status
(
view
.
$emailStatus
,
true
,
"validation-error"
,
""
);
});
});
it
(
"displays an invalid password error"
,
function
()
{
it
(
"displays an invalid password error"
,
function
()
{
// Password cannot be empty
// Password cannot be empty
requestEmailChange
(
"bob@example.com"
,
""
);
requestEmailChange
(
"bob@example.com"
,
""
);
assert
PasswordStatus
(
false
,
"Please enter a valid password"
);
assert
Status
(
view
.
$passwordStatus
,
false
,
"validation-error"
,
"Please enter a valid password"
);
// Once the error is fixed, the status should return to normal
// Once the error is fixed, the status should return to normal
requestEmailChange
(
"bob@example.com"
,
"password"
);
requestEmailChange
(
"bob@example.com"
,
"password"
);
assert
PasswordStatus
(
true
,
""
);
assert
Status
(
view
.
$passwordStatus
,
true
,
"validation-error"
,
""
);
});
});
it
(
"displays server errors"
,
function
()
{
it
(
"displays server errors"
,
function
()
{
// Simulate an error from the server
// Simulate an error from the server
ajaxSuccess
=
false
;
ajaxSuccess
=
false
;
requestEmailChange
(
"bob@example.com"
,
"password"
);
requestEmailChange
(
"bob@example.com"
,
"password"
);
assert
RequestStatus
(
false
,
"The data could not be saved."
);
assert
Status
(
view
.
$requestStatus
,
false
,
"error"
,
"The data could not be saved."
);
// On retry, it should succeed
// On retry, it should succeed
ajaxSuccess
=
true
;
ajaxSuccess
=
true
;
requestEmailChange
(
"bob@example.com"
,
"password"
);
requestEmailChange
(
"bob@example.com"
,
"password"
);
assertRequestStatus
(
true
,
"Please check your email to confirm the change"
);
assertStatus
(
view
.
$requestStatus
,
true
,
"error"
,
"Please check your email to confirm the change"
);
});
it
(
"requests a password reset"
,
function
()
{
requestPasswordChange
();
assertAjax
(
"password"
,
"POST"
,
{});
assertStatus
(
view
.
$passwordResetStatus
,
true
,
"error"
,
"Password reset email sent. Follow the link in the email to change your password."
);
});
it
(
"displays an error message if a password reset email could not be sent"
,
function
()
{
// Simulate an error from the server
ajaxSuccess
=
false
;
requestPasswordChange
();
assertStatus
(
view
.
$passwordResetStatus
,
false
,
"error"
,
"We weren't able to send you a password reset email."
);
// Retry, this time simulating success
ajaxSuccess
=
true
;
requestPasswordChange
();
assertStatus
(
view
.
$passwordResetStatus
,
true
,
"error"
,
"Password reset email sent. Follow the link in the email to change your password."
);
});
});
});
});
}
}
...
...
lms/static/js/student_account/account.js
View file @
6013c529
...
@@ -71,11 +71,12 @@ var edx = edx || {};
...
@@ -71,11 +71,12 @@ var edx = edx || {};
events
:
{
events
:
{
'submit'
:
'submit'
,
'submit'
:
'submit'
,
'change'
:
'change'
'change'
:
'change'
,
'click #password-reset'
:
'click'
},
},
initialize
:
function
()
{
initialize
:
function
()
{
_
.
bindAll
(
this
,
'render'
,
'submit'
,
'change'
,
'clearStatus'
,
'invalid'
,
'error'
,
'sync'
);
_
.
bindAll
(
this
,
'render'
,
'submit'
,
'change'
,
'cl
ick'
,
'cl
earStatus'
,
'invalid'
,
'error'
,
'sync'
);
this
.
model
=
new
edx
.
student
.
account
.
AccountModel
();
this
.
model
=
new
edx
.
student
.
account
.
AccountModel
();
this
.
model
.
on
(
'invalid'
,
this
.
invalid
);
this
.
model
.
on
(
'invalid'
,
this
.
invalid
);
this
.
model
.
on
(
'error'
,
this
.
error
);
this
.
model
.
on
(
'error'
,
this
.
error
);
...
@@ -89,6 +90,9 @@ var edx = edx || {};
...
@@ -89,6 +90,9 @@ var edx = edx || {};
this
.
$emailStatus
=
$
(
'#new-email-status'
,
this
.
$el
);
this
.
$emailStatus
=
$
(
'#new-email-status'
,
this
.
$el
);
this
.
$passwordStatus
=
$
(
'#password-status'
,
this
.
$el
);
this
.
$passwordStatus
=
$
(
'#password-status'
,
this
.
$el
);
this
.
$requestStatus
=
$
(
'#request-email-status'
,
this
.
$el
);
this
.
$requestStatus
=
$
(
'#request-email-status'
,
this
.
$el
);
this
.
$passwordReset
=
$
(
'#password-reset'
,
this
.
$el
);
this
.
$passwordResetStatus
=
$
(
'#password-reset-status'
,
this
.
$el
);
return
this
;
return
this
;
},
},
...
@@ -105,6 +109,31 @@ var edx = edx || {};
...
@@ -105,6 +109,31 @@ var edx = edx || {};
});
});
},
},
click
:
function
(
event
)
{
event
.
preventDefault
();
this
.
clearStatus
();
self
=
this
;
$
.
ajax
({
url
:
'password'
,
type
:
'POST'
,
data
:
{},
headers
:
{
'X-CSRFToken'
:
$
.
cookie
(
'csrftoken'
)
}
})
.
done
(
function
()
{
self
.
$passwordResetStatus
.
addClass
(
'success'
)
.
text
(
gettext
(
"Password reset email sent. Follow the link in the email to change your password."
));
})
.
fail
(
function
()
{
self
.
$passwordResetStatus
.
addClass
(
'error'
)
.
text
(
gettext
(
"We weren't able to send you a password reset email."
));
});
},
invalid
:
function
(
model
)
{
invalid
:
function
(
model
)
{
var
errors
=
model
.
validationError
;
var
errors
=
model
.
validationError
;
...
@@ -145,6 +174,10 @@ var edx = edx || {};
...
@@ -145,6 +174,10 @@ var edx = edx || {};
this
.
$requestStatus
this
.
$requestStatus
.
removeClass
(
'error'
)
.
removeClass
(
'error'
)
.
text
(
""
);
.
text
(
""
);
this
.
$passwordResetStatus
.
removeClass
(
'error'
)
.
text
(
""
);
},
},
});
});
...
...
lms/templates/student_account/account.underscore
View file @
6013c529
<form id="email-change-form" method="post">
<form id="email-change-form" method="post">
<label for="new-email"><%- gettext(
'New Address'
) %></label>
<label for="new-email"><%- gettext(
"New Address"
) %></label>
<input id="new-email" type="text" name="new-email" value="" placeholder="xsy@edx.org" data-validate="required email"/>
<input id="new-email" type="text" name="new-email" value="" placeholder="xsy@edx.org" data-validate="required email"/>
<div id="new-email-status" />
<div id="new-email-status" />
<label for="password"><%- gettext(
'Password'
) %></label>
<label for="password"><%- gettext(
"Password"
) %></label>
<input id="password" type="password" name="password" value="" data-validate="required"/>
<input id="password" type="password" name="password" value="" data-validate="required"/>
<div id="password-status" />
<div id="password-status" />
<div class="submit-button">
<div class="submit-button">
<input type="submit" id="email-change-submit" value="<%- gettext(
'Change My Email Address'
) %>">
<input type="submit" id="email-change-submit" value="<%- gettext(
"Change My Email Address"
) %>">
</div>
</div>
<div id="request-email-status" />
<div id="request-email-status" />
<div id="password-reset">
<a href="#"><%- gettext("Reset Password") %></a>
</div>
<div id="password-reset-status" />
</form>
</form>
lms/templates/student_account/index.html
View file @
6013c529
...
@@ -23,4 +23,4 @@
...
@@ -23,4 +23,4 @@
<p>
This is a placeholder for the student's account page.
</p>
<p>
This is a placeholder for the student's account page.
</p>
<div
id=
"account-container"
/
>
<div
id=
"account-container"
></div
>
lms/templates/student_profile/index.html
View file @
6013c529
...
@@ -19,7 +19,7 @@
...
@@ -19,7 +19,7 @@
% endfor
% endfor
</
%
block>
</
%
block>
<h1>
${_("Student Profile")}
</h1>
<h1>
Student Profile
</h1>
<p>
This is a placeholder for the student's profile page.
</p>
<p>
This is a placeholder for the student's profile page.
</p>
...
...
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