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
37dd0ec8
Commit
37dd0ec8
authored
Mar 17, 2015
by
Andy Armstrong
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7327 from edx/andya/preferences-api
Implement full preferences API
parents
ba85f82e
6976a33a
Show whitespace changes
Inline
Side-by-side
Showing
55 changed files
with
2460 additions
and
1157 deletions
+2460
-1157
common/djangoapps/enrollment/views.py
+2
-2
common/djangoapps/lang_pref/middleware.py
+2
-2
common/djangoapps/lang_pref/tests/test_middleware.py
+3
-3
common/djangoapps/lang_pref/tests/test_views.py
+3
-3
common/djangoapps/lang_pref/views.py
+2
-3
common/djangoapps/student/tests/factories.py
+1
-1
common/djangoapps/student/tests/test_create_account.py
+6
-6
common/djangoapps/student/tests/test_enrollment.py
+1
-1
common/djangoapps/student/views.py
+16
-20
common/djangoapps/third_party_auth/pipeline.py
+4
-3
lms/djangoapps/instructor/enrollment.py
+3
-1
lms/djangoapps/instructor/tests/test_api_email_localization.py
+3
-3
lms/djangoapps/instructor/views/api.py
+5
-4
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+1
-1
lms/djangoapps/lms_xblock/runtime.py
+1
-1
lms/djangoapps/mobile_api/social_facebook/preferences/views.py
+3
-3
lms/djangoapps/mobile_api/social_facebook/test_utils.py
+4
-3
lms/djangoapps/mobile_api/social_facebook/utils.py
+11
-3
lms/djangoapps/notification_prefs/features/unsubscribe.py
+3
-3
lms/djangoapps/notification_prefs/views.py
+7
-13
lms/djangoapps/oauth2_handler/handlers.py
+2
-2
lms/djangoapps/oauth2_handler/tests.py
+2
-2
lms/djangoapps/student_account/test/test_views.py
+8
-8
lms/djangoapps/student_account/views.py
+5
-7
lms/djangoapps/verify_student/views.py
+2
-2
openedx/core/djangoapps/user_api/accounts/__init__.py
+14
-1
openedx/core/djangoapps/user_api/accounts/api.py
+315
-28
openedx/core/djangoapps/user_api/accounts/serializers.py
+2
-2
openedx/core/djangoapps/user_api/accounts/tests/test_api.py
+209
-32
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+66
-29
openedx/core/djangoapps/user_api/accounts/views.py
+6
-8
openedx/core/djangoapps/user_api/api/account.py
+0
-381
openedx/core/djangoapps/user_api/api/profile.py
+0
-158
openedx/core/djangoapps/user_api/course_tag/__init__.py
+0
-0
openedx/core/djangoapps/user_api/course_tag/api.py
+0
-0
openedx/core/djangoapps/user_api/course_tag/tests/test_api.py
+1
-1
openedx/core/djangoapps/user_api/errors.py
+95
-0
openedx/core/djangoapps/user_api/helpers.py
+12
-1
openedx/core/djangoapps/user_api/management/tests/test_email_opt_in_list.py
+2
-2
openedx/core/djangoapps/user_api/models.py
+15
-16
openedx/core/djangoapps/user_api/partition_schemes.py
+1
-1
openedx/core/djangoapps/user_api/preferences/__init__.py
+0
-0
openedx/core/djangoapps/user_api/preferences/api.py
+391
-0
openedx/core/djangoapps/user_api/preferences/tests/__init__.py
+0
-0
openedx/core/djangoapps/user_api/preferences/tests/test_api.py
+415
-0
openedx/core/djangoapps/user_api/preferences/tests/test_views.py
+531
-0
openedx/core/djangoapps/user_api/preferences/views.py
+208
-0
openedx/core/djangoapps/user_api/serializers.py
+10
-0
openedx/core/djangoapps/user_api/tests/test_account_api.py
+0
-168
openedx/core/djangoapps/user_api/tests/test_models.py
+6
-7
openedx/core/djangoapps/user_api/tests/test_profile_api.py
+0
-163
openedx/core/djangoapps/user_api/tests/test_views.py
+27
-31
openedx/core/djangoapps/user_api/urls.py
+11
-0
openedx/core/djangoapps/user_api/views.py
+21
-16
openedx/core/lib/api/permissions.py
+2
-12
No files found.
common/djangoapps/enrollment/views.py
View file @
37dd0ec8
...
@@ -6,7 +6,7 @@ consist primarily of authentication, request validation, and serialization.
...
@@ -6,7 +6,7 @@ consist primarily of authentication, request validation, and serialization.
from
ipware.ip
import
get_ip
from
ipware.ip
import
get_ip
from
django.utils.decorators
import
method_decorator
from
django.utils.decorators
import
method_decorator
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
openedx.core.djangoapps.user_api
import
api
as
user_api
from
openedx.core.djangoapps.user_api
.preferences.api
import
update_email_opt_in
from
openedx.core.lib.api.permissions
import
ApiKeyHeaderPermission
,
ApiKeyHeaderPermissionIsAuthenticated
from
openedx.core.lib.api.permissions
import
ApiKeyHeaderPermission
,
ApiKeyHeaderPermissionIsAuthenticated
from
rest_framework
import
status
from
rest_framework
import
status
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
...
@@ -349,7 +349,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
...
@@ -349,7 +349,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
email_opt_in
=
request
.
DATA
.
get
(
'email_opt_in'
,
None
)
email_opt_in
=
request
.
DATA
.
get
(
'email_opt_in'
,
None
)
if
email_opt_in
is
not
None
:
if
email_opt_in
is
not
None
:
org
=
course_id
.
org
org
=
course_id
.
org
u
ser_api
.
profile
.
u
pdate_email_opt_in
(
request
.
user
,
org
,
email_opt_in
)
update_email_opt_in
(
request
.
user
,
org
,
email_opt_in
)
return
Response
(
response
)
return
Response
(
response
)
except
CourseModeNotFoundError
as
error
:
except
CourseModeNotFoundError
as
error
:
return
Response
(
return
Response
(
...
...
common/djangoapps/lang_pref/middleware.py
View file @
37dd0ec8
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
Middleware for Language Preferences
Middleware for Language Preferences
"""
"""
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
get_user_p
reference
from
lang_pref
import
LANGUAGE_KEY
from
lang_pref
import
LANGUAGE_KEY
...
@@ -20,6 +20,6 @@ class LanguagePreferenceMiddleware(object):
...
@@ -20,6 +20,6 @@ class LanguagePreferenceMiddleware(object):
no language set on the session (i.e. from dark language overrides), use the user's preference.
no language set on the session (i.e. from dark language overrides), use the user's preference.
"""
"""
if
request
.
user
.
is_authenticated
()
and
'django_language'
not
in
request
.
session
:
if
request
.
user
.
is_authenticated
()
and
'django_language'
not
in
request
.
session
:
user_pref
=
UserPreference
.
get
_preference
(
request
.
user
,
LANGUAGE_KEY
)
user_pref
=
get_user
_preference
(
request
.
user
,
LANGUAGE_KEY
)
if
user_pref
:
if
user_pref
:
request
.
session
[
'django_language'
]
=
user_pref
request
.
session
[
'django_language'
]
=
user_pref
common/djangoapps/lang_pref/tests/test_middleware.py
View file @
37dd0ec8
...
@@ -3,7 +3,7 @@ from django.test.client import RequestFactory
...
@@ -3,7 +3,7 @@ from django.test.client import RequestFactory
from
django.contrib.sessions.middleware
import
SessionMiddleware
from
django.contrib.sessions.middleware
import
SessionMiddleware
from
lang_pref.middleware
import
LanguagePreferenceMiddleware
from
lang_pref.middleware
import
LanguagePreferenceMiddleware
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
set_user_p
reference
from
lang_pref
import
LANGUAGE_KEY
from
lang_pref
import
LANGUAGE_KEY
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
...
@@ -28,7 +28,7 @@ class TestUserPreferenceMiddleware(TestCase):
...
@@ -28,7 +28,7 @@ class TestUserPreferenceMiddleware(TestCase):
def
test_language_in_user_prefs
(
self
):
def
test_language_in_user_prefs
(
self
):
# language set in the user preferences and not the session
# language set in the user preferences and not the session
UserPreference
.
set
_preference
(
self
.
user
,
LANGUAGE_KEY
,
'eo'
)
set_user
_preference
(
self
.
user
,
LANGUAGE_KEY
,
'eo'
)
self
.
middleware
.
process_request
(
self
.
request
)
self
.
middleware
.
process_request
(
self
.
request
)
self
.
assertEquals
(
self
.
request
.
session
[
'django_language'
],
'eo'
)
self
.
assertEquals
(
self
.
request
.
session
[
'django_language'
],
'eo'
)
...
@@ -36,7 +36,7 @@ class TestUserPreferenceMiddleware(TestCase):
...
@@ -36,7 +36,7 @@ class TestUserPreferenceMiddleware(TestCase):
# language set in both the user preferences and session,
# language set in both the user preferences and session,
# session should get precedence
# session should get precedence
self
.
request
.
session
[
'django_language'
]
=
'en'
self
.
request
.
session
[
'django_language'
]
=
'en'
UserPreference
.
set
_preference
(
self
.
user
,
LANGUAGE_KEY
,
'eo'
)
set_user
_preference
(
self
.
user
,
LANGUAGE_KEY
,
'eo'
)
self
.
middleware
.
process_request
(
self
.
request
)
self
.
middleware
.
process_request
(
self
.
request
)
self
.
assertEquals
(
self
.
request
.
session
[
'django_language'
],
'en'
)
self
.
assertEquals
(
self
.
request
.
session
[
'django_language'
],
'en'
)
common/djangoapps/lang_pref/tests/test_views.py
View file @
37dd0ec8
...
@@ -4,7 +4,7 @@ Tests for the language setting view
...
@@ -4,7 +4,7 @@ Tests for the language setting view
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.test
import
TestCase
from
django.test
import
TestCase
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
get_user_p
reference
from
lang_pref
import
LANGUAGE_KEY
from
lang_pref
import
LANGUAGE_KEY
...
@@ -20,7 +20,7 @@ class TestLanguageSetting(TestCase):
...
@@ -20,7 +20,7 @@ class TestLanguageSetting(TestCase):
response
=
self
.
client
.
post
(
reverse
(
'lang_pref_set_language'
),
{
'language'
:
lang
})
response
=
self
.
client
.
post
(
reverse
(
'lang_pref_set_language'
),
{
'language'
:
lang
})
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
user_pref
=
UserPreference
.
get
_preference
(
user
,
LANGUAGE_KEY
)
user_pref
=
get_user
_preference
(
user
,
LANGUAGE_KEY
)
self
.
assertEqual
(
user_pref
,
lang
)
self
.
assertEqual
(
user_pref
,
lang
)
def
test_set_preference_missing_lang
(
self
):
def
test_set_preference_missing_lang
(
self
):
...
@@ -31,4 +31,4 @@ class TestLanguageSetting(TestCase):
...
@@ -31,4 +31,4 @@ class TestLanguageSetting(TestCase):
self
.
assertEquals
(
response
.
status_code
,
400
)
self
.
assertEquals
(
response
.
status_code
,
400
)
self
.
assertIsNone
(
UserPreference
.
get
_preference
(
user
,
LANGUAGE_KEY
))
self
.
assertIsNone
(
get_user
_preference
(
user
,
LANGUAGE_KEY
))
common/djangoapps/lang_pref/views.py
View file @
37dd0ec8
...
@@ -4,7 +4,7 @@ Views for accessing language preferences
...
@@ -4,7 +4,7 @@ Views for accessing language preferences
from
django.contrib.auth.decorators
import
login_required
from
django.contrib.auth.decorators
import
login_required
from
django.http
import
HttpResponse
,
HttpResponseBadRequest
from
django.http
import
HttpResponse
,
HttpResponseBadRequest
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
set_user_p
reference
from
lang_pref
import
LANGUAGE_KEY
from
lang_pref
import
LANGUAGE_KEY
...
@@ -13,11 +13,10 @@ def set_language(request):
...
@@ -13,11 +13,10 @@ def set_language(request):
"""
"""
This view is called when the user would like to set a language preference
This view is called when the user would like to set a language preference
"""
"""
user
=
request
.
user
lang_pref
=
request
.
POST
.
get
(
'language'
,
None
)
lang_pref
=
request
.
POST
.
get
(
'language'
,
None
)
if
lang_pref
:
if
lang_pref
:
UserPreference
.
set_preference
(
user
,
LANGUAGE_KEY
,
lang_pref
)
set_user_preference
(
request
.
user
,
LANGUAGE_KEY
,
lang_pref
)
return
HttpResponse
(
'{"success": true}'
)
return
HttpResponse
(
'{"success": true}'
)
return
HttpResponseBadRequest
(
'no language provided'
)
return
HttpResponseBadRequest
(
'no language provided'
)
common/djangoapps/student/tests/factories.py
View file @
37dd0ec8
...
@@ -40,7 +40,7 @@ class UserProfileFactory(DjangoModelFactory):
...
@@ -40,7 +40,7 @@ class UserProfileFactory(DjangoModelFactory):
level_of_education
=
None
level_of_education
=
None
gender
=
u'm'
gender
=
u'm'
mailing_address
=
None
mailing_address
=
None
goals
=
u'
World domination
'
goals
=
u'
Learn a lot
'
class
CourseModeFactory
(
DjangoModelFactory
):
class
CourseModeFactory
(
DjangoModelFactory
):
...
...
common/djangoapps/student/tests/test_create_account.py
View file @
37dd0ec8
"
Tests for account creation
"
"
""Tests for account creation""
"
import
json
import
json
import
ddt
import
ddt
...
@@ -14,7 +14,7 @@ from django.test.utils import override_settings
...
@@ -14,7 +14,7 @@ from django.test.utils import override_settings
import
mock
import
mock
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
get_user_p
reference
from
lang_pref
import
LANGUAGE_KEY
from
lang_pref
import
LANGUAGE_KEY
from
notification_prefs
import
NOTIFICATION_PREF_KEY
from
notification_prefs
import
NOTIFICATION_PREF_KEY
...
@@ -42,7 +42,7 @@ TEST_CS_URL = 'https://comments.service.test:123/'
...
@@ -42,7 +42,7 @@ TEST_CS_URL = 'https://comments.service.test:123/'
}
}
)
)
class
TestCreateAccount
(
TestCase
):
class
TestCreateAccount
(
TestCase
):
"
Tests for account creation
"
"
""Tests for account creation""
"
def
setUp
(
self
):
def
setUp
(
self
):
self
.
username
=
"test_user"
self
.
username
=
"test_user"
...
@@ -63,14 +63,14 @@ class TestCreateAccount(TestCase):
...
@@ -63,14 +63,14 @@ class TestCreateAccount(TestCase):
response
=
self
.
client
.
post
(
self
.
url
,
self
.
params
)
response
=
self
.
client
.
post
(
self
.
url
,
self
.
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
user
=
User
.
objects
.
get
(
username
=
self
.
username
)
user
=
User
.
objects
.
get
(
username
=
self
.
username
)
self
.
assertEqual
(
UserPreference
.
get
_preference
(
user
,
LANGUAGE_KEY
),
lang
)
self
.
assertEqual
(
get_user
_preference
(
user
,
LANGUAGE_KEY
),
lang
)
@ddt.data
(
"en"
,
"eo"
)
@ddt.data
(
"en"
,
"eo"
)
def
test_header_lang_pref_saved
(
self
,
lang
):
def
test_header_lang_pref_saved
(
self
,
lang
):
response
=
self
.
client
.
post
(
self
.
url
,
self
.
params
,
HTTP_ACCEPT_LANGUAGE
=
lang
)
response
=
self
.
client
.
post
(
self
.
url
,
self
.
params
,
HTTP_ACCEPT_LANGUAGE
=
lang
)
user
=
User
.
objects
.
get
(
username
=
self
.
username
)
user
=
User
.
objects
.
get
(
username
=
self
.
username
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
UserPreference
.
get
_preference
(
user
,
LANGUAGE_KEY
),
lang
)
self
.
assertEqual
(
get_user
_preference
(
user
,
LANGUAGE_KEY
),
lang
)
def
create_account_and_fetch_profile
(
self
):
def
create_account_and_fetch_profile
(
self
):
"""
"""
...
@@ -225,7 +225,7 @@ class TestCreateAccount(TestCase):
...
@@ -225,7 +225,7 @@ class TestCreateAccount(TestCase):
response
=
self
.
client
.
post
(
self
.
url
,
self
.
params
)
response
=
self
.
client
.
post
(
self
.
url
,
self
.
params
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
user
=
User
.
objects
.
get
(
username
=
self
.
username
)
user
=
User
.
objects
.
get
(
username
=
self
.
username
)
preference
=
UserPreference
.
get
_preference
(
user
,
NOTIFICATION_PREF_KEY
)
preference
=
get_user
_preference
(
user
,
NOTIFICATION_PREF_KEY
)
if
digest_enabled
:
if
digest_enabled
:
self
.
assertIsNotNone
(
preference
)
self
.
assertIsNotNone
(
preference
)
else
:
else
:
...
...
common/djangoapps/student/tests/test_enrollment.py
View file @
37dd0ec8
...
@@ -103,7 +103,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
...
@@ -103,7 +103,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
self
.
assertFalse
(
CourseEnrollment
.
is_enrolled
(
self
.
user
,
self
.
course
.
id
))
@patch.dict
(
settings
.
FEATURES
,
{
'ENABLE_MKTG_EMAIL_OPT_IN'
:
True
})
@patch.dict
(
settings
.
FEATURES
,
{
'ENABLE_MKTG_EMAIL_OPT_IN'
:
True
})
@patch
(
'openedx.core.djangoapps.user_api.
api.profile
.update_email_opt_in'
)
@patch
(
'openedx.core.djangoapps.user_api.
preferences.api
.update_email_opt_in'
)
@ddt.data
(
@ddt.data
(
([],
'true'
),
([],
'true'
),
([],
'false'
),
([],
'false'
),
...
...
common/djangoapps/student/views.py
View file @
37dd0ec8
...
@@ -85,7 +85,6 @@ from external_auth.login_and_register import (
...
@@ -85,7 +85,6 @@ from external_auth.login_and_register import (
from
bulk_email.models
import
Optout
,
CourseAuthorization
from
bulk_email.models
import
Optout
,
CourseAuthorization
import
shoppingcart
import
shoppingcart
from
lang_pref
import
LANGUAGE_KEY
from
lang_pref
import
LANGUAGE_KEY
from
notification_prefs.views
import
enable_notifications
import
track.views
import
track.views
...
@@ -118,6 +117,12 @@ from embargo import api as embargo_api
...
@@ -118,6 +117,12 @@ from embargo import api as embargo_api
import
analytics
import
analytics
from
eventtracking
import
tracker
from
eventtracking
import
tracker
# Note that this lives in LMS, so this dependency should be refactored.
from
notification_prefs.views
import
enable_notifications
# Note that this lives in openedx, so this dependency should be refactored.
from
openedx.core.djangoapps.user_api.preferences
import
api
as
preferences_api
log
=
logging
.
getLogger
(
"edx.student"
)
log
=
logging
.
getLogger
(
"edx.student"
)
AUDIT_LOG
=
logging
.
getLogger
(
"audit"
)
AUDIT_LOG
=
logging
.
getLogger
(
"audit"
)
...
@@ -632,20 +637,17 @@ def dashboard(request):
...
@@ -632,20 +637,17 @@ def dashboard(request):
# Re-alphabetize language options
# Re-alphabetize language options
language_options
.
sort
()
language_options
.
sort
()
# TODO: remove circular dependency on openedx from common
# try to get the preferred language for the user
from
openedx.core.djangoapps.user_api.models
import
UserPreference
preferred_language_code
=
preferences_api
.
get_user_preference
(
request
.
user
,
LANGUAGE_KEY
)
# try to get the prefered language for the user
cur_pref_lang_code
=
UserPreference
.
get_preference
(
request
.
user
,
LANGUAGE_KEY
)
# try and get the current language of the user
# try and get the current language of the user
cur
_lang
_code
=
get_language
()
cur
rent_language
_code
=
get_language
()
if
cur_pref_lang_code
and
cur_pref_lang
_code
in
settings
.
LANGUAGE_DICT
:
if
preferred_language_code
and
preferred_language
_code
in
settings
.
LANGUAGE_DICT
:
# if the user has a preference, get the name from the code
# if the user has a preference, get the name from the code
current_language
=
settings
.
LANGUAGE_DICT
[
cur_pref_lang
_code
]
current_language
=
settings
.
LANGUAGE_DICT
[
preferred_language
_code
]
elif
cur
_lang
_code
in
settings
.
LANGUAGE_DICT
:
elif
cur
rent_language
_code
in
settings
.
LANGUAGE_DICT
:
# if the user's browser is showing a particular language,
# if the user's browser is showing a particular language,
# use that as the current language
# use that as the current language
current_language
=
settings
.
LANGUAGE_DICT
[
cur
_lang
_code
]
current_language
=
settings
.
LANGUAGE_DICT
[
cur
rent_language
_code
]
else
:
else
:
# otherwise, use the default language
# otherwise, use the default language
current_language
=
settings
.
LANGUAGE_DICT
[
settings
.
LANGUAGE_CODE
]
current_language
=
settings
.
LANGUAGE_DICT
[
settings
.
LANGUAGE_CODE
]
...
@@ -680,7 +682,7 @@ def dashboard(request):
...
@@ -680,7 +682,7 @@ def dashboard(request):
'billing_email'
:
settings
.
PAYMENT_SUPPORT_EMAIL
,
'billing_email'
:
settings
.
PAYMENT_SUPPORT_EMAIL
,
'language_options'
:
language_options
,
'language_options'
:
language_options
,
'current_language'
:
current_language
,
'current_language'
:
current_language
,
'current_language_code'
:
cur
_lang
_code
,
'current_language_code'
:
cur
rent_language
_code
,
'user'
:
user
,
'user'
:
user
,
'duplicate_provider'
:
None
,
'duplicate_provider'
:
None
,
'logout_url'
:
reverse
(
logout_user
),
'logout_url'
:
reverse
(
logout_user
),
...
@@ -800,13 +802,10 @@ def try_change_enrollment(request):
...
@@ -800,13 +802,10 @@ def try_change_enrollment(request):
def
_update_email_opt_in
(
request
,
org
):
def
_update_email_opt_in
(
request
,
org
):
"""Helper function used to hit the profile API if email opt-in is enabled."""
"""Helper function used to hit the profile API if email opt-in is enabled."""
# TODO: remove circular dependency on openedx from common
from
openedx.core.djangoapps.user_api.api
import
profile
as
profile_api
email_opt_in
=
request
.
POST
.
get
(
'email_opt_in'
)
email_opt_in
=
request
.
POST
.
get
(
'email_opt_in'
)
if
email_opt_in
is
not
None
:
if
email_opt_in
is
not
None
:
email_opt_in_boolean
=
email_opt_in
==
'true'
email_opt_in_boolean
=
email_opt_in
==
'true'
pr
ofile
_api
.
update_email_opt_in
(
request
.
user
,
org
,
email_opt_in_boolean
)
pr
eferences
_api
.
update_email_opt_in
(
request
.
user
,
org
,
email_opt_in_boolean
)
@require_POST
@require_POST
...
@@ -1391,10 +1390,7 @@ def _do_create_account(form):
...
@@ -1391,10 +1390,7 @@ def _do_create_account(form):
log
.
exception
(
"UserProfile creation failed for user {id}."
.
format
(
id
=
user
.
id
))
log
.
exception
(
"UserProfile creation failed for user {id}."
.
format
(
id
=
user
.
id
))
raise
raise
# TODO: remove circular dependency on openedx from common
preferences_api
.
set_user_preference
(
user
,
LANGUAGE_KEY
,
get_language
())
from
openedx.core.djangoapps.user_api.models
import
UserPreference
UserPreference
.
set_preference
(
user
,
LANGUAGE_KEY
,
get_language
())
return
(
user
,
profile
,
registration
)
return
(
user
,
profile
,
registration
)
...
...
common/djangoapps/third_party_auth/pipeline.py
View file @
37dd0ec8
...
@@ -89,6 +89,9 @@ from logging import getLogger
...
@@ -89,6 +89,9 @@ from logging import getLogger
from
.
import
provider
from
.
import
provider
# Note that this lives in openedx, so this dependency should be refactored.
from
openedx.core.djangoapps.user_api.preferences.api
import
update_email_opt_in
# These are the query string params you can pass
# These are the query string params you can pass
# to the URL that starts the authentication process.
# to the URL that starts the authentication process.
...
@@ -669,10 +672,8 @@ def change_enrollment(strategy, user=None, is_dashboard=False, *args, **kwargs):
...
@@ -669,10 +672,8 @@ def change_enrollment(strategy, user=None, is_dashboard=False, *args, **kwargs):
# If the email opt in parameter is found, set the preference.
# If the email opt in parameter is found, set the preference.
email_opt_in
=
strategy
.
session_get
(
AUTH_EMAIL_OPT_IN_KEY
)
email_opt_in
=
strategy
.
session_get
(
AUTH_EMAIL_OPT_IN_KEY
)
if
email_opt_in
:
if
email_opt_in
:
# TODO: remove circular dependency on openedx from common
from
openedx.core.djangoapps.user_api.api
import
profile
opt_in
=
email_opt_in
.
lower
()
==
'true'
opt_in
=
email_opt_in
.
lower
()
==
'true'
profile
.
update_email_opt_in
(
user
,
course_id
.
org
,
opt_in
)
update_email_opt_in
(
user
,
course_id
.
org
,
opt_in
)
# Check whether we're blocked from enrolling by a
# Check whether we're blocked from enrolling by a
# country access rule.
# country access rule.
...
...
lms/djangoapps/instructor/enrollment.py
View file @
37dd0ec8
...
@@ -79,7 +79,9 @@ def get_user_email_language(user):
...
@@ -79,7 +79,9 @@ def get_user_email_language(user):
Return the language most appropriate for writing emails to user. Returns
Return the language most appropriate for writing emails to user. Returns
None if the preference has not been set, or if the user does not exist.
None if the preference has not been set, or if the user does not exist.
"""
"""
return
UserPreference
.
get_preference
(
user
,
LANGUAGE_KEY
)
# Calling UserPreference directly instead of get_user_preference because the user requesting the
# information is not "user" and also may not have is_staff access.
return
UserPreference
.
get_value
(
user
,
LANGUAGE_KEY
)
def
enroll_email
(
course_id
,
student_email
,
auto_enroll
=
False
,
email_students
=
False
,
email_params
=
None
,
language
=
None
):
def
enroll_email
(
course_id
,
student_email
,
auto_enroll
=
False
,
email_students
=
False
,
email_params
=
None
,
language
=
None
):
...
...
lms/djangoapps/instructor/tests/test_api_email_localization.py
View file @
37dd0ec8
...
@@ -10,7 +10,7 @@ from courseware.tests.factories import InstructorFactory
...
@@ -10,7 +10,7 @@ from courseware.tests.factories import InstructorFactory
from
lang_pref
import
LANGUAGE_KEY
from
lang_pref
import
LANGUAGE_KEY
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
set_user_p
reference
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
...
@@ -29,11 +29,11 @@ class TestInstructorAPIEnrollmentEmailLocalization(ModuleStoreTestCase):
...
@@ -29,11 +29,11 @@ class TestInstructorAPIEnrollmentEmailLocalization(ModuleStoreTestCase):
# French.
# French.
self
.
course
=
CourseFactory
.
create
()
self
.
course
=
CourseFactory
.
create
()
self
.
instructor
=
InstructorFactory
(
course_key
=
self
.
course
.
id
)
self
.
instructor
=
InstructorFactory
(
course_key
=
self
.
course
.
id
)
UserPreference
.
set
_preference
(
self
.
instructor
,
LANGUAGE_KEY
,
'zh-cn'
)
set_user
_preference
(
self
.
instructor
,
LANGUAGE_KEY
,
'zh-cn'
)
self
.
client
.
login
(
username
=
self
.
instructor
.
username
,
password
=
'test'
)
self
.
client
.
login
(
username
=
self
.
instructor
.
username
,
password
=
'test'
)
self
.
student
=
UserFactory
.
create
()
self
.
student
=
UserFactory
.
create
()
UserPreference
.
set
_preference
(
self
.
student
,
LANGUAGE_KEY
,
'fr'
)
set_user
_preference
(
self
.
student
,
LANGUAGE_KEY
,
'fr'
)
def
update_enrollement
(
self
,
action
,
student_email
):
def
update_enrollement
(
self
,
action
,
student_email
):
"""
"""
...
...
lms/djangoapps/instructor/views/api.py
View file @
37dd0ec8
...
@@ -79,7 +79,7 @@ import instructor_analytics.basic
...
@@ -79,7 +79,7 @@ import instructor_analytics.basic
import
instructor_analytics.distributions
import
instructor_analytics.distributions
import
instructor_analytics.csvs
import
instructor_analytics.csvs
import
csv
import
csv
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
get_user_preference
,
set_user_p
reference
from
instructor.views
import
INVOICE_KEY
from
instructor.views
import
INVOICE_KEY
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
from
submissions
import
api
as
sub_api
# installed from the edx-submissions repository
...
@@ -1238,7 +1238,7 @@ def generate_registration_codes(request, course_id):
...
@@ -1238,7 +1238,7 @@ def generate_registration_codes(request, course_id):
invoice_copy
=
True
invoice_copy
=
True
sale_price
=
unit_price
*
course_code_number
sale_price
=
unit_price
*
course_code_number
UserPreference
.
set
_preference
(
request
.
user
,
INVOICE_KEY
,
invoice_copy
)
set_user
_preference
(
request
.
user
,
INVOICE_KEY
,
invoice_copy
)
sale_invoice
=
Invoice
.
objects
.
create
(
sale_invoice
=
Invoice
.
objects
.
create
(
total_amount
=
sale_price
,
total_amount
=
sale_price
,
company_name
=
company_name
,
company_name
=
company_name
,
...
@@ -2187,8 +2187,9 @@ def get_user_invoice_preference(request, course_id): # pylint: disable=unused-a
...
@@ -2187,8 +2187,9 @@ def get_user_invoice_preference(request, course_id): # pylint: disable=unused-a
Gets invoice copy user's preferences.
Gets invoice copy user's preferences.
"""
"""
invoice_copy_preference
=
True
invoice_copy_preference
=
True
if
UserPreference
.
get_preference
(
request
.
user
,
INVOICE_KEY
)
is
not
None
:
invoice_preference_value
=
get_user_preference
(
request
.
user
,
INVOICE_KEY
)
invoice_copy_preference
=
UserPreference
.
get_preference
(
request
.
user
,
INVOICE_KEY
)
==
'True'
if
invoice_preference_value
is
not
None
:
invoice_copy_preference
=
invoice_preference_value
==
'True'
return
JsonResponse
({
return
JsonResponse
({
'invoice_copy'
:
invoice_copy_preference
'invoice_copy'
:
invoice_copy_preference
...
...
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
View file @
37dd0ec8
...
@@ -18,7 +18,7 @@ from xmodule.partitions.partitions import Group, UserPartition
...
@@ -18,7 +18,7 @@ from xmodule.partitions.partitions import Group, UserPartition
from
openedx.core.djangoapps.course_groups.models
import
CourseUserGroupPartitionGroup
from
openedx.core.djangoapps.course_groups.models
import
CourseUserGroupPartitionGroup
from
openedx.core.djangoapps.course_groups.tests.helpers
import
CohortFactory
from
openedx.core.djangoapps.course_groups.tests.helpers
import
CohortFactory
import
openedx.core.djangoapps.user_api.
api.course_tag
as
course_tag_api
import
openedx.core.djangoapps.user_api.
course_tag.api
as
course_tag_api
from
openedx.core.djangoapps.user_api.partition_schemes
import
RandomUserPartitionScheme
from
openedx.core.djangoapps.user_api.partition_schemes
import
RandomUserPartitionScheme
from
instructor_task.models
import
ReportStore
from
instructor_task.models
import
ReportStore
from
instructor_task.tasks_helper
import
cohort_students_and_upload
,
upload_grades_csv
,
upload_students_csv
from
instructor_task.tasks_helper
import
cohort_students_and_upload
,
upload_grades_csv
,
upload_students_csv
...
...
lms/djangoapps/lms_xblock/runtime.py
View file @
37dd0ec8
...
@@ -8,7 +8,7 @@ import xblock.reference.plugins
...
@@ -8,7 +8,7 @@ import xblock.reference.plugins
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.conf
import
settings
from
django.conf
import
settings
from
lms.djangoapps.lms_xblock.models
import
XBlockAsidesConfig
from
lms.djangoapps.lms_xblock.models
import
XBlockAsidesConfig
from
openedx.core.djangoapps.user_api.
api
import
course_tag
as
user_course_tag_api
from
openedx.core.djangoapps.user_api.
course_tag
import
api
as
user_course_tag_api
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.services
import
SettingsService
from
xmodule.services
import
SettingsService
from
xmodule.library_tools
import
LibraryToolsService
from
xmodule.library_tools
import
LibraryToolsService
...
...
lms/djangoapps/mobile_api/social_facebook/preferences/views.py
View file @
37dd0ec8
...
@@ -5,7 +5,7 @@ Views for users sharing preferences
...
@@ -5,7 +5,7 @@ Views for users sharing preferences
from
rest_framework
import
generics
,
status
from
rest_framework
import
generics
,
status
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
openedx.core.djangoapps.user_api.
api.profile
import
preference_info
,
update_preferences
from
openedx.core.djangoapps.user_api.
preferences.api
import
get_user_preferences
,
set_user_preference
from
...utils
import
mobile_view
from
...utils
import
mobile_view
from
.
import
serializers
from
.
import
serializers
...
@@ -42,11 +42,11 @@ class UserSharing(generics.ListCreateAPIView):
...
@@ -42,11 +42,11 @@ class UserSharing(generics.ListCreateAPIView):
serializer
=
self
.
get_serializer
(
data
=
request
.
DATA
,
files
=
request
.
FILES
)
serializer
=
self
.
get_serializer
(
data
=
request
.
DATA
,
files
=
request
.
FILES
)
if
serializer
.
is_valid
():
if
serializer
.
is_valid
():
value
=
serializer
.
object
[
'share_with_facebook_friends'
]
value
=
serializer
.
object
[
'share_with_facebook_friends'
]
update_preferences
(
request
.
user
.
username
,
share_with_facebook_friends
=
value
)
set_user_preference
(
request
.
user
,
"share_with_facebook_friends"
,
value
)
return
self
.
get
(
request
,
*
args
,
**
kwargs
)
return
self
.
get
(
request
,
*
args
,
**
kwargs
)
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
serializer
.
errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
preferences
=
preference_info
(
request
.
user
.
username
)
preferences
=
get_user_preferences
(
request
.
user
)
response
=
{
'share_with_facebook_friends'
:
preferences
.
get
(
'share_with_facebook_friends'
,
'False'
)}
response
=
{
'share_with_facebook_friends'
:
preferences
.
get
(
'share_with_facebook_friends'
,
'False'
)}
return
Response
(
response
)
return
Response
(
response
)
lms/djangoapps/mobile_api/social_facebook/test_utils.py
View file @
37dd0ec8
...
@@ -12,7 +12,7 @@ from social.apps.django_app.default.models import UserSocialAuth
...
@@ -12,7 +12,7 @@ from social.apps.django_app.default.models import UserSocialAuth
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
student.views
import
login_oauth_token
from
student.views
import
login_oauth_token
from
openedx.core.djangoapps.user_api.
api.profile
import
preference_info
,
update_preferences
from
openedx.core.djangoapps.user_api.
preferences.api
import
get_user_preference
,
set_user_preference
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
courseware.tests.factories
import
UserFactory
from
courseware.tests.factories
import
UserFactory
...
@@ -132,8 +132,9 @@ class SocialFacebookTestCase(ModuleStoreTestCase, APITestCase):
...
@@ -132,8 +132,9 @@ class SocialFacebookTestCase(ModuleStoreTestCase, APITestCase):
"""
"""
Sets self.user's share settings to boolean_value
Sets self.user's share settings to boolean_value
"""
"""
update_preferences
(
user
.
username
,
share_with_facebook_friends
=
boolean_value
)
# Note that setting the value to boolean will result in the conversion to the unicode form of the boolean.
self
.
assertEqual
(
preference_info
(
user
.
username
)[
'share_with_facebook_friends'
],
unicode
(
boolean_value
))
set_user_preference
(
user
,
'share_with_facebook_friends'
,
boolean_value
)
self
.
assertEqual
(
get_user_preference
(
user
,
'share_with_facebook_friends'
),
unicode
(
boolean_value
))
def
_change_enrollment
(
self
,
action
,
course_id
=
None
,
email_opt_in
=
None
):
def
_change_enrollment
(
self
,
action
,
course_id
=
None
,
email_opt_in
=
None
):
"""
"""
...
...
lms/djangoapps/mobile_api/social_facebook/utils.py
View file @
37dd0ec8
...
@@ -5,10 +5,12 @@ import json
...
@@ -5,10 +5,12 @@ import json
import
urllib2
import
urllib2
import
facebook
import
facebook
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.exceptions
import
ObjectDoesNotExist
from
rest_framework
import
status
from
rest_framework
import
status
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
social.apps.django_app.default.models
import
UserSocialAuth
from
social.apps.django_app.default.models
import
UserSocialAuth
from
openedx.core.djangoapps.user_api.api.profile
import
preference_info
from
openedx.core.djangoapps.user_api.models
import
UserPreference
from
student.models
import
User
# TODO
# TODO
...
@@ -64,5 +66,11 @@ def share_with_facebook_friends(friend):
...
@@ -64,5 +66,11 @@ def share_with_facebook_friends(friend):
"""
"""
Return true if the user's share_with_facebook_friends preference is set to true.
Return true if the user's share_with_facebook_friends preference is set to true.
"""
"""
share_fb_friends_settings
=
preference_info
(
friend
[
'edX_username'
])
return
share_fb_friends_settings
.
get
(
'share_with_facebook_friends'
,
None
)
==
'True'
# Calling UserPreference directly because the requesting user may be different (and not is_staff).
try
:
existing_user
=
User
.
objects
.
get
(
username
=
friend
[
'edX_username'
])
except
ObjectDoesNotExist
:
return
False
return
UserPreference
.
get_value
(
existing_user
,
'share_with_facebook_friends'
)
==
'True'
lms/djangoapps/notification_prefs/features/unsubscribe.py
View file @
37dd0ec8
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
lettuce
import
step
,
world
from
lettuce
import
step
,
world
from
notification_prefs
import
NOTIFICATION_PREF_KEY
from
notification_prefs
import
NOTIFICATION_PREF_KEY
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
set_user_preference
,
get_user_p
reference
USERNAME
=
"robot"
USERNAME
=
"robot"
...
@@ -11,7 +11,7 @@ UNSUB_TOKEN = "av9E-14sAP1bVBRCPbrTHQ=="
...
@@ -11,7 +11,7 @@ UNSUB_TOKEN = "av9E-14sAP1bVBRCPbrTHQ=="
@step
(
u"I have notifications enabled"
)
@step
(
u"I have notifications enabled"
)
def
enable_notifications
(
step_
):
def
enable_notifications
(
step_
):
user
=
User
.
objects
.
get
(
username
=
USERNAME
)
user
=
User
.
objects
.
get
(
username
=
USERNAME
)
UserPreference
.
objects
.
create
(
user
=
user
,
key
=
NOTIFICATION_PREF_KEY
,
value
=
UNSUB_TOKEN
)
set_user_preference
(
user
,
NOTIFICATION_PREF_KEY
,
UNSUB_TOKEN
)
@step
(
u"I access my unsubscribe url"
)
@step
(
u"I access my unsubscribe url"
)
...
@@ -22,4 +22,4 @@ def access_unsubscribe_url(step_):
...
@@ -22,4 +22,4 @@ def access_unsubscribe_url(step_):
@step
(
u"my notifications should be disabled"
)
@step
(
u"my notifications should be disabled"
)
def
notifications_should_be_disabled
(
step_
):
def
notifications_should_be_disabled
(
step_
):
user
=
User
.
objects
.
get
(
username
=
USERNAME
)
user
=
User
.
objects
.
get
(
username
=
USERNAME
)
assert
not
UserPreference
.
objects
.
filter
(
user
=
user
,
key
=
NOTIFICATION_PREF_KEY
)
.
exists
(
)
assert
not
get_user_preference
(
user
,
NOTIFICATION_PREF_KEY
)
lms/djangoapps/notification_prefs/views.py
View file @
37dd0ec8
...
@@ -13,6 +13,7 @@ from django.views.decorators.http import require_GET, require_POST
...
@@ -13,6 +13,7 @@ from django.views.decorators.http import require_GET, require_POST
from
edxmako.shortcuts
import
render_to_response
from
edxmako.shortcuts
import
render_to_response
from
notification_prefs
import
NOTIFICATION_PREF_KEY
from
notification_prefs
import
NOTIFICATION_PREF_KEY
from
openedx.core.djangoapps.user_api.models
import
UserPreference
from
openedx.core.djangoapps.user_api.models
import
UserPreference
from
openedx.core.djangoapps.user_api.preferences.api
import
delete_user_preference
class
UsernameDecryptionException
(
Exception
):
class
UsernameDecryptionException
(
Exception
):
...
@@ -95,6 +96,8 @@ def enable_notifications(user):
...
@@ -95,6 +96,8 @@ def enable_notifications(user):
Enable notifications for a user.
Enable notifications for a user.
Currently only used for daily forum digests.
Currently only used for daily forum digests.
"""
"""
# Calling UserPreference directly because this method is called from a couple of places,
# and it is not clear that user is always the user initiating the request.
UserPreference
.
objects
.
get_or_create
(
UserPreference
.
objects
.
get_or_create
(
user
=
user
,
user
=
user
,
key
=
NOTIFICATION_PREF_KEY
,
key
=
NOTIFICATION_PREF_KEY
,
...
@@ -104,17 +107,6 @@ def enable_notifications(user):
...
@@ -104,17 +107,6 @@ def enable_notifications(user):
)
)
def
disable_notifications
(
user
):
"""
Disable notifications for a user.
Currently only used for daily forum digests.
"""
UserPreference
.
objects
.
filter
(
user
=
user
,
key
=
NOTIFICATION_PREF_KEY
)
.
delete
()
@require_POST
@require_POST
def
ajax_enable
(
request
):
def
ajax_enable
(
request
):
"""
"""
...
@@ -123,7 +115,7 @@ def ajax_enable(request):
...
@@ -123,7 +115,7 @@ def ajax_enable(request):
This view should be invoked by an AJAX POST call. It returns status 204
This view should be invoked by an AJAX POST call. It returns status 204
(no content) or an error. If notifications were already enabled for this
(no content) or an error. If notifications were already enabled for this
user, this has no effect. Otherwise, a preference is created with the
user, this has no effect. Otherwise, a preference is created with the
unsubscribe token (an e
cnryption of the username) as the value.unsernam
unsubscribe token (an e
ncryption of the username) as the value.username
"""
"""
if
not
request
.
user
.
is_authenticated
():
if
not
request
.
user
.
is_authenticated
():
raise
PermissionDenied
raise
PermissionDenied
...
@@ -144,7 +136,7 @@ def ajax_disable(request):
...
@@ -144,7 +136,7 @@ def ajax_disable(request):
if
not
request
.
user
.
is_authenticated
():
if
not
request
.
user
.
is_authenticated
():
raise
PermissionDenied
raise
PermissionDenied
d
isable_notifications
(
request
.
user
)
d
elete_user_preference
(
request
.
user
,
NOTIFICATION_PREF_KEY
)
return
HttpResponse
(
status
=
204
)
return
HttpResponse
(
status
=
204
)
...
@@ -192,6 +184,8 @@ def set_subscription(request, token, subscribe): # pylint: disable=unused-argum
...
@@ -192,6 +184,8 @@ def set_subscription(request, token, subscribe): # pylint: disable=unused-argum
except
User
.
DoesNotExist
:
except
User
.
DoesNotExist
:
raise
Http404
(
"username"
)
raise
Http404
(
"username"
)
# Calling UserPreference directly because the fact that the user is passed in the token implies
# that it may not match request.user.
if
subscribe
:
if
subscribe
:
UserPreference
.
objects
.
get_or_create
(
user
=
user
,
UserPreference
.
objects
.
get_or_create
(
user
=
user
,
key
=
NOTIFICATION_PREF_KEY
,
key
=
NOTIFICATION_PREF_KEY
,
...
...
lms/djangoapps/oauth2_handler/handlers.py
View file @
37dd0ec8
...
@@ -66,10 +66,10 @@ class ProfileHandler(object):
...
@@ -66,10 +66,10 @@ class ProfileHandler(object):
"""
"""
Return the locale for the users based on their preferences.
Return the locale for the users based on their preferences.
Does not return a value if the users have not set their locale preferences.
Does not return a value if the users have not set their locale preferences.
"""
"""
language
=
UserPreference
.
get_preference
(
data
[
'user'
],
LANGUAGE_KEY
)
# Calling UserPreference directly because it is not clear which user made the request.
language
=
UserPreference
.
get_value
(
data
[
'user'
],
LANGUAGE_KEY
)
# If the user has no language specified, return the default one.
# If the user has no language specified, return the default one.
if
not
language
:
if
not
language
:
...
...
lms/djangoapps/oauth2_handler/tests.py
View file @
37dd0ec8
...
@@ -9,7 +9,7 @@ from student.models import anonymous_id_for_user
...
@@ -9,7 +9,7 @@ from student.models import anonymous_id_for_user
from
student.models
import
UserProfile
from
student.models
import
UserProfile
from
student.roles
import
CourseStaffRole
,
CourseInstructorRole
from
student.roles
import
CourseStaffRole
,
CourseInstructorRole
from
student.tests.factories
import
UserFactory
,
UserProfileFactory
from
student.tests.factories
import
UserFactory
,
UserProfileFactory
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
set_user_p
reference
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
# Will also run default tests for IDTokens and UserInfo
# Will also run default tests for IDTokens and UserInfo
...
@@ -68,7 +68,7 @@ class IDTokenTest(BaseTestMixin, IDTokenTestCase):
...
@@ -68,7 +68,7 @@ class IDTokenTest(BaseTestMixin, IDTokenTestCase):
def
test_user_with_locale_claim
(
self
):
def
test_user_with_locale_claim
(
self
):
language
=
'en'
language
=
'en'
UserPreference
.
set
_preference
(
self
.
user
,
LANGUAGE_KEY
,
language
)
set_user
_preference
(
self
.
user
,
LANGUAGE_KEY
,
language
)
scopes
,
claims
=
self
.
get_id_token_values
(
'openid profile'
)
scopes
,
claims
=
self
.
get_id_token_values
(
'openid profile'
)
self
.
assertIn
(
'profile'
,
scopes
)
self
.
assertIn
(
'profile'
,
scopes
)
...
...
lms/djangoapps/student_account/test/test_views.py
View file @
37dd0ec8
...
@@ -18,8 +18,8 @@ from django.test.utils import override_settings
...
@@ -18,8 +18,8 @@ from django.test.utils import override_settings
from
util.testing
import
UrlResetMixin
from
util.testing
import
UrlResetMixin
from
third_party_auth.tests.testutil
import
simulate_running_pipeline
from
third_party_auth.tests.testutil
import
simulate_running_pipeline
from
embargo.test_utils
import
restrict_course
from
embargo.test_utils
import
restrict_course
from
openedx.core.djangoapps.user_api.a
pi
import
account
as
account_api
from
openedx.core.djangoapps.user_api.a
ccounts.api
import
activate_account
,
create_account
from
openedx.core.djangoapps.user_api.a
pi
import
profile
as
profile_api
from
openedx.core.djangoapps.user_api.a
ccounts
import
EMAIL_MAX_LENGTH
from
xmodule.modulestore.tests.django_utils
import
(
from
xmodule.modulestore.tests.django_utils
import
(
ModuleStoreTestCase
,
mixed_store_config
ModuleStoreTestCase
,
mixed_store_config
)
)
...
@@ -53,7 +53,7 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
...
@@ -53,7 +53,7 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
# 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'
*
(
EMAIL_MAX_LENGTH
-
11
))
)
)
]
]
...
@@ -63,8 +63,8 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
...
@@ -63,8 +63,8 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
super
(
StudentAccountUpdateTest
,
self
)
.
setUp
(
"student_account.urls"
)
super
(
StudentAccountUpdateTest
,
self
)
.
setUp
(
"student_account.urls"
)
# Create/activate a new account
# Create/activate a new account
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
OLD_PASSWORD
,
self
.
OLD_EMAIL
)
activation_key
=
create_account
(
self
.
USERNAME
,
self
.
OLD_PASSWORD
,
self
.
OLD_EMAIL
)
ac
count_api
.
ac
tivate_account
(
activation_key
)
activate_account
(
activation_key
)
# Login
# Login
result
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
OLD_PASSWORD
)
result
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
OLD_PASSWORD
)
...
@@ -148,7 +148,7 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
...
@@ -148,7 +148,7 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
self
.
client
.
logout
()
self
.
client
.
logout
()
# Create a second user, but do not activate it
# Create a second user, but do not activate it
account_api
.
create_account
(
self
.
ALTERNATE_USERNAME
,
self
.
OLD_PASSWORD
,
self
.
NEW_EMAIL
)
create_account
(
self
.
ALTERNATE_USERNAME
,
self
.
OLD_PASSWORD
,
self
.
NEW_EMAIL
)
# Send the view the email address tied to the inactive user
# Send the view the email address tied to the inactive user
response
=
self
.
_change_password
(
email
=
self
.
NEW_EMAIL
)
response
=
self
.
_change_password
(
email
=
self
.
NEW_EMAIL
)
...
@@ -226,8 +226,8 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase)
...
@@ -226,8 +226,8 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase)
@ddt.data
(
"account_login"
,
"account_register"
)
@ddt.data
(
"account_login"
,
"account_register"
)
def
test_login_and_registration_form_already_authenticated
(
self
,
url_name
):
def
test_login_and_registration_form_already_authenticated
(
self
,
url_name
):
# Create/activate a new account and log in
# Create/activate a new account and log in
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
activation_key
=
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
ac
count_api
.
ac
tivate_account
(
activation_key
)
activate_account
(
activation_key
)
result
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
PASSWORD
)
result
=
self
.
client
.
login
(
username
=
self
.
USERNAME
,
password
=
self
.
PASSWORD
)
self
.
assertTrue
(
result
)
self
.
assertTrue
(
result
)
...
...
lms/djangoapps/student_account/views.py
View file @
37dd0ec8
...
@@ -11,15 +11,13 @@ from django.http import (
...
@@ -11,15 +11,13 @@ from django.http import (
from
django.shortcuts
import
redirect
from
django.shortcuts
import
redirect
from
django.http
import
HttpRequest
from
django.http
import
HttpRequest
from
django.core.urlresolvers
import
reverse
,
resolve
from
django.core.urlresolvers
import
reverse
,
resolve
from
django.core.mail
import
send_mail
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
from
django_future.csrf
import
ensure_csrf_cookie
from
django_future.csrf
import
ensure_csrf_cookie
from
django.contrib.auth.decorators
import
login_required
from
django.views.decorators.http
import
require_http_methods
from
django.views.decorators.http
import
require_http_methods
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
edxmako.shortcuts
import
render_to_response
,
render_to_string
from
edxmako.shortcuts
import
render_to_response
from
microsite_configuration
import
microsite
from
microsite_configuration
import
microsite
from
embargo
import
api
as
embargo_api
from
embargo
import
api
as
embargo_api
import
third_party_auth
import
third_party_auth
...
@@ -32,8 +30,8 @@ from student.views import (
...
@@ -32,8 +30,8 @@ from student.views import (
register_user
as
old_register_view
register_user
as
old_register_view
)
)
from
openedx.core.djangoapps.user_api.a
pi
import
account
as
account_api
from
openedx.core.djangoapps.user_api.a
ccounts.api
import
request_password_change
from
openedx.core.djangoapps.user_api.
api
import
profile
as
profile_api
from
openedx.core.djangoapps.user_api.
errors
import
UserNotFound
from
util.bad_request_rate_limiter
import
BadRequestRateLimiter
from
util.bad_request_rate_limiter
import
BadRequestRateLimiter
from
student_account.helpers
import
auth_pipeline_urls
from
student_account.helpers
import
auth_pipeline_urls
...
@@ -136,8 +134,8 @@ def password_change_request_handler(request):
...
@@ -136,8 +134,8 @@ def password_change_request_handler(request):
if
email
:
if
email
:
try
:
try
:
account_api
.
request_password_change
(
email
,
request
.
get_host
(),
request
.
is_secure
())
request_password_change
(
email
,
request
.
get_host
(),
request
.
is_secure
())
except
account_api
.
Account
UserNotFound
:
except
UserNotFound
:
AUDIT_LOG
.
info
(
"Invalid password reset attempt"
)
AUDIT_LOG
.
info
(
"Invalid password reset attempt"
)
# Increment the rate limit counter
# Increment the rate limit counter
limiter
.
tick_bad_request_counter
(
request
)
limiter
.
tick_bad_request_counter
(
request
)
...
...
lms/djangoapps/verify_student/views.py
View file @
37dd0ec8
...
@@ -29,7 +29,7 @@ from django.core.mail import send_mail
...
@@ -29,7 +29,7 @@ from django.core.mail import send_mail
from
openedx.core.djangoapps.user_api.accounts.api
import
get_account_settings
,
update_account_settings
from
openedx.core.djangoapps.user_api.accounts.api
import
get_account_settings
,
update_account_settings
from
openedx.core.djangoapps.user_api.accounts
import
NAME_MIN_LENGTH
from
openedx.core.djangoapps.user_api.accounts
import
NAME_MIN_LENGTH
from
openedx.core.djangoapps.user_api.
api.account
import
Account
UserNotFound
,
AccountValidationError
from
openedx.core.djangoapps.user_api.
errors
import
UserNotFound
,
AccountValidationError
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
...
@@ -734,7 +734,7 @@ def submit_photos_for_verification(request):
...
@@ -734,7 +734,7 @@ def submit_photos_for_verification(request):
if
request
.
POST
.
get
(
'full_name'
):
if
request
.
POST
.
get
(
'full_name'
):
try
:
try
:
update_account_settings
(
request
.
user
,
{
"name"
:
request
.
POST
.
get
(
'full_name'
)})
update_account_settings
(
request
.
user
,
{
"name"
:
request
.
POST
.
get
(
'full_name'
)})
except
Account
UserNotFound
:
except
UserNotFound
:
return
HttpResponseBadRequest
(
_
(
"No profile found for user"
))
return
HttpResponseBadRequest
(
_
(
"No profile found for user"
))
except
AccountValidationError
:
except
AccountValidationError
:
msg
=
_
(
msg
=
_
(
...
...
openedx/core/djangoapps/user_api/accounts/__init__.py
View file @
37dd0ec8
...
@@ -2,8 +2,21 @@
...
@@ -2,8 +2,21 @@
Account constants
Account constants
"""
"""
# The minimum a
cceptable length for the name
account field
# The minimum a
nd maximum length for the name ("full name")
account field
NAME_MIN_LENGTH
=
2
NAME_MIN_LENGTH
=
2
NAME_MAX_LENGTH
=
255
# The minimum and maximum length for the username account field
USERNAME_MIN_LENGTH
=
2
USERNAME_MAX_LENGTH
=
30
# The minimum and maximum length for the email account field
EMAIL_MIN_LENGTH
=
3
EMAIL_MAX_LENGTH
=
254
# The minimum and maximum length for the password account field
PASSWORD_MIN_LENGTH
=
2
PASSWORD_MAX_LENGTH
=
75
ACCOUNT_VISIBILITY_PREF_KEY
=
'account_privacy'
ACCOUNT_VISIBILITY_PREF_KEY
=
'account_privacy'
...
...
openedx/core/djangoapps/user_api/accounts/api.py
View file @
37dd0ec8
from
django.contrib.auth.models
import
User
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
from
django.db
import
transaction
,
IntegrityError
import
datetime
import
datetime
from
pytz
import
UTC
from
pytz
import
UTC
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.validators
import
validate_email
,
validate_slug
,
ValidationError
from
openedx.core.djangoapps.user_api.api.account
import
(
from
student.models
import
User
,
UserProfile
,
Registration
AccountUserNotFound
,
AccountUpdateError
,
AccountNotAuthorized
,
AccountValidationError
)
from
.serializers
import
AccountLegacyProfileSerializer
,
AccountUserSerializer
from
student.models
import
UserProfile
from
student.views
import
validate_new_email
,
do_email_change_request
from
student.views
import
validate_new_email
,
do_email_change_request
from
..errors
import
(
AccountUpdateError
,
AccountValidationError
,
AccountUsernameInvalid
,
AccountPasswordInvalid
,
AccountEmailInvalid
,
AccountUserAlreadyExists
,
UserAPIInternalError
,
UserAPIRequestError
,
UserNotFound
,
UserNotAuthorized
)
from
..forms
import
PasswordResetFormNoActive
from
..helpers
import
intercept_errors
from
..models
import
UserPreference
from
..models
import
UserPreference
from
.
import
ACCOUNT_VISIBILITY_PREF_KEY
,
ALL_USERS_VISIBILITY
from
.
import
(
ACCOUNT_VISIBILITY_PREF_KEY
,
ALL_USERS_VISIBILITY
,
EMAIL_MIN_LENGTH
,
EMAIL_MAX_LENGTH
,
PASSWORD_MIN_LENGTH
,
PASSWORD_MAX_LENGTH
,
USERNAME_MIN_LENGTH
,
USERNAME_MAX_LENGTH
)
from
.serializers
import
AccountLegacyProfileSerializer
,
AccountUserSerializer
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
def
get_account_settings
(
requesting_user
,
username
=
None
,
configuration
=
None
,
view
=
None
):
def
get_account_settings
(
requesting_user
,
username
=
None
,
configuration
=
None
,
view
=
None
):
"""Returns account information for a user serialized as JSON.
"""Returns account information for a user serialized as JSON.
...
@@ -39,8 +51,9 @@ def get_account_settings(requesting_user, username=None, configuration=None, vie
...
@@ -39,8 +51,9 @@ def get_account_settings(requesting_user, username=None, configuration=None, vie
A dict containing account fields.
A dict containing account fields.
Raises:
Raises:
Account
UserNotFound: no user with username `username` exists (or `requesting_user.username` if
UserNotFound: no user with username `username` exists (or `requesting_user.username` if
`username` is not specified)
`username` is not specified)
UserAPIInternalError: the operation failed due to an unexpected error.
"""
"""
if
username
is
None
:
if
username
is
None
:
username
=
requesting_user
.
username
username
=
requesting_user
.
username
...
@@ -63,7 +76,9 @@ def get_account_settings(requesting_user, username=None, configuration=None, vie
...
@@ -63,7 +76,9 @@ def get_account_settings(requesting_user, username=None, configuration=None, vie
visible_settings
=
{}
visible_settings
=
{}
profile_privacy
=
UserPreference
.
get_preference
(
existing_user
,
ACCOUNT_VISIBILITY_PREF_KEY
)
# Calling UserPreference directly because the requesting user may be different from existing_user
# (and does not have to be is_staff).
profile_privacy
=
UserPreference
.
get_value
(
existing_user
,
ACCOUNT_VISIBILITY_PREF_KEY
)
privacy_setting
=
profile_privacy
if
profile_privacy
else
configuration
.
get
(
'default_visibility'
)
privacy_setting
=
profile_privacy
if
profile_privacy
else
configuration
.
get
(
'default_visibility'
)
if
privacy_setting
==
ALL_USERS_VISIBILITY
:
if
privacy_setting
==
ALL_USERS_VISIBILITY
:
...
@@ -77,6 +92,8 @@ def get_account_settings(requesting_user, username=None, configuration=None, vie
...
@@ -77,6 +92,8 @@ def get_account_settings(requesting_user, username=None, configuration=None, vie
return
visible_settings
return
visible_settings
@transaction.commit_on_success
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
def
update_account_settings
(
requesting_user
,
update
,
username
=
None
):
def
update_account_settings
(
requesting_user
,
update
,
username
=
None
):
"""Update user account information.
"""Update user account information.
...
@@ -92,9 +109,9 @@ def update_account_settings(requesting_user, update, username=None):
...
@@ -92,9 +109,9 @@ def update_account_settings(requesting_user, update, username=None):
`requesting_user.username` is assumed.
`requesting_user.username` is assumed.
Raises:
Raises:
Account
UserNotFound: no user with username `username` exists (or `requesting_user.username` if
UserNotFound: no user with username `username` exists (or `requesting_user.username` if
`username` is not specified)
`username` is not specified)
Account
NotAuthorized: the requesting_user does not have access to change the account
User
NotAuthorized: the requesting_user does not have access to change the account
associated with `username`
associated with `username`
AccountValidationError: the update was not attempted because validation errors were found with
AccountValidationError: the update was not attempted because validation errors were found with
the supplied update
the supplied update
...
@@ -102,7 +119,7 @@ def update_account_settings(requesting_user, update, username=None):
...
@@ -102,7 +119,7 @@ def update_account_settings(requesting_user, update, username=None):
time, some parts of the update may have been successful, even if an AccountUpdateError is returned;
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,
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.
but then the e-mail change request, which is processed last, may throw an error.
UserAPIInternalError: the operation failed due to an unexpected error.
"""
"""
if
username
is
None
:
if
username
is
None
:
username
=
requesting_user
.
username
username
=
requesting_user
.
username
...
@@ -110,7 +127,7 @@ def update_account_settings(requesting_user, update, username=None):
...
@@ -110,7 +127,7 @@ def update_account_settings(requesting_user, update, username=None):
existing_user
,
existing_user_profile
=
_get_user_and_profile
(
username
)
existing_user
,
existing_user_profile
=
_get_user_and_profile
(
username
)
if
requesting_user
.
username
!=
username
:
if
requesting_user
.
username
!=
username
:
raise
Account
NotAuthorized
()
raise
User
NotAuthorized
()
# If user has requested to change email, we must call the multi-step process to handle this.
# 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).
# It is not handled by the serializer (which considers email to be read-only).
...
@@ -138,7 +155,7 @@ def update_account_settings(requesting_user, update, username=None):
...
@@ -138,7 +155,7 @@ def update_account_settings(requesting_user, update, username=None):
for
read_only_field
in
read_only_fields
:
for
read_only_field
in
read_only_fields
:
field_errors
[
read_only_field
]
=
{
field_errors
[
read_only_field
]
=
{
"developer_message"
:
"This field is not editable via this API"
,
"developer_message"
:
"This field is not editable via this API"
,
"user_message"
:
_
(
"Field '{field_name}' cannot be edited."
.
format
(
field_name
=
read_only_field
)
)
"user_message"
:
_
(
u"Field '{field_name}' cannot be edited."
)
.
format
(
field_name
=
read_only_field
)
}
}
del
update
[
read_only_field
]
del
update
[
read_only_field
]
...
@@ -154,7 +171,7 @@ def update_account_settings(requesting_user, update, username=None):
...
@@ -154,7 +171,7 @@ def update_account_settings(requesting_user, update, username=None):
validate_new_email
(
existing_user
,
new_email
)
validate_new_email
(
existing_user
,
new_email
)
except
ValueError
as
err
:
except
ValueError
as
err
:
field_errors
[
"email"
]
=
{
field_errors
[
"email"
]
=
{
"developer_message"
:
"Error thrown from validate_new_email: '{}'"
.
format
(
err
.
message
),
"developer_message"
:
u
"Error thrown from validate_new_email: '{}'"
.
format
(
err
.
message
),
"user_message"
:
err
.
message
"user_message"
:
err
.
message
}
}
...
@@ -175,7 +192,7 @@ def update_account_settings(requesting_user, update, username=None):
...
@@ -175,7 +192,7 @@ def update_account_settings(requesting_user, update, username=None):
meta
[
'old_names'
]
=
[]
meta
[
'old_names'
]
=
[]
meta
[
'old_names'
]
.
append
([
meta
[
'old_names'
]
.
append
([
old_name
,
old_name
,
"Name change requested through account API by {0}"
.
format
(
requesting_user
.
username
),
u
"Name change requested through account API by {0}"
.
format
(
requesting_user
.
username
),
datetime
.
datetime
.
now
(
UTC
)
.
isoformat
()
datetime
.
datetime
.
now
(
UTC
)
.
isoformat
()
])
])
existing_user_profile
.
set_meta
(
meta
)
existing_user_profile
.
set_meta
(
meta
)
...
@@ -183,7 +200,7 @@ def update_account_settings(requesting_user, update, username=None):
...
@@ -183,7 +200,7 @@ def update_account_settings(requesting_user, update, username=None):
except
Exception
as
err
:
except
Exception
as
err
:
raise
AccountUpdateError
(
raise
AccountUpdateError
(
"Error thrown when saving account updates: '{}'"
.
format
(
err
.
message
)
u
"Error thrown when saving account updates: '{}'"
.
format
(
err
.
message
)
)
)
# And try to send the email change request if necessary.
# And try to send the email change request if necessary.
...
@@ -192,7 +209,7 @@ def update_account_settings(requesting_user, update, username=None):
...
@@ -192,7 +209,7 @@ def update_account_settings(requesting_user, update, username=None):
do_email_change_request
(
existing_user
,
new_email
)
do_email_change_request
(
existing_user
,
new_email
)
except
ValueError
as
err
:
except
ValueError
as
err
:
raise
AccountUpdateError
(
raise
AccountUpdateError
(
"Error thrown from do_email_change_request: '{}'"
.
format
(
err
.
message
),
u
"Error thrown from do_email_change_request: '{}'"
.
format
(
err
.
message
),
user_message
=
err
.
message
user_message
=
err
.
message
)
)
...
@@ -205,7 +222,7 @@ def _get_user_and_profile(username):
...
@@ -205,7 +222,7 @@ def _get_user_and_profile(username):
existing_user
=
User
.
objects
.
get
(
username
=
username
)
existing_user
=
User
.
objects
.
get
(
username
=
username
)
existing_user_profile
=
UserProfile
.
objects
.
get
(
user
=
existing_user
)
existing_user_profile
=
UserProfile
.
objects
.
get
(
user
=
existing_user
)
except
ObjectDoesNotExist
:
except
ObjectDoesNotExist
:
raise
Account
UserNotFound
()
raise
UserNotFound
()
return
existing_user
,
existing_user_profile
return
existing_user
,
existing_user_profile
...
@@ -217,16 +234,286 @@ def _add_serializer_errors(update, serializer, field_errors):
...
@@ -217,16 +234,286 @@ def _add_serializer_errors(update, serializer, field_errors):
"""
"""
if
not
serializer
.
is_valid
():
if
not
serializer
.
is_valid
():
errors
=
serializer
.
errors
errors
=
serializer
.
errors
for
key
,
value
in
errors
.
iteritems
():
for
key
,
error
in
errors
.
iteritems
():
if
isinstance
(
value
,
list
)
and
len
(
value
)
>
0
:
field_value
=
update
[
key
]
developer_message
=
value
[
0
]
else
:
developer_message
=
"Invalid value: {field_value}'"
.
format
(
field_value
=
update
[
key
])
field_errors
[
key
]
=
{
field_errors
[
key
]
=
{
"developer_message"
:
developer_message
,
"developer_message"
:
u"Value '{field_value}' is not valid for field '{field_name}': {error}"
.
format
(
"user_message"
:
_
(
"Value '{field_value}' is not valid for field '{field_name}'."
.
format
(
field_value
=
field_value
,
field_name
=
key
,
error
=
error
field_value
=
update
[
key
],
field_name
=
key
)
),
)
"user_message"
:
_
(
u"Value '{field_value}' is not valid for field '{field_name}'."
)
.
format
(
field_value
=
field_value
,
field_name
=
key
),
}
}
return
field_errors
return
field_errors
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
@transaction.commit_on_success
def
create_account
(
username
,
password
,
email
):
"""Create a new user account.
This will implicitly create an empty profile for the user.
WARNING: This function does NOT yet implement all the features
in `student/views.py`. Until it does, please use this method
ONLY for tests of the account API, not in production code.
In particular, these are currently missing:
* 3rd party auth
* External auth (shibboleth)
* Complex password policies (ENFORCE_PASSWORD_POLICY)
In addition, we assume that some functionality is handled
at higher layers:
* Analytics events
* Activation email
* Terms of service / honor code checking
* Recording demographic info (use profile API)
* Auto-enrollment in courses (if invited via instructor dash)
Args:
username (unicode): The username for the new account.
password (unicode): The user's password.
email (unicode): The email address associated with the account.
Returns:
unicode: an activation key for the account.
Raises:
AccountUserAlreadyExists
AccountUsernameInvalid
AccountEmailInvalid
AccountPasswordInvalid
UserAPIInternalError: the operation failed due to an unexpected error.
"""
# Validate the username, password, and email
# This will raise an exception if any of these are not in a valid format.
_validate_username
(
username
)
_validate_password
(
password
,
username
)
_validate_email
(
email
)
# Create the user account, setting them to "inactive" until they activate their account.
user
=
User
(
username
=
username
,
email
=
email
,
is_active
=
False
)
user
.
set_password
(
password
)
try
:
user
.
save
()
except
IntegrityError
:
raise
AccountUserAlreadyExists
# Create a registration to track the activation process
# This implicitly saves the registration.
registration
=
Registration
()
registration
.
register
(
user
)
# Create an empty user profile with default values
UserProfile
(
user
=
user
)
.
save
()
# Return the activation key, which the caller should send to the user
return
registration
.
activation_key
def
check_account_exists
(
username
=
None
,
email
=
None
):
"""Check whether an account with a particular username or email already exists.
Keyword Arguments:
username (unicode)
email (unicode)
Returns:
list of conflicting fields
Example Usage:
>>> account_api.check_account_exists(username="bob")
[]
>>> account_api.check_account_exists(username="ted", email="ted@example.com")
["email", "username"]
"""
conflicts
=
[]
if
email
is
not
None
and
User
.
objects
.
filter
(
email
=
email
)
.
exists
():
conflicts
.
append
(
"email"
)
if
username
is
not
None
and
User
.
objects
.
filter
(
username
=
username
)
.
exists
():
conflicts
.
append
(
"username"
)
return
conflicts
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
def
activate_account
(
activation_key
):
"""Activate a user's account.
Args:
activation_key (unicode): The activation key the user received via email.
Returns:
None
Raises:
UserNotAuthorized
UserAPIInternalError: the operation failed due to an unexpected error.
"""
try
:
registration
=
Registration
.
objects
.
get
(
activation_key
=
activation_key
)
except
Registration
.
DoesNotExist
:
raise
UserNotAuthorized
else
:
# This implicitly saves the registration
registration
.
activate
()
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
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:
UserNotFound
AccountRequestError
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
})
# Validate that a 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 user with the provided email address exists.
raise
UserNotFound
def
_validate_username
(
username
):
"""Validate the username.
Arguments:
username (unicode): The proposed username.
Returns:
None
Raises:
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_slug
(
username
)
except
ValidationError
:
raise
AccountUsernameInvalid
(
u"Username '{username}' must contain only A-Z, a-z, 0-9, -, or _ characters"
)
def
_validate_password
(
password
,
username
):
"""Validate the format of the user's password.
Passwords cannot be the same as the username of the account,
so we take `username` as an argument.
Arguments:
password (unicode): The proposed password.
username (unicode): The username associated with the user's account.
Returns:
None
Raises:
AccountPasswordInvalid
"""
if
not
isinstance
(
password
,
basestring
):
raise
AccountPasswordInvalid
(
u"Password must be a string"
)
if
len
(
password
)
<
PASSWORD_MIN_LENGTH
:
raise
AccountPasswordInvalid
(
u"Password must be at least {min} characters long"
.
format
(
min
=
PASSWORD_MIN_LENGTH
)
)
if
len
(
password
)
>
PASSWORD_MAX_LENGTH
:
raise
AccountPasswordInvalid
(
u"Password must be at most {max} characters long"
.
format
(
max
=
PASSWORD_MAX_LENGTH
)
)
if
password
==
username
:
raise
AccountPasswordInvalid
(
u"Password cannot be the same as the username"
)
def
_validate_email
(
email
):
"""Validate the format of the email address.
Arguments:
email (unicode): The proposed email.
Returns:
None
Raises:
AccountEmailInvalid
"""
if
not
isinstance
(
email
,
basestring
):
raise
AccountEmailInvalid
(
u"Email must be a string"
)
if
len
(
email
)
<
EMAIL_MIN_LENGTH
:
raise
AccountEmailInvalid
(
u"Email '{email}' must be at least {min} characters long"
.
format
(
email
=
email
,
min
=
EMAIL_MIN_LENGTH
)
)
if
len
(
email
)
>
EMAIL_MAX_LENGTH
:
raise
AccountEmailInvalid
(
u"Email '{email}' must be at most {max} characters long"
.
format
(
email
=
email
,
max
=
EMAIL_MAX_LENGTH
)
)
try
:
validate_email
(
email
)
except
ValidationError
:
raise
AccountEmailInvalid
(
u"Email '{email}' format is not valid"
.
format
(
email
=
email
)
)
openedx/core/djangoapps/user_api/accounts/serializers.py
View file @
37dd0ec8
...
@@ -10,8 +10,8 @@ class AccountUserSerializer(serializers.HyperlinkedModelSerializer):
...
@@ -10,8 +10,8 @@ class AccountUserSerializer(serializers.HyperlinkedModelSerializer):
"""
"""
class
Meta
:
class
Meta
:
model
=
User
model
=
User
fields
=
(
"username"
,
"email"
,
"date_joined"
)
fields
=
(
"username"
,
"email"
,
"date_joined"
,
"is_active"
)
read_only_fields
=
(
"username"
,
"email"
,
"date_joined"
)
read_only_fields
=
(
"username"
,
"email"
,
"date_joined"
,
"is_active"
)
class
AccountLegacyProfileSerializer
(
serializers
.
HyperlinkedModelSerializer
):
class
AccountLegacyProfileSerializer
(
serializers
.
HyperlinkedModelSerializer
):
...
...
openedx/core/djangoapps/user_api/accounts/tests/test_api.py
View file @
37dd0ec8
...
@@ -3,18 +3,27 @@
...
@@ -3,18 +3,27 @@
Unit tests for behavior that is specific to the api methods (vs. the view methods).
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.
Most of the functionality is covered in test_views.py.
"""
"""
import
re
import
ddt
from
dateutil.parser
import
parse
as
parse_datetime
from
mock
import
Mock
,
patch
from
mock
import
Mock
,
patch
from
django.test
import
TestCase
from
django.test
import
TestCase
from
nose.tools
import
raises
import
unittest
import
unittest
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
django.core
import
mail
from
student.models
import
PendingEmailChange
from
student.models
import
PendingEmailChange
from
openedx.core.djangoapps.user_api.api.account
import
(
from
...errors
import
(
AccountUserNotFound
,
AccountUpdateError
,
AccountNotAuthorized
,
AccountValidationError
UserNotFound
,
UserNotAuthorized
,
AccountUpdateError
,
AccountValidationError
,
AccountUserAlreadyExists
,
AccountUsernameInvalid
,
AccountEmailInvalid
,
AccountPasswordInvalid
,
AccountRequestError
)
)
from
..api
import
get_account_settings
,
update_account_settings
from
..api
import
(
from
..serializers
import
AccountUserSerializer
get_account_settings
,
update_account_settings
,
create_account
,
activate_account
,
request_password_change
)
from
..
import
USERNAME_MAX_LENGTH
,
EMAIL_MAX_LENGTH
,
PASSWORD_MAX_LENGTH
def
mock_render_to_string
(
template_name
,
context
):
def
mock_render_to_string
(
template_name
,
context
):
...
@@ -70,12 +79,12 @@ class TestAccountApi(TestCase):
...
@@ -70,12 +79,12 @@ class TestAccountApi(TestCase):
self
.
assertEqual
(
self
.
different_user
.
email
,
account_settings
[
"email"
])
self
.
assertEqual
(
self
.
different_user
.
email
,
account_settings
[
"email"
])
def
test_get_user_not_found
(
self
):
def
test_get_user_not_found
(
self
):
"""Test that
Account
UserNotFound is thrown if there is no user with username."""
"""Test that UserNotFound is thrown if there is no user with username."""
with
self
.
assertRaises
(
Account
UserNotFound
):
with
self
.
assertRaises
(
UserNotFound
):
get_account_settings
(
self
.
user
,
username
=
"does_not_exist"
)
get_account_settings
(
self
.
user
,
username
=
"does_not_exist"
)
self
.
user
.
username
=
"does_not_exist"
self
.
user
.
username
=
"does_not_exist"
with
self
.
assertRaises
(
Account
UserNotFound
):
with
self
.
assertRaises
(
UserNotFound
):
get_account_settings
(
self
.
user
)
get_account_settings
(
self
.
user
)
def
test_update_username_provided
(
self
):
def
test_update_username_provided
(
self
):
...
@@ -88,16 +97,16 @@ class TestAccountApi(TestCase):
...
@@ -88,16 +97,16 @@ class TestAccountApi(TestCase):
account_settings
=
get_account_settings
(
self
.
user
)
account_settings
=
get_account_settings
(
self
.
user
)
self
.
assertEqual
(
"Donald Duck"
,
account_settings
[
"name"
])
self
.
assertEqual
(
"Donald Duck"
,
account_settings
[
"name"
])
with
self
.
assertRaises
(
Account
NotAuthorized
):
with
self
.
assertRaises
(
User
NotAuthorized
):
update_account_settings
(
self
.
different_user
,
{
"name"
:
"Pluto"
},
username
=
self
.
user
.
username
)
update_account_settings
(
self
.
different_user
,
{
"name"
:
"Pluto"
},
username
=
self
.
user
.
username
)
def
test_update_user_not_found
(
self
):
def
test_update_user_not_found
(
self
):
"""Test that
Account
UserNotFound is thrown if there is no user with username."""
"""Test that UserNotFound is thrown if there is no user with username."""
with
self
.
assertRaises
(
Account
UserNotFound
):
with
self
.
assertRaises
(
UserNotFound
):
update_account_settings
(
self
.
user
,
{},
username
=
"does_not_exist"
)
update_account_settings
(
self
.
user
,
{},
username
=
"does_not_exist"
)
self
.
user
.
username
=
"does_not_exist"
self
.
user
.
username
=
"does_not_exist"
with
self
.
assertRaises
(
Account
UserNotFound
):
with
self
.
assertRaises
(
UserNotFound
):
update_account_settings
(
self
.
user
,
{})
update_account_settings
(
self
.
user
,
{})
def
test_update_error_validating
(
self
):
def
test_update_error_validating
(
self
):
...
@@ -117,19 +126,14 @@ class TestAccountApi(TestCase):
...
@@ -117,19 +126,14 @@ class TestAccountApi(TestCase):
"email"
:
"not an email address"
"email"
:
"not an email address"
}
}
error_thrown
=
False
with
self
.
assertRaises
(
AccountValidationError
)
as
context_manager
:
try
:
update_account_settings
(
self
.
user
,
naughty_update
)
update_account_settings
(
self
.
user
,
naughty_update
)
except
AccountValidationError
as
response
:
field_errors
=
context_manager
.
exception
.
field_errors
error_thrown
=
True
field_errors
=
response
.
field_errors
self
.
assertEqual
(
3
,
len
(
field_errors
))
self
.
assertEqual
(
3
,
len
(
field_errors
))
self
.
assertEqual
(
"This field is not editable via this API"
,
field_errors
[
"username"
][
"developer_message"
])
self
.
assertEqual
(
"This field is not editable via this API"
,
field_errors
[
"username"
][
"developer_message"
])
self
.
assertIn
(
"Select a valid choice"
,
field_errors
[
"gender"
][
"developer_message"
])
self
.
assertIn
(
"Select a valid choice"
,
field_errors
[
"gender"
][
"developer_message"
])
self
.
assertIn
(
"Valid e-mail address required."
,
field_errors
[
"email"
][
"developer_message"
])
self
.
assertIn
(
"Valid e-mail address required."
,
field_errors
[
"email"
][
"developer_message"
])
self
.
assertTrue
(
error_thrown
,
"No AccountValidationError was thrown"
)
@patch
(
'django.core.mail.send_mail'
)
@patch
(
'django.core.mail.send_mail'
)
@patch
(
'student.views.render_to_string'
,
Mock
(
side_effect
=
mock_render_to_string
,
autospec
=
True
))
@patch
(
'student.views.render_to_string'
,
Mock
(
side_effect
=
mock_render_to_string
,
autospec
=
True
))
def
test_update_sending_email_fails
(
self
,
send_mail
):
def
test_update_sending_email_fails
(
self
,
send_mail
):
...
@@ -139,14 +143,9 @@ class TestAccountApi(TestCase):
...
@@ -139,14 +143,9 @@ class TestAccountApi(TestCase):
"name"
:
"Mickey Mouse"
,
"name"
:
"Mickey Mouse"
,
"email"
:
"seems_ok@sample.com"
"email"
:
"seems_ok@sample.com"
}
}
error_thrown
=
False
with
self
.
assertRaises
(
AccountUpdateError
)
as
context_manager
:
try
:
update_account_settings
(
self
.
user
,
less_naughty_update
)
update_account_settings
(
self
.
user
,
less_naughty_update
)
except
AccountUpdateError
as
response
:
self
.
assertIn
(
"Error thrown from do_email_change_request"
,
context_manager
.
exception
.
developer_message
)
error_thrown
=
True
self
.
assertIn
(
"Error thrown from do_email_change_request"
,
response
.
developer_message
)
self
.
assertTrue
(
error_thrown
,
"No AccountUpdateError was thrown"
)
# Verify that the name change happened, even though the attempt to send the email failed.
# Verify that the name change happened, even though the attempt to send the email failed.
account_settings
=
get_account_settings
(
self
.
user
)
account_settings
=
get_account_settings
(
self
.
user
)
...
@@ -163,15 +162,193 @@ class TestAccountApi(TestCase):
...
@@ -163,15 +162,193 @@ class TestAccountApi(TestCase):
"name"
:
"Mickey Mouse"
,
"name"
:
"Mickey Mouse"
,
"email"
:
"ok@sample.com"
"email"
:
"ok@sample.com"
}
}
error_thrown
=
False
try
:
update_account_settings
(
self
.
user
,
update_will_fail
)
except
AccountUpdateError
as
response
:
error_thrown
=
True
self
.
assertIn
(
"Error thrown when saving account updates"
,
response
.
developer_message
)
self
.
assertTrue
(
error_thrown
,
"No AccountUpdateError was thrown"
)
with
self
.
assertRaises
(
AccountUpdateError
)
as
context_manager
:
update_account_settings
(
self
.
user
,
update_will_fail
)
self
.
assertIn
(
"Error thrown when saving account updates"
,
context_manager
.
exception
.
developer_message
)
# Verify that no email change request was initiated.
# Verify that no email change request was initiated.
pending_change
=
PendingEmailChange
.
objects
.
filter
(
user
=
self
.
user
)
pending_change
=
PendingEmailChange
.
objects
.
filter
(
user
=
self
.
user
)
self
.
assertEqual
(
0
,
len
(
pending_change
))
self
.
assertEqual
(
0
,
len
(
pending_change
))
class
AccountSettingsOnCreationTest
(
TestCase
):
USERNAME
=
u'frank-underwood'
PASSWORD
=
u'ṕáśśẃőŕd'
EMAIL
=
u'frank+underwood@example.com'
def
test_create_account
(
self
):
# Create a new account, which should have empty account settings by default.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
# Retrieve the account settings
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
account_settings
=
get_account_settings
(
user
)
# Expect a date joined field but remove it to simplify the following comparison
self
.
assertIsNotNone
(
account_settings
[
'date_joined'
])
del
account_settings
[
'date_joined'
]
# Expect all the values to be defaulted
self
.
assertEqual
(
account_settings
,
{
'username'
:
self
.
USERNAME
,
'email'
:
self
.
EMAIL
,
'name'
:
u''
,
'gender'
:
None
,
'language'
:
u''
,
'goals'
:
None
,
'is_active'
:
False
,
'level_of_education'
:
None
,
'mailing_address'
:
None
,
'year_of_birth'
:
None
,
'country'
:
None
,
})
@ddt.ddt
class
AccountCreationActivationAndPasswordChangeTest
(
TestCase
):
USERNAME
=
u'frank-underwood'
PASSWORD
=
u'ṕáśśẃőŕd'
EMAIL
=
u'frank+underwood@example.com'
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'
,
u'frank@éxáḿṕĺé.ćőḿ'
,
# 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
)
]
def
test_activate_account
(
self
):
# Create the account, which is initially inactive
activation_key
=
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
account
=
get_account_settings
(
user
)
self
.
assertEqual
(
self
.
USERNAME
,
account
[
"username"
])
self
.
assertEqual
(
self
.
EMAIL
,
account
[
"email"
])
self
.
assertFalse
(
account
[
"is_active"
])
# Activate the account and verify that it is now active
activate_account
(
activation_key
)
account
=
get_account_settings
(
user
)
self
.
assertTrue
(
account
[
'is_active'
])
def
test_create_account_duplicate_username
(
self
):
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
with
self
.
assertRaises
(
AccountUserAlreadyExists
):
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
'different+email@example.com'
)
# Email uniqueness constraints were introduced in a database migration,
# which we disable in the unit tests to improve the speed of the test suite.
@unittest.skipUnless
(
settings
.
SOUTH_TESTS_MIGRATE
,
"South migrations required"
)
def
test_create_account_duplicate_email
(
self
):
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
with
self
.
assertRaises
(
AccountUserAlreadyExists
):
create_account
(
'different_user'
,
self
.
PASSWORD
,
self
.
EMAIL
)
def
test_username_too_long
(
self
):
long_username
=
'e'
*
(
USERNAME_MAX_LENGTH
+
1
)
with
self
.
assertRaises
(
AccountUsernameInvalid
):
create_account
(
long_username
,
self
.
PASSWORD
,
self
.
EMAIL
)
@raises
(
AccountEmailInvalid
)
@ddt.data
(
*
INVALID_EMAILS
)
def
test_create_account_invalid_email
(
self
,
invalid_email
):
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
invalid_email
)
@raises
(
AccountPasswordInvalid
)
@ddt.data
(
*
INVALID_PASSWORDS
)
def
test_create_account_invalid_password
(
self
,
invalid_password
):
create_account
(
self
.
USERNAME
,
invalid_password
,
self
.
EMAIL
)
@raises
(
AccountPasswordInvalid
)
def
test_create_account_username_password_equal
(
self
):
# Username and password cannot be the same
create_account
(
self
.
USERNAME
,
self
.
USERNAME
,
self
.
EMAIL
)
@raises
(
AccountRequestError
)
@ddt.data
(
*
INVALID_USERNAMES
)
def
test_create_account_invalid_username
(
self
,
invalid_username
):
create_account
(
invalid_username
,
self
.
PASSWORD
,
self
.
EMAIL
)
@raises
(
UserNotAuthorized
)
def
test_activate_account_invalid_key
(
self
):
activate_account
(
u'invalid'
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in LMS'
)
def
test_request_password_change
(
self
):
# Create and activate an account
activation_key
=
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
activate_account
(
activation_key
)
# Request a password change
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
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in LMS'
)
def
test_request_password_change_invalid_user
(
self
):
with
self
.
assertRaises
(
UserNotFound
):
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
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in LMS'
)
def
test_request_password_change_inactive_user
(
self
):
# Create an account, but do not activate it
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
request_password_change
(
self
.
EMAIL
,
self
.
ORIG_HOST
,
self
.
IS_SECURE
)
# Verify that the activation email was still sent
self
.
assertEqual
(
len
(
mail
.
outbox
),
1
)
def
_assert_is_datetime
(
self
,
timestamp
):
if
not
timestamp
:
return
False
try
:
parse_datetime
(
timestamp
)
except
ValueError
:
return
False
else
:
return
True
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
View file @
37dd0ec8
...
@@ -11,31 +11,31 @@ from rest_framework.test import APITestCase, APIClient
...
@@ -11,31 +11,31 @@ from rest_framework.test import APITestCase, APIClient
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
student.models
import
UserProfile
,
PendingEmailChange
from
student.models
import
UserProfile
,
PendingEmailChange
from
openedx.core.djangoapps.user_api.accounts
import
ACCOUNT_VISIBILITY_PREF_KEY
from
openedx.core.djangoapps.user_api.accounts
import
ACCOUNT_VISIBILITY_PREF_KEY
from
openedx.core.djangoapps.user_api.
models
import
UserP
reference
from
openedx.core.djangoapps.user_api.
preferences.api
import
set_user_p
reference
from
..
import
PRIVATE_VISIBILITY
,
ALL_USERS_VISIBILITY
from
..
import
PRIVATE_VISIBILITY
,
ALL_USERS_VISIBILITY
TEST_PASSWORD
=
"test"
class
UserAPITestCase
(
APITestCase
):
class
UserAPITestCase
(
APITestCase
):
"""
"""
The base class for all tests of the User API
The base class for all tests of the User API
"""
"""
test_password
=
"test"
def
setUp
(
self
):
def
setUp
(
self
):
super
(
UserAPITestCase
,
self
)
.
setUp
()
super
(
UserAPITestCase
,
self
)
.
setUp
()
self
.
anonymous_client
=
APIClient
()
self
.
anonymous_client
=
APIClient
()
self
.
different_user
=
UserFactory
.
create
(
password
=
TEST_PASSWORD
)
self
.
different_user
=
UserFactory
.
create
(
password
=
self
.
test_password
)
self
.
different_client
=
APIClient
()
self
.
different_client
=
APIClient
()
self
.
staff_user
=
UserFactory
(
is_staff
=
True
,
password
=
TEST_PASSWORD
)
self
.
staff_user
=
UserFactory
(
is_staff
=
True
,
password
=
self
.
test_password
)
self
.
staff_client
=
APIClient
()
self
.
staff_client
=
APIClient
()
self
.
user
=
UserFactory
.
create
(
password
=
TEST_PASSWORD
)
self
.
user
=
UserFactory
.
create
(
password
=
self
.
test_password
)
# will be assigned to self.client by default
def
login_client
(
self
,
api_client
,
user
):
def
login_client
(
self
,
api_client
,
user
):
"""Helper method for getting the client and user and logging in. Returns client. """
"""Helper method for getting the client and user and logging in. Returns client. """
client
=
getattr
(
self
,
api_client
)
client
=
getattr
(
self
,
api_client
)
user
=
getattr
(
self
,
user
)
user
=
getattr
(
self
,
user
)
client
.
login
(
username
=
user
.
username
,
password
=
TEST_PASSWORD
)
client
.
login
(
username
=
user
.
username
,
password
=
self
.
test_password
)
return
client
return
client
def
send_patch
(
self
,
client
,
json_data
,
content_type
=
"application/merge-patch+json"
,
expected_status
=
204
):
def
send_patch
(
self
,
client
,
json_data
,
content_type
=
"application/merge-patch+json"
,
expected_status
=
204
):
...
@@ -57,6 +57,22 @@ class UserAPITestCase(APITestCase):
...
@@ -57,6 +57,22 @@ class UserAPITestCase(APITestCase):
self
.
assertEqual
(
expected_status
,
response
.
status_code
)
self
.
assertEqual
(
expected_status
,
response
.
status_code
)
return
response
return
response
def
send_put
(
self
,
client
,
json_data
,
content_type
=
"application/json"
,
expected_status
=
204
):
"""
Helper method for sending a PUT to the server. Verifies the expected status and returns the response.
"""
response
=
client
.
put
(
self
.
url
,
data
=
json
.
dumps
(
json_data
),
content_type
=
content_type
)
self
.
assertEqual
(
expected_status
,
response
.
status_code
)
return
response
def
send_delete
(
self
,
client
,
expected_status
=
204
):
"""
Helper method for sending a DELETE to the server. Verifies the expected status and returns the response.
"""
response
=
client
.
delete
(
self
.
url
)
self
.
assertEqual
(
expected_status
,
response
.
status_code
)
return
response
def
create_mock_profile
(
self
,
user
):
def
create_mock_profile
(
self
,
user
):
"""
"""
Helper method that creates a mock profile for the specified user
Helper method that creates a mock profile for the specified user
...
@@ -109,7 +125,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -109,7 +125,7 @@ class TestAccountAPI(UserAPITestCase):
Verify that all account fields are returned (even those that are not shareable).
Verify that all account fields are returned (even those that are not shareable).
"""
"""
data
=
response
.
data
data
=
response
.
data
self
.
assertEqual
(
1
1
,
len
(
data
))
self
.
assertEqual
(
1
2
,
len
(
data
))
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertEqual
(
self
.
user
.
first_name
+
" "
+
self
.
user
.
last_name
,
data
[
"name"
])
self
.
assertEqual
(
self
.
user
.
first_name
+
" "
+
self
.
user
.
last_name
,
data
[
"name"
])
self
.
assertEqual
(
"US"
,
data
[
"country"
])
self
.
assertEqual
(
"US"
,
data
[
"country"
])
...
@@ -120,6 +136,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -120,6 +136,7 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertEqual
(
"world peace"
,
data
[
"goals"
])
self
.
assertEqual
(
"world peace"
,
data
[
"goals"
])
self
.
assertEqual
(
"Park Ave"
,
data
[
'mailing_address'
])
self
.
assertEqual
(
"Park Ave"
,
data
[
'mailing_address'
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertTrue
(
data
[
"is_active"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
def
test_anonymous_access
(
self
):
def
test_anonymous_access
(
self
):
...
@@ -133,7 +150,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -133,7 +150,7 @@ class TestAccountAPI(UserAPITestCase):
"""
"""
Test that DELETE, POST, and PUT are not supported.
Test that DELETE, POST, and PUT are not supported.
"""
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
assertEqual
(
405
,
self
.
client
.
put
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
put
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
post
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
post
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
delete
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
delete
(
self
.
url
)
.
status_code
)
...
@@ -160,7 +177,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -160,7 +177,7 @@ class TestAccountAPI(UserAPITestCase):
Test that a client (logged in) can only get the shareable fields for a different user.
Test that a client (logged in) can only get the shareable fields for a different user.
This is the case when default_visibility is set to "all_users".
This is the case when default_visibility is set to "all_users".
"""
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
TEST_PASSWORD
)
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
self
.
test_password
)
self
.
create_mock_profile
(
self
.
user
)
self
.
create_mock_profile
(
self
.
user
)
response
=
self
.
send_get
(
self
.
different_client
)
response
=
self
.
send_get
(
self
.
different_client
)
self
.
_verify_full_shareable_account_response
(
response
)
self
.
_verify_full_shareable_account_response
(
response
)
...
@@ -174,7 +191,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -174,7 +191,7 @@ class TestAccountAPI(UserAPITestCase):
Test that a client (logged in) can only get the shareable fields for a different user.
Test that a client (logged in) can only get the shareable fields for a different user.
This is the case when default_visibility is set to "private".
This is the case when default_visibility is set to "private".
"""
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
TEST_PASSWORD
)
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
self
.
test_password
)
self
.
create_mock_profile
(
self
.
user
)
self
.
create_mock_profile
(
self
.
user
)
response
=
self
.
send_get
(
self
.
different_client
)
response
=
self
.
send_get
(
self
.
different_client
)
self
.
_verify_private_account_response
(
response
)
self
.
_verify_private_account_response
(
response
)
...
@@ -201,7 +218,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -201,7 +218,7 @@ class TestAccountAPI(UserAPITestCase):
client
=
self
.
login_client
(
api_client
,
requesting_username
)
client
=
self
.
login_client
(
api_client
,
requesting_username
)
# Update user account visibility setting.
# Update user account visibility setting.
UserPreference
.
set
_preference
(
self
.
user
,
ACCOUNT_VISIBILITY_PREF_KEY
,
preference_visibility
)
set_user
_preference
(
self
.
user
,
ACCOUNT_VISIBILITY_PREF_KEY
,
preference_visibility
)
self
.
create_mock_profile
(
self
.
user
)
self
.
create_mock_profile
(
self
.
user
)
response
=
self
.
send_get
(
client
)
response
=
self
.
send_get
(
client
)
...
@@ -219,10 +236,10 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -219,10 +236,10 @@ class TestAccountAPI(UserAPITestCase):
Test that a client (logged in) can get her own account information (using default legacy profile information,
Test that a client (logged in) can get her own account information (using default legacy profile information,
as created by the test UserFactory).
as created by the test UserFactory).
"""
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
def
verify_get_own_information
():
response
=
self
.
send_get
(
self
.
client
)
response
=
self
.
send_get
(
self
.
client
)
data
=
response
.
data
data
=
response
.
data
self
.
assertEqual
(
11
,
len
(
data
))
self
.
assertEqual
(
12
,
len
(
data
))
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertEqual
(
self
.
user
.
first_name
+
" "
+
self
.
user
.
last_name
,
data
[
"name"
])
self
.
assertEqual
(
self
.
user
.
first_name
+
" "
+
self
.
user
.
last_name
,
data
[
"name"
])
for
empty_field
in
(
"year_of_birth"
,
"level_of_education"
,
"mailing_address"
):
for
empty_field
in
(
"year_of_birth"
,
"level_of_education"
,
"mailing_address"
):
...
@@ -231,9 +248,18 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -231,9 +248,18 @@ class TestAccountAPI(UserAPITestCase):
# TODO: what should the format of this be?
# TODO: what should the format of this be?
self
.
assertEqual
(
""
,
data
[
"language"
])
self
.
assertEqual
(
""
,
data
[
"language"
])
self
.
assertEqual
(
"m"
,
data
[
"gender"
])
self
.
assertEqual
(
"m"
,
data
[
"gender"
])
self
.
assertEqual
(
"World domination
"
,
data
[
"goals"
])
self
.
assertEqual
(
"Learn a lot
"
,
data
[
"goals"
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
self
.
assertEqual
(
self
.
user
.
is_active
,
data
[
"is_active"
])
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
verify_get_own_information
()
# Now make sure that the user can get the same information, even if not active
self
.
user
.
is_active
=
False
self
.
user
.
save
()
verify_get_own_information
()
def
test_get_account_empty_string
(
self
):
def
test_get_account_empty_string
(
self
):
"""
"""
...
@@ -245,7 +271,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -245,7 +271,7 @@ class TestAccountAPI(UserAPITestCase):
legacy_profile
.
gender
=
""
legacy_profile
.
gender
=
""
legacy_profile
.
save
()
legacy_profile
.
save
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
response
=
self
.
send_get
(
self
.
client
)
response
=
self
.
send_get
(
self
.
client
)
for
empty_field
in
(
"level_of_education"
,
"gender"
,
"country"
):
for
empty_field
in
(
"level_of_education"
,
"gender"
,
"country"
):
self
.
assertIsNone
(
response
.
data
[
empty_field
])
self
.
assertIsNone
(
response
.
data
[
empty_field
])
...
@@ -280,12 +306,12 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -280,12 +306,12 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertEqual
(
404
,
response
.
status_code
)
self
.
assertEqual
(
404
,
response
.
status_code
)
@ddt.data
(
@ddt.data
(
(
"gender"
,
"f"
,
"not a gender"
,
"Select a valid choice. not a gender is not one of the available choices."
),
(
"gender"
,
"f"
,
"not a gender"
,
u
"Select a valid choice. not a gender is not one of the available choices."
),
(
"level_of_education"
,
"none"
,
"x"
,
"Select a valid choice. x
is not one of the available choices."
),
(
"level_of_education"
,
"none"
,
u"ȻħȺɍłɇs"
,
u"Select a valid choice. ȻħȺɍłɇs
is not one of the available choices."
),
(
"country"
,
"GB"
,
"XY"
,
"Select a valid choice. XY is not one of the available choices."
),
(
"country"
,
"GB"
,
"XY"
,
u
"Select a valid choice. XY is not one of the available choices."
),
(
"year_of_birth"
,
2009
,
"not_an_int"
,
"Enter a whole number."
),
(
"year_of_birth"
,
2009
,
"not_an_int"
,
u
"Enter a whole number."
),
(
"name"
,
"bob"
,
"z"
*
256
,
"Ensure this value has at most 255 characters (it has 256)."
),
(
"name"
,
"bob"
,
"z"
*
256
,
u
"Ensure this value has at most 255 characters (it has 256)."
),
(
"name"
,
u"ȻħȺɍłɇs"
,
"z "
,
"The name field must be at least 2 characters long."
),
(
"name"
,
u"ȻħȺɍłɇs"
,
"z "
,
u
"The name field must be at least 2 characters long."
),
(
"language"
,
"Creole"
),
(
"language"
,
"Creole"
),
(
"goals"
,
"Smell the roses"
),
(
"goals"
,
"Smell the roses"
),
(
"mailing_address"
,
"Sesame Street"
),
(
"mailing_address"
,
"Sesame Street"
),
...
@@ -305,11 +331,13 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -305,11 +331,13 @@ class TestAccountAPI(UserAPITestCase):
if
fails_validation_value
:
if
fails_validation_value
:
error_response
=
self
.
send_patch
(
client
,
{
field
:
fails_validation_value
},
expected_status
=
400
)
error_response
=
self
.
send_patch
(
client
,
{
field
:
fails_validation_value
},
expected_status
=
400
)
self
.
assertEqual
(
self
.
assertEqual
(
"Value '{0}' is not valid for field '{1}'."
.
format
(
fails_validation_value
,
field
),
u
"Value '{0}' is not valid for field '{1}'."
.
format
(
fails_validation_value
,
field
),
error_response
.
data
[
"field_errors"
][
field
][
"user_message"
]
error_response
.
data
[
"field_errors"
][
field
][
"user_message"
]
)
)
self
.
assertEqual
(
self
.
assertEqual
(
developer_validation_message
,
u"Value '{value}' is not valid for field '{field}': {messages}"
.
format
(
value
=
fails_validation_value
,
field
=
field
,
messages
=
[
developer_validation_message
]
),
error_response
.
data
[
"field_errors"
][
field
][
"developer_message"
]
error_response
.
data
[
"field_errors"
][
field
][
"developer_message"
]
)
)
else
:
else
:
...
@@ -319,6 +347,15 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -319,6 +347,15 @@ class TestAccountAPI(UserAPITestCase):
get_response
=
self
.
send_get
(
client
)
get_response
=
self
.
send_get
(
client
)
self
.
assertEqual
(
""
,
get_response
.
data
[
field
])
self
.
assertEqual
(
""
,
get_response
.
data
[
field
])
def
test_patch_inactive_user
(
self
):
""" Verify that a user can patch her own account, even if inactive. """
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
user
.
is_active
=
False
self
.
user
.
save
()
self
.
send_patch
(
self
.
client
,
{
"goals"
:
"to not activate account"
})
get_response
=
self
.
send_get
(
self
.
client
)
self
.
assertEqual
(
"to not activate account"
,
get_response
.
data
[
"goals"
])
@ddt.unpack
@ddt.unpack
def
test_patch_account_noneditable
(
self
):
def
test_patch_account_noneditable
(
self
):
"""
"""
...
@@ -334,7 +371,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -334,7 +371,7 @@ class TestAccountAPI(UserAPITestCase):
"Field '{0}' cannot be edited."
.
format
(
field_name
),
data
[
"field_errors"
][
field_name
][
"user_message"
]
"Field '{0}' cannot be edited."
.
format
(
field_name
),
data
[
"field_errors"
][
field_name
][
"user_message"
]
)
)
for
field_name
in
[
"username"
,
"date_joined"
]:
for
field_name
in
[
"username"
,
"date_joined"
,
"is_active"
]:
response
=
self
.
send_patch
(
client
,
{
field_name
:
"will_error"
,
"gender"
:
"f"
},
expected_status
=
400
)
response
=
self
.
send_patch
(
client
,
{
field_name
:
"will_error"
,
"gender"
:
"f"
},
expected_status
=
400
)
verify_error_response
(
field_name
,
response
.
data
)
verify_error_response
(
field_name
,
response
.
data
)
...
@@ -352,7 +389,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -352,7 +389,7 @@ class TestAccountAPI(UserAPITestCase):
"""
"""
Test the behavior of patch when an incorrect content_type is specified.
Test the behavior of patch when an incorrect content_type is specified.
"""
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
send_patch
(
self
.
client
,
{},
content_type
=
"application/json"
,
expected_status
=
415
)
self
.
send_patch
(
self
.
client
,
{},
content_type
=
"application/json"
,
expected_status
=
415
)
self
.
send_patch
(
self
.
client
,
{},
content_type
=
"application/xml"
,
expected_status
=
415
)
self
.
send_patch
(
self
.
client
,
{},
content_type
=
"application/xml"
,
expected_status
=
415
)
...
@@ -361,7 +398,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -361,7 +398,7 @@ class TestAccountAPI(UserAPITestCase):
Tests the behavior of patch when attempting to set fields with a select list of options to the empty string.
Tests the behavior of patch when attempting to set fields with a select list of options to the empty string.
Also verifies the behaviour when setting to None.
Also verifies the behaviour when setting to None.
"""
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
for
field_name
in
[
"gender"
,
"level_of_education"
,
"country"
]:
for
field_name
in
[
"gender"
,
"level_of_education"
,
"country"
]:
self
.
send_patch
(
self
.
client
,
{
field_name
:
""
})
self
.
send_patch
(
self
.
client
,
{
field_name
:
""
})
response
=
self
.
send_get
(
self
.
client
)
response
=
self
.
send_get
(
self
.
client
)
...
@@ -393,7 +430,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -393,7 +430,7 @@ class TestAccountAPI(UserAPITestCase):
get_response
=
self
.
send_get
(
self
.
client
)
get_response
=
self
.
send_get
(
self
.
client
)
self
.
assertEqual
(
new_name
,
get_response
.
data
[
"name"
])
self
.
assertEqual
(
new_name
,
get_response
.
data
[
"name"
])
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
legacy_profile
=
UserProfile
.
objects
.
get
(
id
=
self
.
user
.
id
)
legacy_profile
=
UserProfile
.
objects
.
get
(
id
=
self
.
user
.
id
)
self
.
assertEqual
({},
legacy_profile
.
get_meta
())
self
.
assertEqual
({},
legacy_profile
.
get_meta
())
old_name
=
legacy_profile
.
name
old_name
=
legacy_profile
.
name
...
@@ -465,7 +502,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -465,7 +502,7 @@ class TestAccountAPI(UserAPITestCase):
Test that AccountUpdateErrors are passed through to the response.
Test that AccountUpdateErrors are passed through to the response.
"""
"""
serializer_save
.
side_effect
=
[
Exception
(
"bummer"
),
None
]
serializer_save
.
side_effect
=
[
Exception
(
"bummer"
),
None
]
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
error_response
=
self
.
send_patch
(
self
.
client
,
{
"goals"
:
"save an account field"
},
expected_status
=
400
)
error_response
=
self
.
send_patch
(
self
.
client
,
{
"goals"
:
"save an account field"
},
expected_status
=
400
)
self
.
assertEqual
(
self
.
assertEqual
(
"Error thrown when saving account updates: 'bummer'"
,
"Error thrown when saving account updates: 'bummer'"
,
...
...
openedx/core/djangoapps/user_api/accounts/views.py
View file @
37dd0ec8
...
@@ -7,12 +7,10 @@ https://openedx.atlassian.net/wiki/display/TNL/User+API
...
@@ -7,12 +7,10 @@ https://openedx.atlassian.net/wiki/display/TNL/User+API
from
rest_framework.views
import
APIView
from
rest_framework.views
import
APIView
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
rest_framework
import
status
from
rest_framework
import
status
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
util.authentication
import
SessionAuthenticationAllowInactiveUser
,
OAuth2AuthenticationAllowInactiveUser
from
rest_framework
import
permissions
from
rest_framework
import
permissions
from
openedx.core.djangoapps.user_api.api.account
import
(
from
..errors
import
UserNotFound
,
UserNotAuthorized
,
AccountUpdateError
,
AccountValidationError
AccountUserNotFound
,
AccountUpdateError
,
AccountNotAuthorized
,
AccountValidationError
)
from
openedx.core.lib.api.parsers
import
MergePatchParser
from
openedx.core.lib.api.parsers
import
MergePatchParser
from
.api
import
get_account_settings
,
update_account_settings
from
.api
import
get_account_settings
,
update_account_settings
...
@@ -93,9 +91,9 @@ class AccountView(APIView):
...
@@ -93,9 +91,9 @@ class AccountView(APIView):
If the update could not be completed due to failure at the time of update, this method returns a 400 with
If the update could not be completed due to failure at the time of update, this method returns a 400 with
specific errors in the returned JSON.
specific errors in the returned JSON.
If the update
d
is successful, a 204 status is returned with no additional content.
If the update is successful, a 204 status is returned with no additional content.
"""
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
authentication_classes
=
(
OAuth2Authentication
AllowInactiveUser
,
SessionAuthenticationAllowInactiveUser
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
parser_classes
=
(
MergePatchParser
,)
parser_classes
=
(
MergePatchParser
,)
...
@@ -105,7 +103,7 @@ class AccountView(APIView):
...
@@ -105,7 +103,7 @@ class AccountView(APIView):
"""
"""
try
:
try
:
account_settings
=
get_account_settings
(
request
.
user
,
username
,
view
=
request
.
QUERY_PARAMS
.
get
(
'view'
))
account_settings
=
get_account_settings
(
request
.
user
,
username
,
view
=
request
.
QUERY_PARAMS
.
get
(
'view'
))
except
Account
UserNotFound
:
except
UserNotFound
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
account_settings
)
return
Response
(
account_settings
)
...
@@ -120,7 +118,7 @@ class AccountView(APIView):
...
@@ -120,7 +118,7 @@ class AccountView(APIView):
"""
"""
try
:
try
:
update_account_settings
(
request
.
user
,
request
.
DATA
,
username
=
username
)
update_account_settings
(
request
.
user
,
request
.
DATA
,
username
=
username
)
except
(
AccountUserNotFound
,
Account
NotAuthorized
):
except
(
UserNotFound
,
User
NotAuthorized
):
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
except
AccountValidationError
as
err
:
except
AccountValidationError
as
err
:
return
Response
({
"field_errors"
:
err
.
field_errors
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
({
"field_errors"
:
err
.
field_errors
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
...
...
openedx/core/djangoapps/user_api/api/account.py
deleted
100644 → 0
View file @
ba85f82e
"""Python API for user accounts.
Account information includes a student's username, password, and email
address, but does NOT include user profile information (i.e., demographic
information and preferences).
"""
from
django.conf
import
settings
from
django.db
import
transaction
,
IntegrityError
from
django.core.validators
import
validate_email
,
validate_slug
,
ValidationError
from
..forms
import
PasswordResetFormNoActive
from
..models
import
User
,
UserProfile
,
Registration
,
PendingEmailChange
from
..helpers
import
intercept_errors
USERNAME_MIN_LENGTH
=
2
USERNAME_MAX_LENGTH
=
30
EMAIL_MIN_LENGTH
=
3
EMAIL_MAX_LENGTH
=
254
PASSWORD_MIN_LENGTH
=
2
PASSWORD_MAX_LENGTH
=
75
class
AccountRequestError
(
Exception
):
"""There was a problem with the request to the account API. """
pass
class
AccountInternalError
(
Exception
):
"""An internal error occurred in the account API. """
pass
class
AccountUserAlreadyExists
(
AccountRequestError
):
"""User with the same username and/or email already exists. """
pass
class
AccountUsernameInvalid
(
AccountRequestError
):
"""The requested username is not in a valid format. """
pass
class
AccountEmailInvalid
(
AccountRequestError
):
"""The requested email is not in a valid format. """
pass
class
AccountPasswordInvalid
(
AccountRequestError
):
"""The requested password is not in a valid format. """
pass
class
AccountUserNotFound
(
AccountRequestError
):
"""The requested user does not exist. """
pass
class
AccountNotAuthorized
(
AccountRequestError
):
"""The user is not authorized to perform the requested action. """
pass
class
AccountUpdateError
(
AccountRequestError
):
"""
An update to the account failed. More detailed information is present in developer_message,
and depending on the type of error encountered, there may also be a non-null user_message field.
"""
def
__init__
(
self
,
developer_message
,
user_message
=
None
):
self
.
developer_message
=
developer_message
self
.
user_message
=
user_message
class
AccountValidationError
(
AccountRequestError
):
"""
Validation issues were found with the supplied data. More detailed information is present in field_errors,
a dict with specific information about each field that failed validation. For each field,
there will be at least a developer_message describing the validation issue, and possibly
also a user_message.
"""
def
__init__
(
self
,
field_errors
):
self
.
field_errors
=
field_errors
@intercept_errors
(
AccountInternalError
,
ignore_errors
=
[
AccountRequestError
])
@transaction.commit_on_success
def
create_account
(
username
,
password
,
email
):
"""Create a new user account.
This will implicitly create an empty profile for the user.
WARNING: This function does NOT yet implement all the features
in `student/views.py`. Until it does, please use this method
ONLY for tests of the account API, not in production code.
In particular, these are currently missing:
* 3rd party auth
* External auth (shibboleth)
* Complex password policies (ENFORCE_PASSWORD_POLICY)
In addition, we assume that some functionality is handled
at higher layers:
* Analytics events
* Activation email
* Terms of service / honor code checking
* Recording demographic info (use profile API)
* Auto-enrollment in courses (if invited via instructor dash)
Args:
username (unicode): The username for the new account.
password (unicode): The user's password.
email (unicode): The email address associated with the account.
Returns:
unicode: an activation key for the account.
Raises:
AccountUserAlreadyExists
AccountUsernameInvalid
AccountEmailInvalid
AccountPasswordInvalid
"""
# Validate the username, password, and email
# This will raise an exception if any of these are not in a valid format.
_validate_username
(
username
)
_validate_password
(
password
,
username
)
_validate_email
(
email
)
# Create the user account, setting them to "inactive" until they activate their account.
user
=
User
(
username
=
username
,
email
=
email
,
is_active
=
False
)
user
.
set_password
(
password
)
try
:
user
.
save
()
except
IntegrityError
:
raise
AccountUserAlreadyExists
# Create a registration to track the activation process
# This implicitly saves the registration.
registration
=
Registration
()
registration
.
register
(
user
)
# Create an empty user profile with default values
UserProfile
(
user
=
user
)
.
save
()
# Return the activation key, which the caller should send to the user
return
registration
.
activation_key
def
check_account_exists
(
username
=
None
,
email
=
None
):
"""Check whether an account with a particular username or email already exists.
Keyword Arguments:
username (unicode)
email (unicode)
Returns:
list of conflicting fields
Example Usage:
>>> account_api.check_account_exists(username="bob")
[]
>>> account_api.check_account_exists(username="ted", email="ted@example.com")
["email", "username"]
"""
conflicts
=
[]
if
email
is
not
None
and
User
.
objects
.
filter
(
email
=
email
)
.
exists
():
conflicts
.
append
(
"email"
)
if
username
is
not
None
and
User
.
objects
.
filter
(
username
=
username
)
.
exists
():
conflicts
.
append
(
"username"
)
return
conflicts
@intercept_errors
(
AccountInternalError
,
ignore_errors
=
[
AccountRequestError
])
def
account_info
(
username
):
"""Retrieve information about a user's account.
Arguments:
username (unicode): The username associated with the account.
Returns:
dict: User's account information, if the user was found.
None: The user does not exist.
"""
try
:
user
=
User
.
objects
.
get
(
username
=
username
)
except
User
.
DoesNotExist
:
return
None
else
:
return
{
u'username'
:
username
,
u'email'
:
user
.
email
,
u'is_active'
:
user
.
is_active
,
}
@intercept_errors
(
AccountInternalError
,
ignore_errors
=
[
AccountRequestError
])
def
activate_account
(
activation_key
):
"""Activate a user's account.
Args:
activation_key (unicode): The activation key the user received via email.
Returns:
None
Raises:
AccountNotAuthorized
"""
try
:
registration
=
Registration
.
objects
.
get
(
activation_key
=
activation_key
)
except
Registration
.
DoesNotExist
:
raise
AccountNotAuthorized
else
:
# This implicitly saves the registration
registration
.
activate
()
@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
=
PasswordResetFormNoActive
({
'email'
:
email
})
# Validate that a 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 user with the provided email address exists.
raise
AccountUserNotFound
def
_validate_username
(
username
):
"""Validate the username.
Arguments:
username (unicode): The proposed username.
Returns:
None
Raises:
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_slug
(
username
)
except
ValidationError
:
raise
AccountUsernameInvalid
(
u"Username '{username}' must contain only A-Z, a-z, 0-9, -, or _ characters"
)
def
_validate_password
(
password
,
username
):
"""Validate the format of the user's password.
Passwords cannot be the same as the username of the account,
so we take `username` as an argument.
Arguments:
password (unicode): The proposed password.
username (unicode): The username associated with the user's account.
Returns:
None
Raises:
AccountPasswordInvalid
"""
if
not
isinstance
(
password
,
basestring
):
raise
AccountPasswordInvalid
(
u"Password must be a string"
)
if
len
(
password
)
<
PASSWORD_MIN_LENGTH
:
raise
AccountPasswordInvalid
(
u"Password must be at least {min} characters long"
.
format
(
min
=
PASSWORD_MIN_LENGTH
)
)
if
len
(
password
)
>
PASSWORD_MAX_LENGTH
:
raise
AccountPasswordInvalid
(
u"Password must be at most {max} characters long"
.
format
(
max
=
PASSWORD_MAX_LENGTH
)
)
if
password
==
username
:
raise
AccountPasswordInvalid
(
u"Password cannot be the same as the username"
)
def
_validate_email
(
email
):
"""Validate the format of the email address.
Arguments:
email (unicode): The proposed email.
Returns:
None
Raises:
AccountEmailInvalid
"""
if
not
isinstance
(
email
,
basestring
):
raise
AccountEmailInvalid
(
u"Email must be a string"
)
if
len
(
email
)
<
EMAIL_MIN_LENGTH
:
raise
AccountEmailInvalid
(
u"Email '{email}' must be at least {min} characters long"
.
format
(
email
=
email
,
min
=
EMAIL_MIN_LENGTH
)
)
if
len
(
email
)
>
EMAIL_MAX_LENGTH
:
raise
AccountEmailInvalid
(
u"Email '{email}' must be at most {max} characters long"
.
format
(
email
=
email
,
max
=
EMAIL_MAX_LENGTH
)
)
try
:
validate_email
(
email
)
except
ValidationError
:
raise
AccountEmailInvalid
(
u"Email '{email}' format is not valid"
.
format
(
email
=
email
)
)
openedx/core/djangoapps/user_api/api/profile.py
deleted
100644 → 0
View file @
ba85f82e
"""Python API for user profiles.
Profile information includes a student's demographic information and preferences,
but does NOT include basic account information such as username, password, and
email address.
"""
import
datetime
import
logging
from
django.conf
import
settings
from
django.db
import
IntegrityError
from
pytz
import
UTC
import
analytics
from
eventtracking
import
tracker
from
..accounts
import
NAME_MIN_LENGTH
from
..accounts.api
import
get_account_settings
from
..models
import
User
,
UserPreference
,
UserOrgTag
from
..helpers
import
intercept_errors
log
=
logging
.
getLogger
(
__name__
)
class
ProfileRequestError
(
Exception
):
""" The request to the API was not valid. """
pass
class
ProfileUserNotFound
(
ProfileRequestError
):
""" The requested user does not exist. """
pass
class
ProfileInternalError
(
Exception
):
""" An error occurred in an API call. """
pass
FULL_NAME_MAX_LENGTH
=
255
FULL_NAME_MIN_LENGTH
=
NAME_MIN_LENGTH
@intercept_errors
(
ProfileInternalError
,
ignore_errors
=
[
ProfileRequestError
])
def
preference_info
(
username
):
"""Retrieve information about a user's preferences.
Arguments:
username (unicode): The username of the account to retrieve.
Returns:
dict: Empty if there is no user
"""
preferences
=
UserPreference
.
objects
.
filter
(
user__username
=
username
)
preferences_dict
=
{}
for
preference
in
preferences
:
preferences_dict
[
preference
.
key
]
=
preference
.
value
return
preferences_dict
@intercept_errors
(
ProfileInternalError
,
ignore_errors
=
[
ProfileRequestError
])
def
update_preferences
(
username
,
**
kwargs
):
"""Update a user's preferences.
Sets the provided preferences for the given user.
Arguments:
username (unicode): The username of the account to retrieve.
Keyword Arguments:
**kwargs (unicode): Arbitrary key-value preference pairs
Returns:
None
Raises:
ProfileUserNotFound
"""
try
:
user
=
User
.
objects
.
get
(
username
=
username
)
except
User
.
DoesNotExist
:
raise
ProfileUserNotFound
else
:
for
key
,
value
in
kwargs
.
iteritems
():
UserPreference
.
set_preference
(
user
,
key
,
value
)
@intercept_errors
(
ProfileInternalError
,
ignore_errors
=
[
ProfileRequestError
])
def
update_email_opt_in
(
user
,
org
,
optin
):
"""Updates a user's preference for receiving org-wide emails.
Sets a User Org Tag defining the choice to opt in or opt out of organization-wide
emails.
Arguments:
user (User): The user to set a preference for.
org (str): The org is used to determine the organization this setting is related to.
optin (Boolean): True if the user is choosing to receive emails for this organization. If the user is not
the correct age to receive emails, email-optin is set to False regardless.
Returns:
None
"""
account_settings
=
get_account_settings
(
user
)
year_of_birth
=
account_settings
[
'year_of_birth'
]
of_age
=
(
year_of_birth
is
None
or
# If year of birth is not set, we assume user is of age.
datetime
.
datetime
.
now
(
UTC
)
.
year
-
year_of_birth
>
# pylint: disable=maybe-no-member
getattr
(
settings
,
'EMAIL_OPTIN_MINIMUM_AGE'
,
13
)
)
try
:
preference
,
_
=
UserOrgTag
.
objects
.
get_or_create
(
user
=
user
,
org
=
org
,
key
=
'email-optin'
)
preference
.
value
=
str
(
optin
and
of_age
)
preference
.
save
()
if
settings
.
FEATURES
.
get
(
'SEGMENT_IO_LMS'
)
and
settings
.
SEGMENT_IO_LMS_KEY
:
_track_update_email_opt_in
(
user
.
id
,
org
,
optin
)
except
IntegrityError
as
err
:
log
.
warn
(
u"Could not update organization wide preference due to IntegrityError: {}"
.
format
(
err
.
message
))
def
_track_update_email_opt_in
(
user_id
,
organization
,
opt_in
):
"""Track an email opt-in preference change.
Arguments:
user_id (str): The ID of the user making the preference change.
organization (str): The organization whose emails are being opted into or out of by the user.
opt_in (Boolean): Whether the user has chosen to opt-in to emails from the organization.
Returns:
None
"""
event_name
=
'edx.bi.user.org_email.opted_in'
if
opt_in
else
'edx.bi.user.org_email.opted_out'
tracking_context
=
tracker
.
get_tracker
()
.
resolve_context
()
analytics
.
track
(
user_id
,
event_name
,
{
'category'
:
'communication'
,
'label'
:
organization
},
context
=
{
'Google Analytics'
:
{
'clientId'
:
tracking_context
.
get
(
'client_id'
)
}
}
)
openedx/core/djangoapps/user_api/
api
/__init__.py
→
openedx/core/djangoapps/user_api/
course_tag
/__init__.py
View file @
37dd0ec8
File moved
openedx/core/djangoapps/user_api/
api/course_tag
.py
→
openedx/core/djangoapps/user_api/
course_tag/api
.py
View file @
37dd0ec8
File moved
openedx/core/djangoapps/user_api/
tests/test_course_tag
_api.py
→
openedx/core/djangoapps/user_api/
course_tag/tests/test
_api.py
View file @
37dd0ec8
...
@@ -4,7 +4,7 @@ Test the user course tag API.
...
@@ -4,7 +4,7 @@ Test the user course tag API.
from
django.test
import
TestCase
from
django.test
import
TestCase
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
openedx.core.djangoapps.user_api.
api
import
course_tag
as
course_tag_api
from
openedx.core.djangoapps.user_api.
course_tag
import
api
as
course_tag_api
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
...
...
openedx/core/djangoapps/user_api/errors.py
0 → 100644
View file @
37dd0ec8
"""
Errors thrown by the various user APIs.
"""
class
UserAPIRequestError
(
Exception
):
"""There was a problem with the request to the User API. """
pass
class
UserAPIInternalError
(
Exception
):
"""An internal error occurred in the User API. """
pass
class
UserNotFound
(
UserAPIRequestError
):
"""The requested user does not exist. """
pass
class
UserNotAuthorized
(
UserAPIRequestError
):
"""The user is not authorized to perform the requested action. """
pass
class
AccountRequestError
(
UserAPIRequestError
):
"""There was a problem with the request to the account API. """
pass
class
AccountUserAlreadyExists
(
AccountRequestError
):
"""User with the same username and/or email already exists. """
pass
class
AccountUsernameInvalid
(
AccountRequestError
):
"""The requested username is not in a valid format. """
pass
class
AccountEmailInvalid
(
AccountRequestError
):
"""The requested email is not in a valid format. """
pass
class
AccountPasswordInvalid
(
AccountRequestError
):
"""The requested password is not in a valid format. """
pass
class
AccountUpdateError
(
AccountRequestError
):
"""
An update to the account failed. More detailed information is present in developer_message,
and depending on the type of error encountered, there may also be a non-null user_message field.
"""
def
__init__
(
self
,
developer_message
,
user_message
=
None
):
self
.
developer_message
=
developer_message
self
.
user_message
=
user_message
class
AccountValidationError
(
AccountRequestError
):
"""
Validation issues were found with the supplied data. More detailed information is present in field_errors,
a dict with specific information about each field that failed validation. For each field,
there will be at least a developer_message describing the validation issue, and possibly
also a user_message.
"""
def
__init__
(
self
,
field_errors
):
self
.
field_errors
=
field_errors
class
PreferenceRequestError
(
UserAPIRequestError
):
"""There was a problem with a preference request."""
pass
class
PreferenceValidationError
(
PreferenceRequestError
):
"""
Validation issues were found with the supplied data. More detailed information is present
in preference_errors, a dict with specific information about each preference that failed
validation. For each preference, there will be at least a developer_message describing
the validation issue, and possibly also a user_message.
"""
def
__init__
(
self
,
preference_errors
):
self
.
preference_errors
=
preference_errors
class
PreferenceUpdateError
(
PreferenceRequestError
):
"""
An update to a user preference failed. More detailed information is present in developer_message,
and depending on the type of error encountered, there may also be a non-null user_message field.
"""
def
__init__
(
self
,
developer_message
,
user_message
=
None
):
self
.
developer_message
=
developer_message
self
.
user_message
=
user_message
openedx/core/djangoapps/user_api/helpers.py
View file @
37dd0ec8
...
@@ -45,9 +45,20 @@ def intercept_errors(api_error, ignore_errors=None):
...
@@ -45,9 +45,20 @@ def intercept_errors(api_error, ignore_errors=None):
try
:
try
:
return
func
(
*
args
,
**
kwargs
)
return
func
(
*
args
,
**
kwargs
)
except
Exception
as
ex
:
except
Exception
as
ex
:
# Raise the original exception if it's in our list of "ignored" errors
# Raise
and log
the original exception if it's in our list of "ignored" errors
for
ignored
in
ignore_errors
or
[]:
for
ignored
in
ignore_errors
or
[]:
if
isinstance
(
ex
,
ignored
):
if
isinstance
(
ex
,
ignored
):
msg
=
(
u"A handled error occurred when calling '{func_name}' "
u"with arguments '{args}' and keyword arguments '{kwargs}': "
u"{exception}"
)
.
format
(
func_name
=
func
.
func_name
,
args
=
args
,
kwargs
=
kwargs
,
exception
=
repr
(
ex
)
)
LOGGER
.
warning
(
msg
)
raise
raise
# Otherwise, log the error and raise the API-specific error
# Otherwise, log the error and raise the API-specific error
...
...
openedx/core/djangoapps/user_api/management/tests/test_email_opt_in_list.py
View file @
37dd0ec8
...
@@ -19,7 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
...
@@ -19,7 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
import
openedx.core.djangoapps.user_api.api.profile
as
profile_api
from
openedx.core.djangoapps.user_api.preferences.api
import
update_email_opt_in
from
openedx.core.djangoapps.user_api.models
import
UserOrgTag
from
openedx.core.djangoapps.user_api.models
import
UserOrgTag
from
openedx.core.djangoapps.user_api.management.commands
import
email_opt_in_list
from
openedx.core.djangoapps.user_api.management.commands
import
email_opt_in_list
...
@@ -297,7 +297,7 @@ class EmailOptInListTest(ModuleStoreTestCase):
...
@@ -297,7 +297,7 @@ class EmailOptInListTest(ModuleStoreTestCase):
None
None
"""
"""
profile_api
.
update_email_opt_in
(
user
,
org
,
is_opted_in
)
update_email_opt_in
(
user
,
org
,
is_opted_in
)
def
_latest_pref_set_datetime
(
self
,
user
):
def
_latest_pref_set_datetime
(
self
,
user
):
"""Retrieve the latest opt-in preference for the user,
"""Retrieve the latest opt-in preference for the user,
...
...
openedx/core/djangoapps/user_api/models.py
View file @
37dd0ec8
...
@@ -25,27 +25,26 @@ class UserPreference(models.Model):
...
@@ -25,27 +25,26 @@ class UserPreference(models.Model):
unique_together
=
(
"user"
,
"key"
)
unique_together
=
(
"user"
,
"key"
)
@classmethod
@classmethod
def
set_preference
(
cls
,
user
,
preference_key
,
preference_value
):
def
get_value
(
cls
,
user
,
preference_key
):
"""
"""Gets the user preference value for a given key.
Sets the user preference for a given key
"""
user_pref
,
_
=
cls
.
objects
.
get_or_create
(
user
=
user
,
key
=
preference_key
)
user_pref
.
value
=
preference_value
user_pref
.
save
()
@classmethod
Note:
def
get_preference
(
cls
,
user
,
preference_key
,
default
=
None
):
This method provides no authorization of access to the user preference.
"""
Consider using user_api.preferences.api.get_user_preference instead if
Gets the user preference value for a given key
this is part of a REST API request.
Returns the given default if there isn't a preference for the given key
Arguments:
"""
user (User): The user whose preference should be set.
preference_key (string): The key for the user preference.
Returns:
The user preference value, or None if one is not set.
"""
try
:
try
:
user_pref
=
cls
.
objects
.
get
(
user
=
user
,
key
=
preference_key
)
user_pref
erence
=
cls
.
objects
.
get
(
user
=
user
,
key
=
preference_key
)
return
user_pref
.
value
return
user_pref
erence
.
value
except
cls
.
DoesNotExist
:
except
cls
.
DoesNotExist
:
return
default
return
None
class
UserCourseTag
(
models
.
Model
):
class
UserCourseTag
(
models
.
Model
):
...
...
openedx/core/djangoapps/user_api/partition_schemes.py
View file @
37dd0ec8
...
@@ -3,7 +3,7 @@ Provides partition support to the user service.
...
@@ -3,7 +3,7 @@ Provides partition support to the user service.
"""
"""
import
logging
import
logging
import
random
import
random
import
api.course_tag
as
course_tag_api
import
course_tag.api
as
course_tag_api
from
xmodule.partitions.partitions
import
UserPartitionError
,
NoSuchUserPartitionGroupError
from
xmodule.partitions.partitions
import
UserPartitionError
,
NoSuchUserPartitionGroupError
...
...
openedx/core/djangoapps/user_api/preferences/__init__.py
0 → 100644
View file @
37dd0ec8
openedx/core/djangoapps/user_api/preferences/api.py
0 → 100644
View file @
37dd0ec8
"""
API for managing user preferences.
"""
import
datetime
import
logging
import
string
import
analytics
from
eventtracking
import
tracker
from
pytz
import
UTC
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.db
import
transaction
,
IntegrityError
from
django.utils.translation
import
ugettext
as
_
from
student.models
import
UserProfile
from
..errors
import
(
UserAPIInternalError
,
UserAPIRequestError
,
UserNotFound
,
UserNotAuthorized
,
PreferenceValidationError
,
PreferenceUpdateError
)
from
..helpers
import
intercept_errors
from
..models
import
UserOrgTag
,
UserPreference
from
..serializers
import
UserSerializer
,
RawUserPreferenceSerializer
log
=
logging
.
getLogger
(
__name__
)
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
def
get_user_preference
(
requesting_user
,
preference_key
,
username
=
None
):
"""Returns the value of the user preference with the specified key.
Args:
requesting_user (User): The user requesting the user preferences. Only the user with username
`username` or users with "is_staff" privileges can access the preferences.
preference_key (string): The key for the user preference.
username (str): Optional username for which to look up the preferences. If not specified,
`requesting_user.username` is assumed.
Returns:
The value for the user preference which is always a string, or None if a preference
has not been specified.
Raises:
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 the user preference.
UserAPIInternalError: the operation failed due to an unexpected error.
"""
existing_user
=
_get_user
(
requesting_user
,
username
,
allow_staff
=
True
)
return
UserPreference
.
get_value
(
existing_user
,
preference_key
)
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
def
get_user_preferences
(
requesting_user
,
username
=
None
):
"""Returns all user preferences as a JSON response.
Args:
requesting_user (User): The user requesting the user preferences. Only the user with username
`username` or users with "is_staff" privileges can access the preferences.
username (str): Optional username for which to look up the preferences. If not specified,
`requesting_user.username` is assumed.
Returns:
A dict containing account fields.
Raises:
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 the user preference.
UserAPIInternalError: the operation failed due to an unexpected error.
"""
existing_user
=
_get_user
(
requesting_user
,
username
,
allow_staff
=
True
)
user_serializer
=
UserSerializer
(
existing_user
)
return
user_serializer
.
data
[
"preferences"
]
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
@transaction.commit_on_success
def
update_user_preferences
(
requesting_user
,
update
,
username
=
None
):
"""Update the user preferences for the given username.
Note:
It is up to the caller of this method to enforce the contract that this method is only called
with the user who made the request.
Arguments:
requesting_user (User): The user requesting to modify account information. Only the user with username
'username' has permissions to modify account information.
update (dict): The updated account field values.
Some notes:
Values are expected to be strings. Non-string values will be converted to strings.
Null values for a preference will be treated as a request to delete the key in question.
username (string): Optional username specifying which account should be updated. If not specified,
`requesting_user.username` is assumed.
Raises:
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
associated with `username`
PreferenceValidationError: the update was not attempted because validation errors were found
PreferenceUpdateError: the operation failed when performing the update.
UserAPIInternalError: the operation failed due to an unexpected error.
"""
existing_user
=
_get_user
(
requesting_user
,
username
)
# First validate each preference setting
errors
=
{}
serializers
=
{}
for
preference_key
in
update
.
keys
():
preference_value
=
update
[
preference_key
]
if
preference_value
is
not
None
:
try
:
serializer
=
create_user_preference_serializer
(
existing_user
,
preference_key
,
preference_value
)
validate_user_preference_serializer
(
serializer
,
preference_key
,
preference_value
)
serializers
[
preference_key
]
=
serializer
except
PreferenceValidationError
as
error
:
preference_error
=
error
.
preference_errors
[
preference_key
]
errors
[
preference_key
]
=
{
"developer_message"
:
preference_error
[
"developer_message"
],
"user_message"
:
preference_error
[
"user_message"
],
}
if
errors
:
raise
PreferenceValidationError
(
errors
)
# Then perform the patch
for
preference_key
in
update
.
keys
():
preference_value
=
update
[
preference_key
]
if
preference_value
is
not
None
:
try
:
serializer
=
serializers
[
preference_key
]
serializer
.
save
()
except
Exception
as
error
:
raise
_create_preference_update_error
(
preference_key
,
preference_value
,
error
)
else
:
delete_user_preference
(
requesting_user
,
preference_key
)
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
@transaction.commit_on_success
def
set_user_preference
(
requesting_user
,
preference_key
,
preference_value
,
username
=
None
):
"""Update a user preference for the given username.
Note:
It is up to the caller of this method to enforce the contract that this method is only called
with the user who made the request.
Arguments:
requesting_user (User): The user requesting to modify account information. Only the user with username
'username' has permissions to modify account information.
preference_key (string): The key for the user preference.
preference_value (string): The value to be stored. Non-string values will be converted to strings.
username (string): Optional username specifying which account should be updated. If not specified,
`requesting_user.username` is assumed.
Raises:
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
associated with `username`
PreferenceValidationError: the update was not attempted because validation errors were found
PreferenceUpdateError: the operation failed when performing the update.
UserAPIInternalError: the operation failed due to an unexpected error.
"""
existing_user
=
_get_user
(
requesting_user
,
username
)
serializer
=
create_user_preference_serializer
(
existing_user
,
preference_key
,
preference_value
)
validate_user_preference_serializer
(
serializer
,
preference_key
,
preference_value
)
try
:
serializer
.
save
()
except
Exception
as
error
:
raise
_create_preference_update_error
(
preference_key
,
preference_value
,
error
)
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
@transaction.commit_on_success
def
delete_user_preference
(
requesting_user
,
preference_key
,
username
=
None
):
"""Deletes a user preference on behalf of a requesting user.
Note:
It is up to the caller of this method to enforce the contract that this method is only called
with the user who made the request.
Arguments:
requesting_user (User): The user requesting to delete the preference. Only the user with username
'username' has permissions to delete their own preference.
preference_key (string): The key for the user preference.
username (string): Optional username specifying which account should be updated. If not specified,
`requesting_user.username` is assumed.
Returns:
True if the preference was deleted, False if the user did not have a preference with the supplied key.
Raises:
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
associated with `username`
PreferenceUpdateError: the operation failed when performing the update.
UserAPIInternalError: the operation failed due to an unexpected error.
"""
existing_user
=
_get_user
(
requesting_user
,
username
)
try
:
user_preference
=
UserPreference
.
objects
.
get
(
user
=
existing_user
,
key
=
preference_key
)
except
ObjectDoesNotExist
:
return
False
try
:
user_preference
.
delete
()
except
Exception
as
error
:
raise
PreferenceUpdateError
(
developer_message
=
u"Delete failed for user preference '{preference_key}': {error}"
.
format
(
preference_key
=
preference_key
,
error
=
error
),
user_message
=
_
(
u"Delete failed for user preference '{preference_key}'."
)
.
format
(
preference_key
=
preference_key
),
)
return
True
@intercept_errors
(
UserAPIInternalError
,
ignore_errors
=
[
UserAPIRequestError
])
def
update_email_opt_in
(
user
,
org
,
optin
):
"""Updates a user's preference for receiving org-wide emails.
Sets a User Org Tag defining the choice to opt in or opt out of organization-wide
emails.
Arguments:
user (User): The user to set a preference for.
org (str): The org is used to determine the organization this setting is related to.
optin (Boolean): True if the user is choosing to receive emails for this organization. If the user is not
the correct age to receive emails, email-optin is set to False regardless.
Returns:
None
"""
# Avoid calling get_account_settings because it introduces circularity for many callers who need both
# preferences and account information.
try
:
user_profile
=
UserProfile
.
objects
.
get
(
user
=
user
)
except
ObjectDoesNotExist
:
raise
UserNotFound
()
year_of_birth
=
user_profile
.
year_of_birth
of_age
=
(
year_of_birth
is
None
or
# If year of birth is not set, we assume user is of age.
datetime
.
datetime
.
now
(
UTC
)
.
year
-
year_of_birth
>
# pylint: disable=maybe-no-member
getattr
(
settings
,
'EMAIL_OPTIN_MINIMUM_AGE'
,
13
)
)
try
:
preference
,
_
=
UserOrgTag
.
objects
.
get_or_create
(
user
=
user
,
org
=
org
,
key
=
'email-optin'
)
preference
.
value
=
str
(
optin
and
of_age
)
preference
.
save
()
if
settings
.
FEATURES
.
get
(
'SEGMENT_IO_LMS'
)
and
settings
.
SEGMENT_IO_LMS_KEY
:
_track_update_email_opt_in
(
user
.
id
,
org
,
optin
)
except
IntegrityError
as
err
:
log
.
warn
(
u"Could not update organization wide preference due to IntegrityError: {}"
.
format
(
err
.
message
))
def
_track_update_email_opt_in
(
user_id
,
organization
,
opt_in
):
"""Track an email opt-in preference change.
Arguments:
user_id (str): The ID of the user making the preference change.
organization (str): The organization whose emails are being opted into or out of by the user.
opt_in (Boolean): Whether the user has chosen to opt-in to emails from the organization.
Returns:
None
"""
event_name
=
'edx.bi.user.org_email.opted_in'
if
opt_in
else
'edx.bi.user.org_email.opted_out'
tracking_context
=
tracker
.
get_tracker
()
.
resolve_context
()
analytics
.
track
(
user_id
,
event_name
,
{
'category'
:
'communication'
,
'label'
:
organization
},
context
=
{
'Google Analytics'
:
{
'clientId'
:
tracking_context
.
get
(
'client_id'
)
}
}
)
def
_get_user
(
requesting_user
,
username
=
None
,
allow_staff
=
False
):
"""
Helper method to return the user for a given username.
If username is not provided, requesting_user.username is assumed.
"""
if
username
is
None
:
username
=
requesting_user
.
username
try
:
existing_user
=
User
.
objects
.
get
(
username
=
username
)
except
ObjectDoesNotExist
:
raise
UserNotFound
()
if
requesting_user
.
username
!=
username
:
if
not
requesting_user
.
is_staff
or
not
allow_staff
:
raise
UserNotAuthorized
()
return
existing_user
def
create_user_preference_serializer
(
user
,
preference_key
,
preference_value
):
"""Creates a serializer for the specified user preference.
Arguments:
user (User): The user whose preference is being serialized.
preference_key (string): The key for the user preference.
preference_value (string): The value to be stored. Non-string values will be converted to strings.
Returns:
A serializer that can be used to save the user preference.
"""
try
:
existing_user_preference
=
UserPreference
.
objects
.
get
(
user
=
user
,
key
=
preference_key
)
except
ObjectDoesNotExist
:
existing_user_preference
=
None
new_data
=
{
"user"
:
user
.
id
,
"key"
:
preference_key
,
"value"
:
preference_value
,
}
if
existing_user_preference
:
serializer
=
RawUserPreferenceSerializer
(
existing_user_preference
,
data
=
new_data
)
else
:
serializer
=
RawUserPreferenceSerializer
(
data
=
new_data
)
return
serializer
def
validate_user_preference_serializer
(
serializer
,
preference_key
,
preference_value
):
"""Validates a user preference serializer.
Arguments:
serializer (UserPreferenceSerializer): The serializer to be validated.
preference_key (string): The key for the user preference.
preference_value (string): The value to be stored. Non-string values will be converted to strings.
Raises:
PreferenceValidationError: the supplied key and/or value for a user preference are invalid.
"""
if
preference_value
is
None
or
unicode
(
preference_value
)
.
strip
()
==
''
:
message
=
_
(
u"Preference '{preference_key}' cannot be set to an empty value."
)
.
format
(
preference_key
=
preference_key
)
raise
PreferenceValidationError
({
preference_key
:
{
"developer_message"
:
message
,
"user_message"
:
message
}
})
if
not
serializer
.
is_valid
():
developer_message
=
u"Value '{preference_value}' not valid for preference '{preference_key}': {error}"
.
format
(
preference_key
=
preference_key
,
preference_value
=
preference_value
,
error
=
serializer
.
errors
)
if
serializer
.
errors
[
"key"
]:
user_message
=
_
(
u"Invalid user preference key '{preference_key}'."
)
.
format
(
preference_key
=
preference_key
)
else
:
user_message
=
_
(
u"Value '{preference_value}' is not valid for user preference '{preference_key}'."
)
.
format
(
preference_key
=
preference_key
,
preference_value
=
preference_value
)
raise
PreferenceValidationError
({
preference_key
:
{
"developer_message"
:
developer_message
,
"user_message"
:
user_message
,
}
})
def
_create_preference_update_error
(
preference_key
,
preference_value
,
error
):
""" Creates a PreferenceUpdateError with developer_message and user_message. """
return
PreferenceUpdateError
(
developer_message
=
u"Save failed for user preference '{key}' with value '{value}': {error}"
.
format
(
key
=
preference_key
,
value
=
preference_value
,
error
=
error
),
user_message
=
_
(
u"Save failed for user preference '{key}' with value '{value}'."
)
.
format
(
key
=
preference_key
,
value
=
preference_value
),
)
openedx/core/djangoapps/user_api/preferences/tests/__init__.py
0 → 100644
View file @
37dd0ec8
openedx/core/djangoapps/user_api/preferences/tests/test_api.py
0 → 100644
View file @
37dd0ec8
# -*- coding: utf-8 -*-
"""
Unit tests for preference APIs.
"""
import
datetime
import
ddt
import
unittest
from
mock
import
patch
from
pytz
import
UTC
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
dateutil.parser
import
parse
as
parse_datetime
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
...accounts.api
import
create_account
from
...errors
import
UserNotFound
,
UserNotAuthorized
,
PreferenceValidationError
,
PreferenceUpdateError
from
...models
import
UserProfile
,
UserOrgTag
from
...preferences.api
import
(
get_user_preference
,
get_user_preferences
,
set_user_preference
,
update_user_preferences
,
delete_user_preference
,
update_email_opt_in
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Account APIs are only supported in LMS'
)
class
TestPreferenceAPI
(
TestCase
):
"""
These tests specifically cover the parts of the API methods that are not covered by test_views.py.
This includes the specific types of error raised, and default behavior when optional arguments
are not specified.
"""
password
=
"test"
def
setUp
(
self
):
super
(
TestPreferenceAPI
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
(
password
=
self
.
password
)
self
.
different_user
=
UserFactory
.
create
(
password
=
self
.
password
)
self
.
staff_user
=
UserFactory
(
is_staff
=
True
,
password
=
self
.
password
)
self
.
no_such_user
=
UserFactory
.
create
(
password
=
self
.
password
)
self
.
no_such_user
.
username
=
"no_such_user"
self
.
test_preference_key
=
"test_key"
self
.
test_preference_value
=
"test_value"
set_user_preference
(
self
.
user
,
self
.
test_preference_key
,
self
.
test_preference_value
)
def
test_get_user_preference
(
self
):
"""
Verifies the basic behavior of get_user_preference.
"""
self
.
assertEqual
(
get_user_preference
(
self
.
user
,
self
.
test_preference_key
),
self
.
test_preference_value
)
self
.
assertEqual
(
get_user_preference
(
self
.
staff_user
,
self
.
test_preference_key
,
username
=
self
.
user
.
username
),
self
.
test_preference_value
)
def
test_get_user_preference_errors
(
self
):
"""
Verifies that get_user_preference returns appropriate errors.
"""
with
self
.
assertRaises
(
UserNotFound
):
get_user_preference
(
self
.
user
,
self
.
test_preference_key
,
username
=
"no_such_user"
)
with
self
.
assertRaises
(
UserNotFound
):
get_user_preference
(
self
.
no_such_user
,
self
.
test_preference_key
)
with
self
.
assertRaises
(
UserNotAuthorized
):
get_user_preference
(
self
.
different_user
,
self
.
test_preference_key
,
username
=
self
.
user
.
username
)
def
test_get_user_preferences
(
self
):
"""
Verifies the basic behavior of get_user_preferences.
"""
expected_user_preferences
=
{
self
.
test_preference_key
:
self
.
test_preference_value
,
}
self
.
assertEqual
(
get_user_preferences
(
self
.
user
),
expected_user_preferences
)
self
.
assertEqual
(
get_user_preferences
(
self
.
staff_user
,
username
=
self
.
user
.
username
),
expected_user_preferences
)
def
test_get_user_preferences_errors
(
self
):
"""
Verifies that get_user_preferences returns appropriate errors.
"""
with
self
.
assertRaises
(
UserNotFound
):
get_user_preferences
(
self
.
user
,
username
=
"no_such_user"
)
with
self
.
assertRaises
(
UserNotFound
):
get_user_preferences
(
self
.
no_such_user
)
with
self
.
assertRaises
(
UserNotAuthorized
):
get_user_preferences
(
self
.
different_user
,
username
=
self
.
user
.
username
)
def
test_set_user_preference
(
self
):
"""
Verifies the basic behavior of set_user_preference.
"""
test_key
=
u'ⓟⓡⓔⓕⓔⓡⓔⓝⓒⓔ_ⓚⓔⓨ'
test_value
=
u'ǝnןɐʌ_ǝɔuǝɹǝɟǝɹd'
set_user_preference
(
self
.
user
,
test_key
,
test_value
)
self
.
assertEqual
(
get_user_preference
(
self
.
user
,
test_key
),
test_value
)
set_user_preference
(
self
.
user
,
test_key
,
"new_value"
,
username
=
self
.
user
.
username
)
self
.
assertEqual
(
get_user_preference
(
self
.
user
,
test_key
),
"new_value"
)
@patch
(
'openedx.core.djangoapps.user_api.models.UserPreference.save'
)
def
test_set_user_preference_errors
(
self
,
user_preference_save
):
"""
Verifies that set_user_preference returns appropriate errors.
"""
with
self
.
assertRaises
(
UserNotFound
):
set_user_preference
(
self
.
user
,
self
.
test_preference_key
,
"new_value"
,
username
=
"no_such_user"
)
with
self
.
assertRaises
(
UserNotFound
):
set_user_preference
(
self
.
no_such_user
,
self
.
test_preference_key
,
"new_value"
)
with
self
.
assertRaises
(
UserNotAuthorized
):
set_user_preference
(
self
.
staff_user
,
self
.
test_preference_key
,
"new_value"
,
username
=
self
.
user
.
username
)
with
self
.
assertRaises
(
UserNotAuthorized
):
set_user_preference
(
self
.
different_user
,
self
.
test_preference_key
,
"new_value"
,
username
=
self
.
user
.
username
)
too_long_key
=
"x"
*
256
with
self
.
assertRaises
(
PreferenceValidationError
)
as
context_manager
:
set_user_preference
(
self
.
user
,
too_long_key
,
"new_value"
)
errors
=
context_manager
.
exception
.
preference_errors
self
.
assertEqual
(
len
(
errors
.
keys
()),
1
)
self
.
assertEqual
(
errors
[
too_long_key
],
{
"developer_message"
:
get_expected_validation_developer_message
(
too_long_key
,
"new_value"
),
"user_message"
:
get_expected_key_error_user_message
(
too_long_key
,
"new_value"
),
}
)
for
empty_value
in
(
None
,
""
,
" "
):
with
self
.
assertRaises
(
PreferenceValidationError
)
as
context_manager
:
set_user_preference
(
self
.
user
,
self
.
test_preference_key
,
empty_value
)
errors
=
context_manager
.
exception
.
preference_errors
self
.
assertEqual
(
len
(
errors
.
keys
()),
1
)
self
.
assertEqual
(
errors
[
self
.
test_preference_key
],
{
"developer_message"
:
get_empty_preference_message
(
self
.
test_preference_key
),
"user_message"
:
get_empty_preference_message
(
self
.
test_preference_key
),
}
)
user_preference_save
.
side_effect
=
[
Exception
,
None
]
with
self
.
assertRaises
(
PreferenceUpdateError
)
as
context_manager
:
set_user_preference
(
self
.
user
,
u"new_key_ȻħȺɍłɇs"
,
u"new_value_ȻħȺɍłɇs"
)
self
.
assertEqual
(
context_manager
.
exception
.
developer_message
,
u"Save failed for user preference 'new_key_ȻħȺɍłɇs' with value 'new_value_ȻħȺɍłɇs': "
)
self
.
assertEqual
(
context_manager
.
exception
.
user_message
,
u"Save failed for user preference 'new_key_ȻħȺɍłɇs' with value 'new_value_ȻħȺɍłɇs'."
)
def
test_update_user_preferences
(
self
):
"""
Verifies the basic behavior of update_user_preferences.
"""
expected_user_preferences
=
{
self
.
test_preference_key
:
"new_value"
,
}
set_user_preference
(
self
.
user
,
self
.
test_preference_key
,
"new_value"
)
self
.
assertEqual
(
get_user_preference
(
self
.
user
,
self
.
test_preference_key
),
"new_value"
)
set_user_preference
(
self
.
user
,
self
.
test_preference_key
,
"new_value"
,
username
=
self
.
user
.
username
)
self
.
assertEqual
(
get_user_preference
(
self
.
user
,
self
.
test_preference_key
),
"new_value"
)
@patch
(
'openedx.core.djangoapps.user_api.models.UserPreference.delete'
)
@patch
(
'openedx.core.djangoapps.user_api.models.UserPreference.save'
)
def
test_update_user_preferences_errors
(
self
,
user_preference_save
,
user_preference_delete
):
"""
Verifies that set_user_preferences returns appropriate errors.
"""
update_data
=
{
self
.
test_preference_key
:
"new_value"
}
with
self
.
assertRaises
(
UserNotFound
):
update_user_preferences
(
self
.
user
,
update_data
,
username
=
"no_such_user"
)
with
self
.
assertRaises
(
UserNotFound
):
update_user_preferences
(
self
.
no_such_user
,
update_data
)
with
self
.
assertRaises
(
UserNotAuthorized
):
update_user_preferences
(
self
.
staff_user
,
update_data
,
username
=
self
.
user
.
username
)
with
self
.
assertRaises
(
UserNotAuthorized
):
update_user_preferences
(
self
.
different_user
,
update_data
,
username
=
self
.
user
.
username
)
too_long_key
=
"x"
*
256
with
self
.
assertRaises
(
PreferenceValidationError
)
as
context_manager
:
update_user_preferences
(
self
.
user
,
{
too_long_key
:
"new_value"
})
errors
=
context_manager
.
exception
.
preference_errors
self
.
assertEqual
(
len
(
errors
.
keys
()),
1
)
self
.
assertEqual
(
errors
[
too_long_key
],
{
"developer_message"
:
get_expected_validation_developer_message
(
too_long_key
,
"new_value"
),
"user_message"
:
get_expected_key_error_user_message
(
too_long_key
,
"new_value"
),
}
)
for
empty_value
in
(
""
,
" "
):
with
self
.
assertRaises
(
PreferenceValidationError
)
as
context_manager
:
update_user_preferences
(
self
.
user
,
{
self
.
test_preference_key
:
empty_value
})
errors
=
context_manager
.
exception
.
preference_errors
self
.
assertEqual
(
len
(
errors
.
keys
()),
1
)
self
.
assertEqual
(
errors
[
self
.
test_preference_key
],
{
"developer_message"
:
get_empty_preference_message
(
self
.
test_preference_key
),
"user_message"
:
get_empty_preference_message
(
self
.
test_preference_key
),
}
)
user_preference_save
.
side_effect
=
[
Exception
,
None
]
with
self
.
assertRaises
(
PreferenceUpdateError
)
as
context_manager
:
update_user_preferences
(
self
.
user
,
{
self
.
test_preference_key
:
"new_value"
})
self
.
assertEqual
(
context_manager
.
exception
.
developer_message
,
u"Save failed for user preference 'test_key' with value 'new_value': "
)
self
.
assertEqual
(
context_manager
.
exception
.
user_message
,
u"Save failed for user preference 'test_key' with value 'new_value'."
)
user_preference_delete
.
side_effect
=
[
Exception
,
None
]
with
self
.
assertRaises
(
PreferenceUpdateError
)
as
context_manager
:
update_user_preferences
(
self
.
user
,
{
self
.
test_preference_key
:
None
})
self
.
assertEqual
(
context_manager
.
exception
.
developer_message
,
u"Delete failed for user preference 'test_key': "
)
self
.
assertEqual
(
context_manager
.
exception
.
user_message
,
u"Delete failed for user preference 'test_key'."
)
def
test_delete_user_preference
(
self
):
"""
Verifies the basic behavior of delete_user_preference.
"""
self
.
assertTrue
(
delete_user_preference
(
self
.
user
,
self
.
test_preference_key
))
set_user_preference
(
self
.
user
,
self
.
test_preference_key
,
self
.
test_preference_value
)
self
.
assertTrue
(
delete_user_preference
(
self
.
user
,
self
.
test_preference_key
,
username
=
self
.
user
.
username
))
self
.
assertFalse
(
delete_user_preference
(
self
.
user
,
"no_such_key"
))
@patch
(
'openedx.core.djangoapps.user_api.models.UserPreference.delete'
)
def
test_delete_user_preference_errors
(
self
,
user_preference_delete
):
"""
Verifies that delete_user_preference returns appropriate errors.
"""
with
self
.
assertRaises
(
UserNotFound
):
delete_user_preference
(
self
.
user
,
self
.
test_preference_key
,
username
=
"no_such_user"
)
with
self
.
assertRaises
(
UserNotFound
):
delete_user_preference
(
self
.
no_such_user
,
self
.
test_preference_key
)
with
self
.
assertRaises
(
UserNotAuthorized
):
delete_user_preference
(
self
.
staff_user
,
self
.
test_preference_key
,
username
=
self
.
user
.
username
)
with
self
.
assertRaises
(
UserNotAuthorized
):
delete_user_preference
(
self
.
different_user
,
self
.
test_preference_key
,
username
=
self
.
user
.
username
)
user_preference_delete
.
side_effect
=
[
Exception
,
None
]
with
self
.
assertRaises
(
PreferenceUpdateError
)
as
context_manager
:
delete_user_preference
(
self
.
user
,
self
.
test_preference_key
)
self
.
assertEqual
(
context_manager
.
exception
.
developer_message
,
u"Delete failed for user preference 'test_key': "
)
self
.
assertEqual
(
context_manager
.
exception
.
user_message
,
u"Delete failed for user preference 'test_key'."
)
@ddt.ddt
class
UpdateEmailOptInTests
(
ModuleStoreTestCase
):
USERNAME
=
u'frank-underwood'
PASSWORD
=
u'ṕáśśẃőŕd'
EMAIL
=
u'frank+underwood@example.com'
@ddt.data
(
# Check that a 27 year old can opt-in
(
27
,
True
,
u"True"
),
# Check that a 32-year old can opt-out
(
32
,
False
,
u"False"
),
# Check that someone 14 years old can opt-in
(
14
,
True
,
u"True"
),
# Check that someone 13 years old cannot opt-in (must have turned 13 before this year)
(
13
,
True
,
u"False"
),
# Check that someone 12 years old cannot opt-in
(
12
,
True
,
u"False"
)
)
@ddt.unpack
@override_settings
(
EMAIL_OPTIN_MINIMUM_AGE
=
13
)
def
test_update_email_optin
(
self
,
age
,
option
,
expected_result
):
# Create the course and account.
course
=
CourseFactory
.
create
()
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
# Set year of birth
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
profile
=
UserProfile
.
objects
.
get
(
user
=
user
)
year_of_birth
=
datetime
.
datetime
.
now
()
.
year
-
age
# pylint: disable=maybe-no-member
profile
.
year_of_birth
=
year_of_birth
profile
.
save
()
update_email_opt_in
(
user
,
course
.
id
.
org
,
option
)
result_obj
=
UserOrgTag
.
objects
.
get
(
user
=
user
,
org
=
course
.
id
.
org
,
key
=
'email-optin'
)
self
.
assertEqual
(
result_obj
.
value
,
expected_result
)
def
test_update_email_optin_no_age_set
(
self
):
# Test that the API still works if no age is specified.
# Create the course and account.
course
=
CourseFactory
.
create
()
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
update_email_opt_in
(
user
,
course
.
id
.
org
,
True
)
result_obj
=
UserOrgTag
.
objects
.
get
(
user
=
user
,
org
=
course
.
id
.
org
,
key
=
'email-optin'
)
self
.
assertEqual
(
result_obj
.
value
,
u"True"
)
@ddt.data
(
# Check that a 27 year old can opt-in, then out.
(
27
,
True
,
False
,
u"False"
),
# Check that a 32-year old can opt-out, then in.
(
32
,
False
,
True
,
u"True"
),
# Check that someone 13 years old can opt-in, then out.
(
13
,
True
,
False
,
u"False"
),
# Check that someone 12 years old cannot opt-in, then explicitly out.
(
12
,
True
,
False
,
u"False"
)
)
@ddt.unpack
@override_settings
(
EMAIL_OPTIN_MINIMUM_AGE
=
13
)
def
test_change_email_optin
(
self
,
age
,
option
,
second_option
,
expected_result
):
# Create the course and account.
course
=
CourseFactory
.
create
()
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
# Set year of birth
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
profile
=
UserProfile
.
objects
.
get
(
user
=
user
)
year_of_birth
=
datetime
.
datetime
.
now
(
UTC
)
.
year
-
age
# pylint: disable=maybe-no-member
profile
.
year_of_birth
=
year_of_birth
profile
.
save
()
update_email_opt_in
(
user
,
course
.
id
.
org
,
option
)
update_email_opt_in
(
user
,
course
.
id
.
org
,
second_option
)
result_obj
=
UserOrgTag
.
objects
.
get
(
user
=
user
,
org
=
course
.
id
.
org
,
key
=
'email-optin'
)
self
.
assertEqual
(
result_obj
.
value
,
expected_result
)
def
_assert_is_datetime
(
self
,
timestamp
):
if
not
timestamp
:
return
False
try
:
parse_datetime
(
timestamp
)
except
ValueError
:
return
False
else
:
return
True
def
get_expected_validation_developer_message
(
preference_key
,
preference_value
):
"""
Returns the expected dict of validation messages for the specified key.
"""
return
u"Value '{preference_value}' not valid for preference '{preference_key}': {error}"
.
format
(
preference_key
=
preference_key
,
preference_value
=
preference_value
,
error
=
{
"key"
:
[
u"Ensure this value has at most 255 characters (it has 256)."
]
}
)
def
get_expected_key_error_user_message
(
preference_key
,
preference_value
):
"""
Returns the expected user message for an invalid key.
"""
return
u"Invalid user preference key '{preference_key}'."
.
format
(
preference_key
=
preference_key
)
def
get_empty_preference_message
(
preference_key
):
"""
Returns the validation message shown for an empty preference.
"""
return
"Preference '{preference_key}' cannot be set to an empty value."
.
format
(
preference_key
=
preference_key
)
openedx/core/djangoapps/user_api/preferences/tests/test_views.py
0 → 100644
View file @
37dd0ec8
# -*- coding: utf-8 -*-
"""
Unit tests for preference APIs.
"""
import
unittest
import
ddt
import
json
from
django.core.urlresolvers
import
reverse
from
django.conf
import
settings
from
...accounts.tests.test_views
import
UserAPITestCase
from
..api
import
set_user_preference
from
.test_api
import
get_expected_validation_developer_message
,
get_expected_key_error_user_message
TOO_LONG_PREFERENCE_KEY
=
u"x"
*
256
@ddt.ddt
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
TestPreferencesAPI
(
UserAPITestCase
):
"""
Unit tests /api/user/v0/accounts/{username}/
"""
def
setUp
(
self
):
super
(
TestPreferencesAPI
,
self
)
.
setUp
()
self
.
url_endpoint_name
=
"preferences_api"
self
.
url
=
reverse
(
self
.
url_endpoint_name
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
def
test_anonymous_access
(
self
):
"""
Test that an anonymous client (not logged in) cannot call GET or PATCH.
"""
self
.
send_get
(
self
.
anonymous_client
,
expected_status
=
401
)
self
.
send_patch
(
self
.
anonymous_client
,
{},
expected_status
=
401
)
def
test_unsupported_methods
(
self
):
"""
Test that DELETE, POST, and PUT are not supported.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
assertEqual
(
405
,
self
.
client
.
put
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
post
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
delete
(
self
.
url
)
.
status_code
)
def
test_get_different_user
(
self
):
"""
Test that a client (logged in) cannot get the preferences information for a different client.
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
self
.
test_password
)
self
.
send_get
(
self
.
different_client
,
expected_status
=
404
)
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_get_unknown_user
(
self
,
api_client
,
username
):
"""
Test that requesting a user who does not exist returns a 404.
"""
client
=
self
.
login_client
(
api_client
,
username
)
response
=
client
.
get
(
reverse
(
self
.
url_endpoint_name
,
kwargs
=
{
'username'
:
"does_not_exist"
}))
self
.
assertEqual
(
404
,
response
.
status_code
)
def
test_get_preferences_default
(
self
):
"""
Test that a client (logged in) can get her own preferences information (verifying the default
state before any preferences are stored).
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
response
=
self
.
send_get
(
self
.
client
)
self
.
assertEqual
({},
response
.
data
)
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_get_preferences
(
self
,
api_client
,
user
):
"""
Test that a client (logged in) can get her own preferences information. Also verifies that a "is_staff"
user can get the preferences information for other users.
"""
# Create some test preferences values.
set_user_preference
(
self
.
user
,
"dict_pref"
,
{
"int_key"
:
10
})
set_user_preference
(
self
.
user
,
"string_pref"
,
"value"
)
# Log in the client and do the GET.
client
=
self
.
login_client
(
api_client
,
user
)
response
=
self
.
send_get
(
client
)
self
.
assertEqual
({
"dict_pref"
:
"{'int_key': 10}"
,
"string_pref"
:
"value"
},
response
.
data
)
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_patch_unknown_user
(
self
,
api_client
,
user
):
"""
Test that trying to update preferences for a user who does not exist returns a 404.
"""
client
=
self
.
login_client
(
api_client
,
user
)
response
=
client
.
patch
(
reverse
(
self
.
url_endpoint_name
,
kwargs
=
{
'username'
:
"does_not_exist"
}),
data
=
json
.
dumps
({
"string_pref"
:
"value"
}),
content_type
=
"application/merge-patch+json"
)
self
.
assertEqual
(
404
,
response
.
status_code
)
def
test_patch_bad_content_type
(
self
):
"""
Test the behavior of patch when an incorrect content_type is specified.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
send_patch
(
self
.
client
,
{},
content_type
=
"application/json"
,
expected_status
=
415
)
self
.
send_patch
(
self
.
client
,
{},
content_type
=
"application/xml"
,
expected_status
=
415
)
def
test_create_preferences
(
self
):
"""
Test that a client (logged in) can create her own preferences information.
"""
self
.
_do_create_preferences_test
(
True
)
def
test_create_preferences_inactive
(
self
):
"""
Test that a client (logged in but not active) can create her own preferences information.
"""
self
.
_do_create_preferences_test
(
False
)
def
_do_create_preferences_test
(
self
,
is_active
):
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
if
not
is_active
:
self
.
user
.
is_active
=
False
self
.
user
.
save
()
self
.
send_patch
(
self
.
client
,
{
"dict_pref"
:
{
"int_key"
:
10
},
"string_pref"
:
"value"
,
})
response
=
self
.
send_get
(
self
.
client
)
self
.
assertEqual
({
u"dict_pref"
:
u"{u'int_key': 10}"
,
u"string_pref"
:
u"value"
},
response
.
data
)
@ddt.data
(
(
"different_client"
,
"different_user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_create_preferences_other_user
(
self
,
api_client
,
user
):
"""
Test that a client (logged in) cannot create preferences for another user.
"""
client
=
self
.
login_client
(
api_client
,
user
)
self
.
send_patch
(
client
,
{
"dict_pref"
:
{
"int_key"
:
10
},
"string_pref"
:
"value"
,
},
expected_status
=
404
,
)
def
test_update_preferences
(
self
):
"""
Test that a client (logged in) can update her own preferences information.
"""
# Create some test preferences values.
set_user_preference
(
self
.
user
,
"dict_pref"
,
{
"int_key"
:
10
})
set_user_preference
(
self
.
user
,
"string_pref"
,
"value"
)
set_user_preference
(
self
.
user
,
"extra_pref"
,
"extra_value"
)
# Send the patch request
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
send_patch
(
self
.
client
,
{
"string_pref"
:
"updated_value"
,
"new_pref"
:
"new_value"
,
"extra_pref"
:
None
,
})
# Verify that GET returns the updated preferences
response
=
self
.
send_get
(
self
.
client
)
expected_preferences
=
{
"dict_pref"
:
"{'int_key': 10}"
,
"string_pref"
:
"updated_value"
,
"new_pref"
:
"new_value"
,
}
self
.
assertEqual
(
expected_preferences
,
response
.
data
)
def
test_update_preferences_bad_data
(
self
):
"""
Test that a client (logged in) receives appropriate errors for a bad update.
"""
# Create some test preferences values.
set_user_preference
(
self
.
user
,
"dict_pref"
,
{
"int_key"
:
10
})
set_user_preference
(
self
.
user
,
"string_pref"
,
"value"
)
set_user_preference
(
self
.
user
,
"extra_pref"
,
"extra_value"
)
# Send the patch request
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
response
=
self
.
send_patch
(
self
.
client
,
{
"string_pref"
:
"updated_value"
,
TOO_LONG_PREFERENCE_KEY
:
"new_value"
,
"new_pref"
:
"new_value"
,
u"empty_pref_ȻħȺɍłɇs"
:
""
,
},
expected_status
=
400
)
self
.
assertTrue
(
response
.
data
.
get
(
"field_errors"
,
None
))
field_errors
=
response
.
data
[
"field_errors"
]
self
.
assertEquals
(
field_errors
,
{
TOO_LONG_PREFERENCE_KEY
:
{
"developer_message"
:
get_expected_validation_developer_message
(
TOO_LONG_PREFERENCE_KEY
,
"new_value"
),
"user_message"
:
get_expected_key_error_user_message
(
TOO_LONG_PREFERENCE_KEY
,
"new_value"
),
},
u"empty_pref_ȻħȺɍłɇs"
:
{
"developer_message"
:
u"Preference 'empty_pref_ȻħȺɍłɇs' cannot be set to an empty value."
,
"user_message"
:
u"Preference 'empty_pref_ȻħȺɍłɇs' cannot be set to an empty value."
,
},
}
)
# Verify that GET returns the original preferences
response
=
self
.
send_get
(
self
.
client
)
expected_preferences
=
{
u"dict_pref"
:
u"{'int_key': 10}"
,
u"string_pref"
:
u"value"
,
u"extra_pref"
:
u"extra_value"
,
}
self
.
assertEqual
(
expected_preferences
,
response
.
data
)
def
test_update_preferences_bad_request
(
self
):
"""
Test that a client (logged in) receives appropriate errors for a bad request.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
# Verify a non-dict request
response
=
self
.
send_patch
(
self
.
client
,
"non_dict_request"
,
expected_status
=
400
)
self
.
assertEqual
(
response
.
data
,
{
"developer_message"
:
u"No data provided for user preference update"
,
"user_message"
:
u"No data provided for user preference update"
}
)
# Verify an empty dict request
response
=
self
.
send_patch
(
self
.
client
,
{},
expected_status
=
400
)
self
.
assertEqual
(
response
.
data
,
{
"developer_message"
:
u"No data provided for user preference update"
,
"user_message"
:
u"No data provided for user preference update"
}
)
@ddt.data
(
(
"different_client"
,
"different_user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_update_preferences_other_user
(
self
,
api_client
,
user
):
"""
Test that a client (logged in) cannot update preferences for another user.
"""
# Create some test preferences values.
set_user_preference
(
self
.
user
,
"dict_pref"
,
{
"int_key"
:
10
})
set_user_preference
(
self
.
user
,
"string_pref"
,
"value"
)
set_user_preference
(
self
.
user
,
"extra_pref"
,
"extra_value"
)
# Send the patch request
client
=
self
.
login_client
(
api_client
,
user
)
self
.
send_patch
(
client
,
{
"string_pref"
:
"updated_value"
,
"new_pref"
:
"new_value"
,
"extra_pref"
:
None
,
},
expected_status
=
404
)
@ddt.ddt
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
TestPreferencesDetailAPI
(
UserAPITestCase
):
"""
Unit tests /api/user/v0/accounts/{username}/{preference_key}
"""
def
setUp
(
self
):
super
(
TestPreferencesDetailAPI
,
self
)
.
setUp
()
self
.
test_pref_key
=
"test_key"
self
.
test_pref_value
=
"test_value"
set_user_preference
(
self
.
user
,
self
.
test_pref_key
,
self
.
test_pref_value
)
self
.
url_endpoint_name
=
"preferences_detail_api"
self
.
_set_url
(
self
.
test_pref_key
)
def
_set_url
(
self
,
preference_key
):
self
.
url
=
reverse
(
self
.
url_endpoint_name
,
kwargs
=
{
'username'
:
self
.
user
.
username
,
'preference_key'
:
preference_key
}
)
def
test_anonymous_user_access
(
self
):
"""
Test that an anonymous client (logged in) cannot manipulate preferences.
"""
self
.
send_get
(
self
.
anonymous_client
,
expected_status
=
401
)
self
.
send_put
(
self
.
anonymous_client
,
"new_value"
,
expected_status
=
401
)
self
.
send_delete
(
self
.
anonymous_client
,
expected_status
=
401
)
def
test_unsupported_methods
(
self
):
"""
Test that POST and PATCH are not supported.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
assertEqual
(
405
,
self
.
client
.
post
(
self
.
url
)
.
status_code
)
self
.
assertEqual
(
405
,
self
.
client
.
patch
(
self
.
url
)
.
status_code
)
def
test_different_user_access
(
self
):
"""
Test that a client (logged in) cannot manipulate a preference for a different client.
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
self
.
test_password
)
self
.
send_get
(
self
.
different_client
,
expected_status
=
404
)
self
.
send_put
(
self
.
different_client
,
"new_value"
,
expected_status
=
404
)
self
.
send_delete
(
self
.
different_client
,
expected_status
=
404
)
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_get_unknown_user
(
self
,
api_client
,
username
):
"""
Test that requesting a user who does not exist returns a 404.
"""
client
=
self
.
login_client
(
api_client
,
username
)
response
=
client
.
get
(
reverse
(
self
.
url_endpoint_name
,
kwargs
=
{
'username'
:
"does_not_exist"
,
'preference_key'
:
self
.
test_pref_key
})
)
self
.
assertEqual
(
404
,
response
.
status_code
)
def
test_get_preference_does_not_exist
(
self
):
"""
Test that a 404 is returned if the user does not have a preference with the given preference_key.
"""
self
.
_set_url
(
"does_not_exist"
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
response
=
self
.
send_get
(
self
.
client
,
expected_status
=
404
)
self
.
assertIsNone
(
response
.
data
)
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_get_preference
(
self
,
api_client
,
user
):
"""
Test that a client (logged in) can get her own preferences information. Also verifies that a "is_staff"
user can get the preferences information for other users.
"""
client
=
self
.
login_client
(
api_client
,
user
)
response
=
self
.
send_get
(
client
)
self
.
assertEqual
(
self
.
test_pref_value
,
response
.
data
)
# Test a different value.
set_user_preference
(
self
.
user
,
"dict_pref"
,
{
"int_key"
:
10
})
self
.
_set_url
(
"dict_pref"
)
response
=
self
.
send_get
(
client
)
self
.
assertEqual
(
"{'int_key': 10}"
,
response
.
data
)
def
test_create_preference
(
self
):
"""
Test that a client (logged in) can create a preference.
"""
self
.
_do_create_preference_test
(
True
)
def
test_create_preference_inactive
(
self
):
"""
Test that a client (logged in but not active) can create a preference.
"""
self
.
_do_create_preference_test
(
False
)
def
_do_create_preference_test
(
self
,
is_active
):
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
if
not
is_active
:
self
.
user
.
is_active
=
False
self
.
user
.
save
()
self
.
_set_url
(
"new_key"
)
new_value
=
"new value"
self
.
send_put
(
self
.
client
,
new_value
)
response
=
self
.
send_get
(
self
.
client
)
self
.
assertEqual
(
new_value
,
response
.
data
)
@ddt.data
(
(
None
,),
(
""
,),
(
" "
,),
)
@ddt.unpack
def
test_create_empty_preference
(
self
,
preference_value
):
"""
Test that a client (logged in) cannot create an empty preference.
"""
self
.
_set_url
(
"new_key"
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
response
=
self
.
send_put
(
self
.
client
,
preference_value
,
expected_status
=
400
)
self
.
assertEqual
(
response
.
data
,
{
"developer_message"
:
u"Preference 'new_key' cannot be set to an empty value."
,
"user_message"
:
u"Preference 'new_key' cannot be set to an empty value."
}
)
self
.
send_get
(
self
.
client
,
expected_status
=
404
)
def
test_create_preference_too_long_key
(
self
):
"""
Test that a client cannot create preferences with bad keys
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
too_long_preference_key
=
"x"
*
256
new_value
=
"new value"
self
.
_set_url
(
too_long_preference_key
)
response
=
self
.
send_put
(
self
.
client
,
new_value
,
expected_status
=
400
)
self
.
assertEquals
(
response
.
data
,
{
"developer_message"
:
get_expected_validation_developer_message
(
too_long_preference_key
,
new_value
),
"user_message"
:
get_expected_key_error_user_message
(
too_long_preference_key
,
new_value
),
}
)
@ddt.data
(
(
"different_client"
,
"different_user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_create_preference_other_user
(
self
,
api_client
,
user
):
"""
Test that a client (logged in) cannot create a preference for a different user.
"""
# Verify that a new preference cannot be created
self
.
_set_url
(
"new_key"
)
client
=
self
.
login_client
(
api_client
,
user
)
new_value
=
"new value"
self
.
send_put
(
client
,
new_value
,
expected_status
=
404
)
@ddt.data
(
(
u"new value"
,),
(
10
,),
({
u"int_key"
:
10
},)
)
@ddt.unpack
def
test_update_preference
(
self
,
preference_value
):
"""
Test that a client (logged in) can update a preference.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
self
.
send_put
(
self
.
client
,
preference_value
)
response
=
self
.
send_get
(
self
.
client
)
self
.
assertEqual
(
unicode
(
preference_value
),
response
.
data
)
@ddt.data
(
(
"different_client"
,
"different_user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_update_preference_other_user
(
self
,
api_client
,
user
):
"""
Test that a client (logged in) cannot update a preference for another user.
"""
client
=
self
.
login_client
(
api_client
,
user
)
new_value
=
"new value"
self
.
send_put
(
client
,
new_value
,
expected_status
=
404
)
@ddt.data
(
(
None
,),
(
""
,),
(
" "
,),
)
@ddt.unpack
def
test_update_preference_to_empty
(
self
,
preference_value
):
"""
Test that a client (logged in) cannot update a preference to null.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
response
=
self
.
send_put
(
self
.
client
,
preference_value
,
expected_status
=
400
)
self
.
assertEqual
(
response
.
data
,
{
"developer_message"
:
u"Preference 'test_key' cannot be set to an empty value."
,
"user_message"
:
u"Preference 'test_key' cannot be set to an empty value."
}
)
response
=
self
.
send_get
(
self
.
client
)
self
.
assertEqual
(
self
.
test_pref_value
,
response
.
data
)
def
test_delete_preference
(
self
):
"""
Test that a client (logged in) can delete her own preference.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
test_password
)
# Verify that a preference can be deleted
self
.
send_delete
(
self
.
client
)
self
.
send_get
(
self
.
client
,
expected_status
=
404
)
# Verify that deleting a non-existent preference throws a 404
self
.
send_delete
(
self
.
client
,
expected_status
=
404
)
@ddt.data
(
(
"different_client"
,
"different_user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_delete_preference_other_user
(
self
,
api_client
,
user
):
"""
Test that a client (logged in) cannot delete a preference for another user.
"""
client
=
self
.
login_client
(
api_client
,
user
)
self
.
send_delete
(
client
,
expected_status
=
404
)
openedx/core/djangoapps/user_api/preferences/views.py
0 → 100644
View file @
37dd0ec8
"""
NOTE: this API is WIP and has not yet been approved. Do not use this API without talking to Christina or Andy.
For more information, see:
https://openedx.atlassian.net/wiki/display/TNL/User+API
"""
from
rest_framework.views
import
APIView
from
rest_framework.response
import
Response
from
rest_framework
import
status
from
util.authentication
import
SessionAuthenticationAllowInactiveUser
,
OAuth2AuthenticationAllowInactiveUser
from
rest_framework
import
permissions
from
django.utils.translation
import
ugettext
as
_
from
openedx.core.lib.api.parsers
import
MergePatchParser
from
..errors
import
UserNotFound
,
UserNotAuthorized
,
PreferenceValidationError
,
PreferenceUpdateError
from
.api
import
(
get_user_preference
,
get_user_preferences
,
set_user_preference
,
update_user_preferences
,
delete_user_preference
)
class
PreferencesView
(
APIView
):
"""
**Use Cases**
Get or update the user's preference information. Updates are only supported through merge patch.
Preference values of null in a patch request are treated as requests to remove the preference.
**Example Requests**:
GET /api/user/v0/preferences/{username}/
PATCH /api/user/v0/preferences/{username}/ with content_type "application/merge-patch+json"
**Response Value for GET**
A JSON dictionary will be returned with key/value pairs (all of type String).
If a user without "is_staff" access has requested preferences for a different user,
this method returns a 404.
If the specified username does not exist, this method returns a 404.
**Response for PATCH**
Users can only modify their own preferences. If the requesting user does not have username
"username", this method will return with a status of 404.
This method will also return a 404 if no user exists with username "username".
If "application/merge-patch+json" is not the specified content_type, this method returns a 415 status.
If the update could not be completed due to validation errors, this method returns a 400 with all
preference-specific error messages in the "field_errors" field of the returned JSON.
If the update could not be completed due to failure at the time of update, this method returns a 400 with
specific errors in the returned JSON.
If the update is successful, a 204 status is returned with no additional content.
"""
authentication_classes
=
(
OAuth2AuthenticationAllowInactiveUser
,
SessionAuthenticationAllowInactiveUser
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
parser_classes
=
(
MergePatchParser
,)
def
get
(
self
,
request
,
username
):
"""
GET /api/user/v0/preferences/{username}/
"""
try
:
user_preferences
=
get_user_preferences
(
request
.
user
,
username
=
username
)
except
(
UserNotFound
,
UserNotAuthorized
):
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
user_preferences
)
def
patch
(
self
,
request
,
username
):
"""
PATCH /api/user/v0/preferences/{username}/
"""
if
not
request
.
DATA
or
not
getattr
(
request
.
DATA
,
"keys"
,
None
):
error_message
=
_
(
"No data provided for user preference update"
)
return
Response
(
{
"developer_message"
:
error_message
,
"user_message"
:
error_message
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
try
:
update_user_preferences
(
request
.
user
,
request
.
DATA
,
username
=
username
)
except
(
UserNotFound
,
UserNotAuthorized
):
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
except
PreferenceValidationError
as
error
:
return
Response
(
{
"field_errors"
:
error
.
preference_errors
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
except
PreferenceUpdateError
as
error
:
return
Response
(
{
"developer_message"
:
error
.
developer_message
,
"user_message"
:
error
.
user_message
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
status
=
status
.
HTTP_204_NO_CONTENT
)
class
PreferencesDetailView
(
APIView
):
"""
**Use Cases**
Get, create, update, or delete a specific user preference.
**Example Requests**:
GET /api/user/v0/preferences/{username}/{preference_key}
PUT /api/user/v0/preferences/{username}/{preference_key}
DELETE /api/user/v0/preferences/{username}/{preference_key}
**Response Values for GET**
The preference value will be returned as a JSON string.
If a user without "is_staff" access has requested preferences for a different user,
this method returns a 404.
If the specified username or preference does not exist, this method returns a 404.
**Response Values for PUT**
A successful put returns a 204 and no content.
If the specified username or preference does not exist, this method returns a 404.
**Response for DELETE**
A successful delete returns a 204 and no content.
If the specified username or preference does not exist, this method returns a 404.
"""
authentication_classes
=
(
OAuth2AuthenticationAllowInactiveUser
,
SessionAuthenticationAllowInactiveUser
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
get
(
self
,
request
,
username
,
preference_key
):
"""
GET /api/user/v0/preferences/{username}/{preference_key}
"""
try
:
value
=
get_user_preference
(
request
.
user
,
preference_key
,
username
=
username
)
# There was no preference with that key, raise a 404.
if
value
is
None
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
except
(
UserNotFound
,
UserNotAuthorized
):
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
value
)
def
put
(
self
,
request
,
username
,
preference_key
):
"""
PUT /api/user/v0/preferences/{username}/{preference_key}
"""
try
:
set_user_preference
(
request
.
user
,
preference_key
,
request
.
DATA
,
username
=
username
)
except
(
UserNotFound
,
UserNotAuthorized
):
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
except
PreferenceValidationError
as
error
:
return
Response
(
{
"developer_message"
:
error
.
preference_errors
[
preference_key
][
"developer_message"
],
"user_message"
:
error
.
preference_errors
[
preference_key
][
"user_message"
]
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
except
PreferenceUpdateError
as
error
:
return
Response
(
{
"developer_message"
:
error
.
developer_message
,
"user_message"
:
error
.
user_message
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
status
=
status
.
HTTP_204_NO_CONTENT
)
def
delete
(
self
,
request
,
username
,
preference_key
):
"""
DELETE /api/user/v0/preferences/{username}/{preference_key}
"""
try
:
preference_existed
=
delete_user_preference
(
request
.
user
,
preference_key
,
username
=
username
)
except
(
UserNotFound
,
UserNotAuthorized
):
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
except
PreferenceUpdateError
as
error
:
return
Response
(
{
"developer_message"
:
error
.
developer_message
,
"user_message"
:
error
.
user_message
},
status
=
status
.
HTTP_400_BAD_REQUEST
)
if
not
preference_existed
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
status
=
status
.
HTTP_204_NO_CONTENT
)
openedx/core/djangoapps/user_api/serializers.py
View file @
37dd0ec8
...
@@ -29,3 +29,13 @@ class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):
...
@@ -29,3 +29,13 @@ class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):
class
Meta
:
class
Meta
:
model
=
UserPreference
model
=
UserPreference
depth
=
1
depth
=
1
class
RawUserPreferenceSerializer
(
serializers
.
ModelSerializer
):
"""Serializer that generates a raw representation of a user preference.
"""
user
=
serializers
.
PrimaryKeyRelatedField
()
class
Meta
:
model
=
UserPreference
depth
=
1
openedx/core/djangoapps/user_api/tests/test_account_api.py
deleted
100644 → 0
View file @
ba85f82e
# -*- coding: utf-8 -*-
""" Tests for the account API. """
import
re
from
unittest
import
skipUnless
from
nose.tools
import
raises
from
mock
import
patch
import
ddt
from
dateutil.parser
import
parse
as
parse_datetime
from
django.core
import
mail
from
django.test
import
TestCase
from
django.conf
import
settings
from
..api
import
account
as
account_api
from
..models
import
UserProfile
@ddt.ddt
class
AccountApiTest
(
TestCase
):
USERNAME
=
u'frank-underwood'
PASSWORD
=
u'ṕáśśẃőŕd'
EMAIL
=
u'frank+underwood@example.com'
ORIG_HOST
=
'example.com'
IS_SECURE
=
False
INVALID_USERNAMES
=
[
None
,
u''
,
u'a'
,
u'a'
*
(
account_api
.
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'
,
u'frank@éxáḿṕĺé.ćőḿ'
,
# 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'
*
(
account_api
.
EMAIL_MAX_LENGTH
-
11
))
)
]
INVALID_PASSWORDS
=
[
None
,
u''
,
u'a'
,
u'a'
*
(
account_api
.
PASSWORD_MAX_LENGTH
+
1
)
]
def
test_activate_account
(
self
):
# Create the account, which is initially inactive
activation_key
=
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account
=
account_api
.
account_info
(
self
.
USERNAME
)
self
.
assertEqual
(
account
,
{
'username'
:
self
.
USERNAME
,
'email'
:
self
.
EMAIL
,
'is_active'
:
False
})
# Activate the account and verify that it is now active
account_api
.
activate_account
(
activation_key
)
account
=
account_api
.
account_info
(
self
.
USERNAME
)
self
.
assertTrue
(
account
[
'is_active'
])
def
test_create_account_duplicate_username
(
self
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
with
self
.
assertRaises
(
account_api
.
AccountUserAlreadyExists
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
'different+email@example.com'
)
# Email uniqueness constraints were introduced in a database migration,
# which we disable in the unit tests to improve the speed of the test suite.
@skipUnless
(
settings
.
SOUTH_TESTS_MIGRATE
,
"South migrations required"
)
def
test_create_account_duplicate_email
(
self
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
with
self
.
assertRaises
(
account_api
.
AccountUserAlreadyExists
):
account_api
.
create_account
(
'different_user'
,
self
.
PASSWORD
,
self
.
EMAIL
)
def
test_username_too_long
(
self
):
long_username
=
'e'
*
(
account_api
.
USERNAME_MAX_LENGTH
+
1
)
with
self
.
assertRaises
(
account_api
.
AccountUsernameInvalid
):
account_api
.
create_account
(
long_username
,
self
.
PASSWORD
,
self
.
EMAIL
)
def
test_account_info_no_user
(
self
):
self
.
assertIs
(
account_api
.
account_info
(
'does_not_exist'
),
None
)
@raises
(
account_api
.
AccountEmailInvalid
)
@ddt.data
(
*
INVALID_EMAILS
)
def
test_create_account_invalid_email
(
self
,
invalid_email
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
invalid_email
)
@raises
(
account_api
.
AccountPasswordInvalid
)
@ddt.data
(
*
INVALID_PASSWORDS
)
def
test_create_account_invalid_password
(
self
,
invalid_password
):
account_api
.
create_account
(
self
.
USERNAME
,
invalid_password
,
self
.
EMAIL
)
@raises
(
account_api
.
AccountPasswordInvalid
)
def
test_create_account_username_password_equal
(
self
):
# Username and password cannot be the same
account_api
.
create_account
(
self
.
USERNAME
,
self
.
USERNAME
,
self
.
EMAIL
)
@raises
(
account_api
.
AccountRequestError
)
@ddt.data
(
*
INVALID_USERNAMES
)
def
test_create_account_invalid_username
(
self
,
invalid_username
):
account_api
.
create_account
(
invalid_username
,
self
.
PASSWORD
,
self
.
EMAIL
)
@raises
(
account_api
.
AccountNotAuthorized
)
def
test_activate_account_invalid_key
(
self
):
account_api
.
activate_account
(
u'invalid'
)
@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
)
@skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in LMS'
)
def
test_request_password_change_invalid_user
(
self
):
with
self
.
assertRaises
(
account_api
.
AccountUserNotFound
):
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
)
@skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in LMS'
)
def
test_request_password_change_inactive_user
(
self
):
# 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 the activation email was still sent
self
.
assertEqual
(
len
(
mail
.
outbox
),
1
)
def
_assert_is_datetime
(
self
,
timestamp
):
if
not
timestamp
:
return
False
try
:
parse_datetime
(
timestamp
)
except
ValueError
:
return
False
else
:
return
True
openedx/core/djangoapps/user_api/tests/test_models.py
View file @
37dd0ec8
...
@@ -5,6 +5,7 @@ from student.tests.factories import UserFactory
...
@@ -5,6 +5,7 @@ from student.tests.factories import UserFactory
from
..tests.factories
import
UserPreferenceFactory
,
UserCourseTagFactory
,
UserOrgTagFactory
from
..tests.factories
import
UserPreferenceFactory
,
UserCourseTagFactory
,
UserOrgTagFactory
from
..models
import
UserPreference
from
..models
import
UserPreference
from
..preferences.api
import
set_user_preference
class
UserPreferenceModelTest
(
ModuleStoreTestCase
):
class
UserPreferenceModelTest
(
ModuleStoreTestCase
):
...
@@ -67,20 +68,18 @@ class UserPreferenceModelTest(ModuleStoreTestCase):
...
@@ -67,20 +68,18 @@ class UserPreferenceModelTest(ModuleStoreTestCase):
self
.
assertEquals
(
tag
.
value
,
"barfoo"
)
self
.
assertEquals
(
tag
.
value
,
"barfoo"
)
self
.
assertNotEqual
(
original_modified
,
tag
.
modified
)
self
.
assertNotEqual
(
original_modified
,
tag
.
modified
)
def
test_get_set_preference
(
self
):
def
test_get_value
(
self
):
# Checks that you can set a preference and get that preference later
"""Verifies the behavior of get_value."""
# Also, tests that no preference is returned for keys that are not set
user
=
UserFactory
.
create
()
user
=
UserFactory
.
create
()
key
=
'testkey'
key
=
'testkey'
value
=
'testvalue'
value
=
'testvalue'
# does a round trip
# does a round trip
UserPreference
.
set_preference
(
user
,
key
,
value
)
set_user_preference
(
user
,
key
,
value
)
pref
=
UserPreference
.
get_preference
(
user
,
key
)
pref
=
UserPreference
.
get_value
(
user
,
key
)
self
.
assertEqual
(
pref
,
value
)
self
.
assertEqual
(
pref
,
value
)
# get preference for key that doesn't exist for user
# get preference for key that doesn't exist for user
pref
=
UserPreference
.
get_
preferenc
e
(
user
,
'testkey_none'
)
pref
=
UserPreference
.
get_
valu
e
(
user
,
'testkey_none'
)
self
.
assertIsNone
(
pref
)
self
.
assertIsNone
(
pref
)
openedx/core/djangoapps/user_api/tests/test_profile_api.py
deleted
100644 → 0
View file @
ba85f82e
# -*- coding: utf-8 -*-
""" Tests for the profile API. """
from
django.contrib.auth.models
import
User
import
ddt
from
django.test.utils
import
override_settings
from
nose.tools
import
raises
from
dateutil.parser
import
parse
as
parse_datetime
from
pytz
import
UTC
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
import
datetime
from
..accounts.api
import
get_account_settings
from
..api
import
account
as
account_api
from
..api
import
profile
as
profile_api
from
..models
import
UserProfile
,
UserOrgTag
@ddt.ddt
class
ProfileApiTest
(
ModuleStoreTestCase
):
USERNAME
=
u'frank-underwood'
PASSWORD
=
u'ṕáśśẃőŕd'
EMAIL
=
u'frank+underwood@example.com'
def
test_create_profile
(
self
):
# Create a new account, which should have an empty profile by default.
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
# Retrieve the account settings
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
account_settings
=
get_account_settings
(
user
)
# Expect a date joined field but remove it to simplify the following comparison
self
.
assertIsNotNone
(
account_settings
[
'date_joined'
])
del
account_settings
[
'date_joined'
]
# Expect all the values to be defaulted
self
.
assertEqual
(
account_settings
,
{
'username'
:
self
.
USERNAME
,
'email'
:
self
.
EMAIL
,
'name'
:
u''
,
'gender'
:
None
,
'language'
:
u''
,
'goals'
:
None
,
'level_of_education'
:
None
,
'mailing_address'
:
None
,
'year_of_birth'
:
None
,
'country'
:
None
,
})
def
test_update_and_retrieve_preference_info
(
self
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
profile_api
.
update_preferences
(
self
.
USERNAME
,
preference_key
=
'preference_value'
)
preferences
=
profile_api
.
preference_info
(
self
.
USERNAME
)
self
.
assertEqual
(
preferences
[
'preference_key'
],
'preference_value'
)
@ddt.data
(
# Check that a 27 year old can opt-in
(
27
,
True
,
u"True"
),
# Check that a 32-year old can opt-out
(
32
,
False
,
u"False"
),
# Check that someone 14 years old can opt-in
(
14
,
True
,
u"True"
),
# Check that someone 13 years old cannot opt-in (must have turned 13 before this year)
(
13
,
True
,
u"False"
),
# Check that someone 12 years old cannot opt-in
(
12
,
True
,
u"False"
)
)
@ddt.unpack
@override_settings
(
EMAIL_OPTIN_MINIMUM_AGE
=
13
)
def
test_update_email_optin
(
self
,
age
,
option
,
expected_result
):
# Create the course and account.
course
=
CourseFactory
.
create
()
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
# Set year of birth
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
profile
=
UserProfile
.
objects
.
get
(
user
=
user
)
year_of_birth
=
datetime
.
datetime
.
now
()
.
year
-
age
# pylint: disable=maybe-no-member
profile
.
year_of_birth
=
year_of_birth
profile
.
save
()
profile_api
.
update_email_opt_in
(
user
,
course
.
id
.
org
,
option
)
result_obj
=
UserOrgTag
.
objects
.
get
(
user
=
user
,
org
=
course
.
id
.
org
,
key
=
'email-optin'
)
self
.
assertEqual
(
result_obj
.
value
,
expected_result
)
def
test_update_email_optin_no_age_set
(
self
):
# Test that the API still works if no age is specified.
# Create the course and account.
course
=
CourseFactory
.
create
()
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
profile_api
.
update_email_opt_in
(
user
,
course
.
id
.
org
,
True
)
result_obj
=
UserOrgTag
.
objects
.
get
(
user
=
user
,
org
=
course
.
id
.
org
,
key
=
'email-optin'
)
self
.
assertEqual
(
result_obj
.
value
,
u"True"
)
@ddt.data
(
# Check that a 27 year old can opt-in, then out.
(
27
,
True
,
False
,
u"False"
),
# Check that a 32-year old can opt-out, then in.
(
32
,
False
,
True
,
u"True"
),
# Check that someone 13 years old can opt-in, then out.
(
13
,
True
,
False
,
u"False"
),
# Check that someone 12 years old cannot opt-in, then explicitly out.
(
12
,
True
,
False
,
u"False"
)
)
@ddt.unpack
@override_settings
(
EMAIL_OPTIN_MINIMUM_AGE
=
13
)
def
test_change_email_optin
(
self
,
age
,
option
,
second_option
,
expected_result
):
# Create the course and account.
course
=
CourseFactory
.
create
()
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
# Set year of birth
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
profile
=
UserProfile
.
objects
.
get
(
user
=
user
)
year_of_birth
=
datetime
.
datetime
.
now
(
UTC
)
.
year
-
age
# pylint: disable=maybe-no-member
profile
.
year_of_birth
=
year_of_birth
profile
.
save
()
profile_api
.
update_email_opt_in
(
user
,
course
.
id
.
org
,
option
)
profile_api
.
update_email_opt_in
(
user
,
course
.
id
.
org
,
second_option
)
result_obj
=
UserOrgTag
.
objects
.
get
(
user
=
user
,
org
=
course
.
id
.
org
,
key
=
'email-optin'
)
self
.
assertEqual
(
result_obj
.
value
,
expected_result
)
@raises
(
profile_api
.
ProfileUserNotFound
)
def
test_retrieve_and_update_preference_info_no_user
(
self
):
preferences
=
profile_api
.
preference_info
(
self
.
USERNAME
)
self
.
assertEqual
(
preferences
,
{})
profile_api
.
update_preferences
(
self
.
USERNAME
,
preference_key
=
'preference_value'
)
def
test_update_and_retrieve_preference_info_unicode
(
self
):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
profile_api
.
update_preferences
(
self
.
USERNAME
,
**
{
u'ⓟⓡⓔⓕⓔⓡⓔⓝⓒⓔ_ⓚⓔⓨ'
:
u'ǝnןɐʌ_ǝɔuǝɹǝɟǝɹd'
})
preferences
=
profile_api
.
preference_info
(
self
.
USERNAME
)
self
.
assertEqual
(
preferences
[
u'ⓟⓡⓔⓕⓔⓡⓔⓝⓒⓔ_ⓚⓔⓨ'
],
u'ǝnןɐʌ_ǝɔuǝɹǝɟǝɹd'
)
def
_assert_is_datetime
(
self
,
timestamp
):
if
not
timestamp
:
return
False
try
:
parse_datetime
(
timestamp
)
except
ValueError
:
return
False
else
:
return
True
openedx/core/djangoapps/user_api/tests/test_views.py
View file @
37dd0ec8
...
@@ -25,7 +25,10 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
...
@@ -25,7 +25,10 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from
third_party_auth.tests.testutil
import
simulate_running_pipeline
from
third_party_auth.tests.testutil
import
simulate_running_pipeline
from
..accounts.api
import
get_account_settings
from
..accounts.api
import
get_account_settings
from
..api
import
account
as
account_api
,
profile
as
profile_api
from
..accounts
import
(
NAME_MAX_LENGTH
,
EMAIL_MIN_LENGTH
,
EMAIL_MAX_LENGTH
,
PASSWORD_MIN_LENGTH
,
PASSWORD_MAX_LENGTH
,
USERNAME_MIN_LENGTH
,
USERNAME_MAX_LENGTH
)
from
..models
import
UserOrgTag
from
..models
import
UserOrgTag
from
..tests.factories
import
UserPreferenceFactory
from
..tests.factories
import
UserPreferenceFactory
from
..tests.test_constants
import
SORTED_COUNTRIES
from
..tests.test_constants
import
SORTED_COUNTRIES
...
@@ -618,8 +621,8 @@ class LoginSessionViewTest(ApiTestCase):
...
@@ -618,8 +621,8 @@ class LoginSessionViewTest(ApiTestCase):
platform_name
=
settings
.
PLATFORM_NAME
platform_name
=
settings
.
PLATFORM_NAME
),
),
"restrictions"
:
{
"restrictions"
:
{
"min_length"
:
account_api
.
EMAIL_MIN_LENGTH
,
"min_length"
:
EMAIL_MIN_LENGTH
,
"max_length"
:
account_api
.
EMAIL_MAX_LENGTH
"max_length"
:
EMAIL_MAX_LENGTH
},
},
"errorMessages"
:
{},
"errorMessages"
:
{},
},
},
...
@@ -632,8 +635,8 @@ class LoginSessionViewTest(ApiTestCase):
...
@@ -632,8 +635,8 @@ class LoginSessionViewTest(ApiTestCase):
"placeholder"
:
""
,
"placeholder"
:
""
,
"instructions"
:
""
,
"instructions"
:
""
,
"restrictions"
:
{
"restrictions"
:
{
"min_length"
:
account_api
.
PASSWORD_MIN_LENGTH
,
"min_length"
:
PASSWORD_MIN_LENGTH
,
"max_length"
:
account_api
.
PASSWORD_MAX_LENGTH
"max_length"
:
PASSWORD_MAX_LENGTH
},
},
"errorMessages"
:
{},
"errorMessages"
:
{},
}
}
...
@@ -769,8 +772,8 @@ class PasswordResetViewTest(ApiTestCase):
...
@@ -769,8 +772,8 @@ class PasswordResetViewTest(ApiTestCase):
platform_name
=
settings
.
PLATFORM_NAME
platform_name
=
settings
.
PLATFORM_NAME
),
),
"restrictions"
:
{
"restrictions"
:
{
"min_length"
:
account_api
.
EMAIL_MIN_LENGTH
,
"min_length"
:
EMAIL_MIN_LENGTH
,
"max_length"
:
account_api
.
EMAIL_MAX_LENGTH
"max_length"
:
EMAIL_MAX_LENGTH
},
},
"errorMessages"
:
{},
"errorMessages"
:
{},
}
}
...
@@ -827,8 +830,8 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -827,8 +830,8 @@ class RegistrationViewTest(ApiTestCase):
u"label"
:
u"Email"
,
u"label"
:
u"Email"
,
u"placeholder"
:
u"username@domain.com"
,
u"placeholder"
:
u"username@domain.com"
,
u"restrictions"
:
{
u"restrictions"
:
{
"min_length"
:
account_api
.
EMAIL_MIN_LENGTH
,
"min_length"
:
EMAIL_MIN_LENGTH
,
"max_length"
:
account_api
.
EMAIL_MAX_LENGTH
"max_length"
:
EMAIL_MAX_LENGTH
},
},
}
}
)
)
...
@@ -842,7 +845,7 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -842,7 +845,7 @@ class RegistrationViewTest(ApiTestCase):
u"label"
:
u"Full name"
,
u"label"
:
u"Full name"
,
u"instructions"
:
u"The name that will appear on your certificates"
,
u"instructions"
:
u"The name that will appear on your certificates"
,
u"restrictions"
:
{
u"restrictions"
:
{
"max_length"
:
profile_api
.
FULL_
NAME_MAX_LENGTH
,
"max_length"
:
NAME_MAX_LENGTH
,
},
},
}
}
)
)
...
@@ -856,8 +859,8 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -856,8 +859,8 @@ class RegistrationViewTest(ApiTestCase):
u"label"
:
u"Public username"
,
u"label"
:
u"Public username"
,
u"instructions"
:
u"The name that will identify you in your courses"
,
u"instructions"
:
u"The name that will identify you in your courses"
,
u"restrictions"
:
{
u"restrictions"
:
{
"min_length"
:
account_api
.
USERNAME_MIN_LENGTH
,
"min_length"
:
USERNAME_MIN_LENGTH
,
"max_length"
:
account_api
.
USERNAME_MAX_LENGTH
"max_length"
:
USERNAME_MAX_LENGTH
},
},
}
}
)
)
...
@@ -870,8 +873,8 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -870,8 +873,8 @@ class RegistrationViewTest(ApiTestCase):
u"required"
:
True
,
u"required"
:
True
,
u"label"
:
u"Password"
,
u"label"
:
u"Password"
,
u"restrictions"
:
{
u"restrictions"
:
{
"min_length"
:
account_api
.
PASSWORD_MIN_LENGTH
,
"min_length"
:
PASSWORD_MIN_LENGTH
,
"max_length"
:
account_api
.
PASSWORD_MAX_LENGTH
"max_length"
:
PASSWORD_MAX_LENGTH
},
},
}
}
)
)
...
@@ -905,8 +908,8 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -905,8 +908,8 @@ class RegistrationViewTest(ApiTestCase):
u"label"
:
u"Email"
,
u"label"
:
u"Email"
,
u"placeholder"
:
u"username@domain.com"
,
u"placeholder"
:
u"username@domain.com"
,
u"restrictions"
:
{
u"restrictions"
:
{
"min_length"
:
account_api
.
EMAIL_MIN_LENGTH
,
"min_length"
:
EMAIL_MIN_LENGTH
,
"max_length"
:
account_api
.
EMAIL_MAX_LENGTH
"max_length"
:
EMAIL_MAX_LENGTH
},
},
}
}
)
)
...
@@ -922,7 +925,7 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -922,7 +925,7 @@ class RegistrationViewTest(ApiTestCase):
u"label"
:
u"Full name"
,
u"label"
:
u"Full name"
,
u"instructions"
:
u"The name that will appear on your certificates"
,
u"instructions"
:
u"The name that will appear on your certificates"
,
u"restrictions"
:
{
u"restrictions"
:
{
"max_length"
:
profile_api
.
FULL_
NAME_MAX_LENGTH
,
"max_length"
:
NAME_MAX_LENGTH
,
}
}
}
}
)
)
...
@@ -939,8 +942,8 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -939,8 +942,8 @@ class RegistrationViewTest(ApiTestCase):
u"placeholder"
:
u""
,
u"placeholder"
:
u""
,
u"instructions"
:
u"The name that will identify you in your courses"
,
u"instructions"
:
u"The name that will identify you in your courses"
,
u"restrictions"
:
{
u"restrictions"
:
{
"min_length"
:
account_api
.
USERNAME_MIN_LENGTH
,
"min_length"
:
USERNAME_MIN_LENGTH
,
"max_length"
:
account_api
.
USERNAME_MAX_LENGTH
"max_length"
:
USERNAME_MAX_LENGTH
}
}
}
}
)
)
...
@@ -1237,20 +1240,13 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -1237,20 +1240,13 @@ class RegistrationViewTest(ApiTestCase):
self
.
assertHttpOK
(
response
)
self
.
assertHttpOK
(
response
)
self
.
assertIn
(
settings
.
EDXMKTG_COOKIE_NAME
,
self
.
client
.
cookies
)
self
.
assertIn
(
settings
.
EDXMKTG_COOKIE_NAME
,
self
.
client
.
cookies
)
# Verify that the user exists
self
.
assertEqual
(
account_api
.
account_info
(
self
.
USERNAME
),
{
"username"
:
self
.
USERNAME
,
"email"
:
self
.
EMAIL
,
"is_active"
:
False
}
)
# Verify that the user's full name is set
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
account_settings
=
get_account_settings
(
user
)
account_settings
=
get_account_settings
(
user
)
self
.
assertEqual
(
account_settings
[
"name"
],
self
.
NAME
)
self
.
assertEqual
(
self
.
USERNAME
,
account_settings
[
"username"
])
self
.
assertEqual
(
self
.
EMAIL
,
account_settings
[
"email"
])
self
.
assertFalse
(
account_settings
[
"is_active"
])
self
.
assertEqual
(
self
.
NAME
,
account_settings
[
"name"
])
# Verify that we've been logged in
# Verify that we've been logged in
# by trying to access a page that requires authentication
# by trying to access a page that requires authentication
...
...
openedx/core/djangoapps/user_api/urls.py
View file @
37dd0ec8
...
@@ -3,6 +3,7 @@ Defines the URL routes for this app.
...
@@ -3,6 +3,7 @@ Defines the URL routes for this app.
"""
"""
from
.accounts.views
import
AccountView
from
.accounts.views
import
AccountView
from
.preferences.views
import
PreferencesView
,
PreferencesDetailView
from
django.conf.urls
import
patterns
,
url
from
django.conf.urls
import
patterns
,
url
...
@@ -15,4 +16,14 @@ urlpatterns = patterns(
...
@@ -15,4 +16,14 @@ urlpatterns = patterns(
AccountView
.
as_view
(),
AccountView
.
as_view
(),
name
=
"accounts_api"
name
=
"accounts_api"
),
),
url
(
r'^v0/preferences/'
+
USERNAME_PATTERN
+
'$'
,
PreferencesView
.
as_view
(),
name
=
"preferences_api"
),
url
(
r'^v0/preferences/'
+
USERNAME_PATTERN
+
'/(?P<preference_key>[a-zA-Z0-9_]+)$'
,
PreferencesDetailView
.
as_view
(),
name
=
"preferences_detail_api"
),
)
)
openedx/core/djangoapps/user_api/views.py
View file @
37dd0ec8
...
@@ -28,9 +28,14 @@ from edxmako.shortcuts import marketing_link
...
@@ -28,9 +28,14 @@ from edxmako.shortcuts import marketing_link
from
student.views
import
create_account_with_params
,
set_marketing_cookie
from
student.views
import
create_account_with_params
,
set_marketing_cookie
from
util.authentication
import
SessionAuthenticationAllowInactiveUser
from
util.authentication
import
SessionAuthenticationAllowInactiveUser
from
util.json_request
import
JsonResponse
from
util.json_request
import
JsonResponse
from
.
api
import
account
as
account_api
,
profile
as
profile_api
from
.
preferences.api
import
update_email_opt_in
from
.helpers
import
FormDescription
,
shim_student_view
,
require_post_params
from
.helpers
import
FormDescription
,
shim_student_view
,
require_post_params
from
.models
import
UserPreference
,
UserProfile
from
.models
import
UserPreference
,
UserProfile
from
.accounts
import
(
NAME_MAX_LENGTH
,
EMAIL_MIN_LENGTH
,
EMAIL_MAX_LENGTH
,
PASSWORD_MIN_LENGTH
,
PASSWORD_MAX_LENGTH
,
USERNAME_MIN_LENGTH
,
USERNAME_MAX_LENGTH
)
from
.accounts.api
import
check_account_exists
from
.serializers
import
UserSerializer
,
UserPreferenceSerializer
from
.serializers
import
UserSerializer
,
UserPreferenceSerializer
...
@@ -79,8 +84,8 @@ class LoginSessionView(APIView):
...
@@ -79,8 +84,8 @@ class LoginSessionView(APIView):
placeholder
=
email_placeholder
,
placeholder
=
email_placeholder
,
instructions
=
email_instructions
,
instructions
=
email_instructions
,
restrictions
=
{
restrictions
=
{
"min_length"
:
account_api
.
EMAIL_MIN_LENGTH
,
"min_length"
:
EMAIL_MIN_LENGTH
,
"max_length"
:
account_api
.
EMAIL_MAX_LENGTH
,
"max_length"
:
EMAIL_MAX_LENGTH
,
}
}
)
)
...
@@ -93,8 +98,8 @@ class LoginSessionView(APIView):
...
@@ -93,8 +98,8 @@ class LoginSessionView(APIView):
label
=
password_label
,
label
=
password_label
,
field_type
=
"password"
,
field_type
=
"password"
,
restrictions
=
{
restrictions
=
{
"min_length"
:
account_api
.
PASSWORD_MIN_LENGTH
,
"min_length"
:
PASSWORD_MIN_LENGTH
,
"max_length"
:
account_api
.
PASSWORD_MAX_LENGTH
,
"max_length"
:
PASSWORD_MAX_LENGTH
,
}
}
)
)
...
@@ -251,7 +256,7 @@ class RegistrationView(APIView):
...
@@ -251,7 +256,7 @@ class RegistrationView(APIView):
username
=
data
.
get
(
'username'
)
username
=
data
.
get
(
'username'
)
# Handle duplicate email/username
# Handle duplicate email/username
conflicts
=
account_api
.
check_account_exists
(
email
=
email
,
username
=
username
)
conflicts
=
check_account_exists
(
email
=
email
,
username
=
username
)
if
conflicts
:
if
conflicts
:
conflict_messages
=
{
conflict_messages
=
{
# Translators: This message is shown to users who attempt to create a new
# Translators: This message is shown to users who attempt to create a new
...
@@ -321,8 +326,8 @@ class RegistrationView(APIView):
...
@@ -321,8 +326,8 @@ class RegistrationView(APIView):
label
=
email_label
,
label
=
email_label
,
placeholder
=
email_placeholder
,
placeholder
=
email_placeholder
,
restrictions
=
{
restrictions
=
{
"min_length"
:
account_api
.
EMAIL_MIN_LENGTH
,
"min_length"
:
EMAIL_MIN_LENGTH
,
"max_length"
:
account_api
.
EMAIL_MAX_LENGTH
,
"max_length"
:
EMAIL_MAX_LENGTH
,
},
},
required
=
required
required
=
required
)
)
...
@@ -350,7 +355,7 @@ class RegistrationView(APIView):
...
@@ -350,7 +355,7 @@ class RegistrationView(APIView):
label
=
name_label
,
label
=
name_label
,
instructions
=
name_instructions
,
instructions
=
name_instructions
,
restrictions
=
{
restrictions
=
{
"max_length"
:
profile_api
.
FULL_
NAME_MAX_LENGTH
,
"max_length"
:
NAME_MAX_LENGTH
,
},
},
required
=
required
required
=
required
)
)
...
@@ -380,8 +385,8 @@ class RegistrationView(APIView):
...
@@ -380,8 +385,8 @@ class RegistrationView(APIView):
label
=
username_label
,
label
=
username_label
,
instructions
=
username_instructions
,
instructions
=
username_instructions
,
restrictions
=
{
restrictions
=
{
"min_length"
:
account_api
.
USERNAME_MIN_LENGTH
,
"min_length"
:
USERNAME_MIN_LENGTH
,
"max_length"
:
account_api
.
USERNAME_MAX_LENGTH
,
"max_length"
:
USERNAME_MAX_LENGTH
,
},
},
required
=
required
required
=
required
)
)
...
@@ -405,8 +410,8 @@ class RegistrationView(APIView):
...
@@ -405,8 +410,8 @@ class RegistrationView(APIView):
label
=
password_label
,
label
=
password_label
,
field_type
=
"password"
,
field_type
=
"password"
,
restrictions
=
{
restrictions
=
{
"min_length"
:
account_api
.
PASSWORD_MIN_LENGTH
,
"min_length"
:
PASSWORD_MIN_LENGTH
,
"max_length"
:
account_api
.
PASSWORD_MAX_LENGTH
,
"max_length"
:
PASSWORD_MAX_LENGTH
,
},
},
required
=
required
required
=
required
)
)
...
@@ -775,8 +780,8 @@ class PasswordResetView(APIView):
...
@@ -775,8 +780,8 @@ class PasswordResetView(APIView):
placeholder
=
email_placeholder
,
placeholder
=
email_placeholder
,
instructions
=
email_instructions
,
instructions
=
email_instructions
,
restrictions
=
{
restrictions
=
{
"min_length"
:
account_api
.
EMAIL_MIN_LENGTH
,
"min_length"
:
EMAIL_MIN_LENGTH
,
"max_length"
:
account_api
.
EMAIL_MAX_LENGTH
,
"max_length"
:
EMAIL_MAX_LENGTH
,
}
}
)
)
...
@@ -870,5 +875,5 @@ class UpdateEmailOptInPreference(APIView):
...
@@ -870,5 +875,5 @@ class UpdateEmailOptInPreference(APIView):
)
)
# Only check for true. All other values are False.
# Only check for true. All other values are False.
email_opt_in
=
request
.
DATA
[
'email_opt_in'
]
.
lower
()
==
'true'
email_opt_in
=
request
.
DATA
[
'email_opt_in'
]
.
lower
()
==
'true'
profile_api
.
update_email_opt_in
(
request
.
user
,
org
,
email_opt_in
)
update_email_opt_in
(
request
.
user
,
org
,
email_opt_in
)
return
HttpResponse
(
status
=
status
.
HTTP_200_OK
)
return
HttpResponse
(
status
=
status
.
HTTP_200_OK
)
openedx/core/lib/api/permissions.py
View file @
37dd0ec8
...
@@ -54,17 +54,7 @@ class IsUserInUrl(permissions.BasePermission):
...
@@ -54,17 +54,7 @@ class IsUserInUrl(permissions.BasePermission):
def
has_permission
(
self
,
request
,
view
):
def
has_permission
(
self
,
request
,
view
):
# Return a 404 instead of a 403 (Unauthorized). If one user is looking up
# Return a 404 instead of a 403 (Unauthorized). If one user is looking up
# other users, do not let them deduce the existence of an account.
# other users, do not let them deduce the existence of an account.
if
request
.
user
.
username
!=
request
.
parser_context
.
get
(
'kwargs'
,
{})
.
get
(
'username'
,
None
):
url_username
=
request
.
parser_context
.
get
(
'kwargs'
,
{})
.
get
(
'username'
,
''
)
if
request
.
user
.
username
.
lower
()
!=
url_username
.
lower
():
raise
Http404
()
raise
Http404
()
return
True
return
True
class
IsUserInUrlOrStaff
(
IsUserInUrl
):
"""
Permission that checks to see if the request user matches the user in the URL or has is_staff access.
"""
def
has_permission
(
self
,
request
,
view
):
if
request
.
user
.
is_staff
:
return
True
return
super
(
IsUserInUrlOrStaff
,
self
)
.
has_permission
(
request
,
view
)
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