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
c8a20df2
Commit
c8a20df2
authored
Mar 05, 2015
by
cahrens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Combine account and profile into same API.
parent
450d9e37
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
251 additions
and
393 deletions
+251
-393
common/djangoapps/student/tests/test_enrollment.py
+1
-1
common/djangoapps/student/views.py
+3
-3
common/djangoapps/third_party_auth/pipeline.py
+1
-1
lms/djangoapps/verify_student/tests/test_views.py
+1
-1
lms/djangoapps/verify_student/views.py
+2
-4
lms/envs/common.py
+5
-5
openedx/core/djangoapps/user_api/accounts/__init__.py
+12
-0
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+132
-59
openedx/core/djangoapps/user_api/accounts/views.py
+79
-27
openedx/core/djangoapps/user_api/api/profile.py
+3
-7
openedx/core/djangoapps/user_api/management/tests/test_email_opt_in_list.py
+1
-1
openedx/core/djangoapps/user_api/profiles/__init__.py
+0
-11
openedx/core/djangoapps/user_api/profiles/tests/__init__.py
+0
-0
openedx/core/djangoapps/user_api/profiles/tests/test_views.py
+0
-137
openedx/core/djangoapps/user_api/profiles/views.py
+0
-123
openedx/core/djangoapps/user_api/tests/test_profile_api.py
+6
-5
openedx/core/djangoapps/user_api/tests/test_views.py
+5
-2
openedx/core/djangoapps/user_api/urls.py
+0
-6
No files found.
common/djangoapps/student/tests/test_enrollment.py
View file @
c8a20df2
...
@@ -130,7 +130,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
...
@@ -130,7 +130,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
# Verify that the profile API has been called as expected
# Verify that the profile API has been called as expected
if
email_opt_in
is
not
None
:
if
email_opt_in
is
not
None
:
opt_in
=
email_opt_in
==
'true'
opt_in
=
email_opt_in
==
'true'
mock_update_email_opt_in
.
assert_called_once_with
(
self
.
USERNAME
,
self
.
course
.
org
,
opt_in
)
mock_update_email_opt_in
.
assert_called_once_with
(
self
.
user
,
self
.
course
.
org
,
opt_in
)
else
:
else
:
self
.
assertFalse
(
mock_update_email_opt_in
.
called
)
self
.
assertFalse
(
mock_update_email_opt_in
.
called
)
...
...
common/djangoapps/student/views.py
View file @
c8a20df2
...
@@ -797,7 +797,7 @@ def try_change_enrollment(request):
...
@@ -797,7 +797,7 @@ def try_change_enrollment(request):
log
.
exception
(
u"Exception automatically enrolling after login:
%
s"
,
exc
)
log
.
exception
(
u"Exception automatically enrolling after login:
%
s"
,
exc
)
def
_update_email_opt_in
(
request
,
username
,
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
# TODO: remove circular dependency on openedx from common
...
@@ -806,7 +806,7 @@ def _update_email_opt_in(request, username, org):
...
@@ -806,7 +806,7 @@ def _update_email_opt_in(request, username, org):
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'
profile_api
.
update_email_opt_in
(
username
,
org
,
email_opt_in_boolean
)
profile_api
.
update_email_opt_in
(
request
.
user
,
org
,
email_opt_in_boolean
)
@require_POST
@require_POST
...
@@ -878,7 +878,7 @@ def change_enrollment(request, check_access=True):
...
@@ -878,7 +878,7 @@ def change_enrollment(request, check_access=True):
# Record the user's email opt-in preference
# Record the user's email opt-in preference
if
settings
.
FEATURES
.
get
(
'ENABLE_MKTG_EMAIL_OPT_IN'
):
if
settings
.
FEATURES
.
get
(
'ENABLE_MKTG_EMAIL_OPT_IN'
):
_update_email_opt_in
(
request
,
user
.
username
,
course_id
.
org
)
_update_email_opt_in
(
request
,
course_id
.
org
)
available_modes
=
CourseMode
.
modes_for_course_dict
(
course_id
)
available_modes
=
CourseMode
.
modes_for_course_dict
(
course_id
)
...
...
common/djangoapps/third_party_auth/pipeline.py
View file @
c8a20df2
...
@@ -672,7 +672,7 @@ def change_enrollment(strategy, user=None, is_dashboard=False, *args, **kwargs):
...
@@ -672,7 +672,7 @@ def change_enrollment(strategy, user=None, is_dashboard=False, *args, **kwargs):
# TODO: remove circular dependency on openedx from common
# TODO: remove circular dependency on openedx from common
from
openedx.core.djangoapps.user_api.api
import
profile
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
.
username
,
course_id
.
org
,
opt_in
)
profile
.
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/verify_student/tests/test_views.py
View file @
c8a20df2
...
@@ -1149,7 +1149,7 @@ class TestSubmitPhotosForVerification(TestCase):
...
@@ -1149,7 +1149,7 @@ class TestSubmitPhotosForVerification(TestCase):
AssertionError
AssertionError
"""
"""
account_settings
=
AccountView
.
get_serialized_account
(
self
.
user
.
username
)
account_settings
=
AccountView
.
get_serialized_account
(
self
.
user
)
self
.
assertEqual
(
account_settings
[
'name'
],
full_name
)
self
.
assertEqual
(
account_settings
[
'name'
],
full_name
)
...
...
lms/djangoapps/verify_student/views.py
View file @
c8a20df2
...
@@ -714,13 +714,11 @@ def submit_photos_for_verification(request):
...
@@ -714,13 +714,11 @@ def submit_photos_for_verification(request):
if
SoftwareSecurePhotoVerification
.
user_has_valid_or_pending
(
request
.
user
):
if
SoftwareSecurePhotoVerification
.
user_has_valid_or_pending
(
request
.
user
):
return
HttpResponseBadRequest
(
_
(
"You already have a valid or pending verification."
))
return
HttpResponseBadRequest
(
_
(
"You already have a valid or pending verification."
))
username
=
request
.
user
.
username
# If the user wants to change his/her full name,
# If the user wants to change his/her full name,
# then try to do that before creating the attempt.
# then try to do that before creating the attempt.
if
request
.
POST
.
get
(
'full_name'
):
if
request
.
POST
.
get
(
'full_name'
):
try
:
try
:
AccountView
.
update_account
(
username
,
{
"name"
:
request
.
POST
.
get
(
'full_name'
)})
AccountView
.
update_account
(
request
.
user
,
{
"name"
:
request
.
POST
.
get
(
'full_name'
)})
except
AccountUserNotFound
:
except
AccountUserNotFound
:
return
HttpResponseBadRequest
(
_
(
"No profile found for user"
))
return
HttpResponseBadRequest
(
_
(
"No profile found for user"
))
except
AccountUpdateError
:
except
AccountUpdateError
:
...
@@ -743,7 +741,7 @@ def submit_photos_for_verification(request):
...
@@ -743,7 +741,7 @@ def submit_photos_for_verification(request):
attempt
.
mark_ready
()
attempt
.
mark_ready
()
attempt
.
submit
()
attempt
.
submit
()
account_settings
=
AccountView
.
get_serialized_account
(
username
)
account_settings
=
AccountView
.
get_serialized_account
(
request
.
user
)
# Send a confirmation email to the user
# Send a confirmation email to the user
context
=
{
context
=
{
...
...
lms/envs/common.py
View file @
c8a20df2
...
@@ -2047,14 +2047,14 @@ SEARCH_ENGINE = None
...
@@ -2047,14 +2047,14 @@ SEARCH_ENGINE = None
# Use the LMS specific result processor
# Use the LMS specific result processor
SEARCH_RESULT_PROCESSOR
=
"lms.lib.courseware_search.lms_result_processor.LmsSearchResultProcessor"
SEARCH_RESULT_PROCESSOR
=
"lms.lib.courseware_search.lms_result_processor.LmsSearchResultProcessor"
# The configuration
for learner profiles
# The configuration
visibility of account fields.
PROFILE
_CONFIGURATION
=
{
ACCOUNT_VISIBILITY
_CONFIGURATION
=
{
# Default visibility level for accounts without a specified value
# Default visibility level for accounts without a specified value
# The value is one of: 'all_users', 'private'
# The value is one of: 'all_users', 'private'
"default_visibility"
:
"private"
,
"default_visibility"
:
"private"
,
# The list of all fields that can be sh
own on a learner's profile
# The list of all fields that can be sh
ared with other users
"
all
_fields"
:
[
"
shareable
_fields"
:
[
'username'
,
'username'
,
'profile_image'
,
'profile_image'
,
'country'
,
'country'
,
...
@@ -2063,7 +2063,7 @@ PROFILE_CONFIGURATION = {
...
@@ -2063,7 +2063,7 @@ PROFILE_CONFIGURATION = {
'bio'
,
'bio'
,
],
],
# The list of
fields that are always public on a learner's profile
# The list of
account fields that are always public
"public_fields"
:
[
"public_fields"
:
[
'username'
,
'username'
,
'profile_image'
,
'profile_image'
,
...
...
openedx/core/djangoapps/user_api/accounts/__init__.py
View file @
c8a20df2
"""
Account constants
"""
# The minimum acceptable length for the name account field
# The minimum acceptable length for the name account field
NAME_MIN_LENGTH
=
2
NAME_MIN_LENGTH
=
2
ACCOUNT_VISIBILITY_PREF_KEY
=
'account_privacy'
# Indicates the user's preference that all users can view the shareable fields in their account information.
ALL_USERS_VISIBILITY
=
'all_users'
# Indicates the user's preference that all their account information be private.
PRIVATE_VISIBILITY
=
'private'
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
View file @
c8a20df2
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
import
unittest
import
unittest
import
ddt
import
ddt
import
json
import
json
from
mock
import
patch
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
...
@@ -9,6 +10,9 @@ from rest_framework.test import APITestCase, APIClient
...
@@ -9,6 +10,9 @@ 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.models
import
UserPreference
from
..
import
PRIVATE_VISIBILITY
,
ALL_USERS_VISIBILITY
TEST_PASSWORD
=
"test"
TEST_PASSWORD
=
"test"
...
@@ -73,24 +77,66 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -73,24 +77,66 @@ class TestAccountAPI(UserAPITestCase):
"""
"""
Unit tests for the Account API.
Unit tests for the Account API.
"""
"""
def
setUp
(
self
):
def
setUp
(
self
):
super
(
TestAccountAPI
,
self
)
.
setUp
()
super
(
TestAccountAPI
,
self
)
.
setUp
()
self
.
url
=
reverse
(
"accounts_api"
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
self
.
url
=
reverse
(
"accounts_api"
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
def
test_get_account_anonymous_user
(
self
):
def
_verify_full_shareable_account_response
(
self
,
response
):
"""
Verify that the shareable fields from the account are returned
"""
data
=
response
.
data
self
.
assertEqual
(
6
,
len
(
data
))
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertEqual
(
"US"
,
data
[
"country"
])
self
.
assertIsNone
(
data
[
"profile_image"
])
self
.
assertIsNone
(
data
[
"time_zone"
])
self
.
assertIsNone
(
data
[
"languages"
])
self
.
assertIsNone
(
data
[
"bio"
])
def
_verify_private_account_response
(
self
,
response
):
"""
Verify that only the public fields are returned if a user does not want to share account fields
"""
data
=
response
.
data
self
.
assertEqual
(
2
,
len
(
data
))
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertIsNone
(
data
[
"profile_image"
])
def
_verify_full_account_response
(
self
,
response
):
"""
Verify that all account fields are returned (even those that are not shareable).
"""
data
=
response
.
data
self
.
assertEqual
(
11
,
len
(
data
))
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertEqual
(
self
.
user
.
first_name
+
" "
+
self
.
user
.
last_name
,
data
[
"name"
])
self
.
assertEqual
(
"US"
,
data
[
"country"
])
self
.
assertEqual
(
""
,
data
[
"language"
])
self
.
assertEqual
(
"m"
,
data
[
"gender"
])
self
.
assertEqual
(
1900
,
data
[
"year_of_birth"
])
self
.
assertEqual
(
"m"
,
data
[
"level_of_education"
])
self
.
assertEqual
(
"world peace"
,
data
[
"goals"
])
self
.
assertEqual
(
"Park Ave"
,
data
[
'mailing_address'
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
def
test_anonymous_access
(
self
):
"""
"""
Test that an anonymous client (not logged in) cannot call
get
.
Test that an anonymous client (not logged in) cannot call
GET or PATCH
.
"""
"""
self
.
send_get
(
self
.
anonymous_client
,
expected_status
=
401
)
self
.
send_get
(
self
.
anonymous_client
,
expected_status
=
401
)
self
.
send_patch
(
self
.
anonymous_client
,
{},
expected_status
=
401
)
def
test_
get_account_different_user
(
self
):
def
test_
unsupported_methods
(
self
):
"""
"""
Test that
a client (logged in) cannot get the account information for a different client
.
Test that
DELETE, POST, and PUT are not supported
.
"""
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
TEST_PASSWORD
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
self
.
send_get
(
self
.
different_client
,
expected_status
=
404
)
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
)
@ddt.data
(
@ddt.data
(
(
"client"
,
"user"
),
(
"client"
,
"user"
),
...
@@ -105,6 +151,69 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -105,6 +151,69 @@ class TestAccountAPI(UserAPITestCase):
response
=
client
.
get
(
reverse
(
"accounts_api"
,
kwargs
=
{
'username'
:
"does_not_exist"
}))
response
=
client
.
get
(
reverse
(
"accounts_api"
,
kwargs
=
{
'username'
:
"does_not_exist"
}))
self
.
assertEqual
(
404
,
response
.
status_code
)
self
.
assertEqual
(
404
,
response
.
status_code
)
# Note: using getattr so that the patching works even if there is no configuration.
# This is needed when testing CMS as the patching is still executed even though the
# suite is skipped.
@patch.dict
(
getattr
(
settings
,
"ACCOUNT_VISIBILITY_CONFIGURATION"
,
{}),
{
"default_visibility"
:
"all_users"
})
def
test_get_account_different_user_visible
(
self
):
"""
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".
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
TEST_PASSWORD
)
self
.
create_mock_profile
(
self
.
user
)
response
=
self
.
send_get
(
self
.
different_client
)
self
.
_verify_full_shareable_account_response
(
response
)
# Note: using getattr so that the patching works even if there is no configuration.
# This is needed when testing CMS as the patching is still executed even though the
# suite is skipped.
@patch.dict
(
getattr
(
settings
,
"ACCOUNT_VISIBILITY_CONFIGURATION"
,
{}),
{
"default_visibility"
:
"private"
})
def
test_get_account_different_user_private
(
self
):
"""
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".
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
TEST_PASSWORD
)
self
.
create_mock_profile
(
self
.
user
)
response
=
self
.
send_get
(
self
.
different_client
)
self
.
_verify_private_account_response
(
response
)
@ddt.data
(
(
"client"
,
"user"
,
PRIVATE_VISIBILITY
),
(
"different_client"
,
"different_user"
,
PRIVATE_VISIBILITY
),
(
"staff_client"
,
"staff_user"
,
PRIVATE_VISIBILITY
),
(
"client"
,
"user"
,
ALL_USERS_VISIBILITY
),
(
"different_client"
,
"different_user"
,
ALL_USERS_VISIBILITY
),
(
"staff_client"
,
"staff_user"
,
ALL_USERS_VISIBILITY
),
)
@ddt.unpack
def
test_get_account_private_visibility
(
self
,
api_client
,
requesting_username
,
preference_visibility
):
"""
Test the return from GET based on user visibility setting.
"""
def
verify_fields_visible_to_all_users
(
response
):
if
preference_visibility
==
PRIVATE_VISIBILITY
:
self
.
_verify_private_account_response
(
response
)
else
:
self
.
_verify_full_shareable_account_response
(
response
)
client
=
self
.
login_client
(
api_client
,
requesting_username
)
# Update user account visibility setting.
UserPreference
.
set_preference
(
self
.
user
,
ACCOUNT_VISIBILITY_PREF_KEY
,
preference_visibility
)
self
.
create_mock_profile
(
self
.
user
)
response
=
self
.
send_get
(
client
)
if
requesting_username
==
"different_user"
:
verify_fields_visible_to_all_users
(
response
)
else
:
self
.
_verify_full_account_response
(
response
)
# Verify how the view parameter changes the fields that are returned.
response
=
self
.
send_get
(
client
,
query_parameters
=
'view=shared'
)
verify_fields_visible_to_all_users
(
response
)
def
test_get_account_default
(
self
):
def
test_get_account_default
(
self
):
"""
"""
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,
...
@@ -126,33 +235,6 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -126,33 +235,6 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_get_account
(
self
,
api_client
,
user
):
"""
Test that a client (logged in) can get her own account information. Also verifies that a "is_staff"
user can get the account information for other users.
"""
self
.
create_mock_profile
(
self
.
user
)
client
=
self
.
login_client
(
api_client
,
user
)
response
=
self
.
send_get
(
client
)
data
=
response
.
data
self
.
assertEqual
(
11
,
len
(
data
))
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertEqual
(
self
.
user
.
first_name
+
" "
+
self
.
user
.
last_name
,
data
[
"name"
])
self
.
assertEqual
(
"US"
,
data
[
"country"
])
self
.
assertEqual
(
""
,
data
[
"language"
])
self
.
assertEqual
(
"m"
,
data
[
"gender"
])
self
.
assertEqual
(
1900
,
data
[
"year_of_birth"
])
self
.
assertEqual
(
"m"
,
data
[
"level_of_education"
])
self
.
assertEqual
(
"world peace"
,
data
[
"goals"
])
self
.
assertEqual
(
"Park Ave"
,
data
[
'mailing_address'
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
def
test_get_account_empty_string
(
self
):
def
test_get_account_empty_string
(
self
):
"""
"""
Test the conversion of empty strings to None for certain fields.
Test the conversion of empty strings to None for certain fields.
...
@@ -168,25 +250,18 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -168,25 +250,18 @@ class TestAccountAPI(UserAPITestCase):
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
])
def
test_patch_account_anonymous_user
(
self
):
@ddt.data
(
"""
(
"different_client"
,
"different_user"
),
Test that an anonymous client (not logged in) cannot call patch.
(
"staff_client"
,
"staff_user"
),
"""
)
self
.
send_patch
(
self
.
anonymous_client
,
{},
expected_status
=
401
)
@ddt.unpack
def
test_patch_account_disallowed_user
(
self
,
api_client
,
user
):
def
test_patch_account_different_user
(
self
):
"""
Test that a client (logged in) cannot update the account information for a different client.
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
TEST_PASSWORD
)
self
.
send_patch
(
self
.
different_client
,
{},
expected_status
=
404
)
def
test_patch_account_is_staff
(
self
):
"""
"""
Test that a client (logged in) with is_staff privileges cannot account settings for other users.
Test that a client cannot call PATCH on a different client's user account (even with
is_staff access).
"""
"""
self
.
staff_client
.
login
(
username
=
self
.
staff_user
.
username
,
password
=
TEST_PASSWORD
)
client
=
self
.
login_client
(
api_client
,
user
)
self
.
send_patch
(
self
.
staff_
client
,
{},
expected_status
=
404
)
self
.
send_patch
(
client
,
{},
expected_status
=
404
)
@ddt.data
(
@ddt.data
(
(
"client"
,
"user"
),
(
"client"
,
"user"
),
...
@@ -217,9 +292,7 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -217,9 +292,7 @@ class TestAccountAPI(UserAPITestCase):
# Note that email is tested below, as it is not immediately updated.
# Note that email is tested below, as it is not immediately updated.
)
)
@ddt.unpack
@ddt.unpack
def
test_patch_account
(
def
test_patch_account
(
self
,
field
,
value
,
fails_validation_value
=
None
,
developer_validation_message
=
None
):
self
,
field
,
value
,
fails_validation_value
=
None
,
developer_validation_message
=
None
):
"""
"""
Test the behavior of patch, when using the correct content_type.
Test the behavior of patch, when using the correct content_type.
"""
"""
...
@@ -311,10 +384,10 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -311,10 +384,10 @@ class TestAccountAPI(UserAPITestCase):
self
.
assertEqual
(
expected_entries
,
len
(
name_change_info
))
self
.
assertEqual
(
expected_entries
,
len
(
name_change_info
))
return
name_change_info
return
name_change_info
def
verify_change_info
(
change_info
,
old_name
,
new_name
):
def
verify_change_info
(
change_info
,
old_name
,
requester
,
new_name
):
self
.
assertEqual
(
3
,
len
(
change_info
))
self
.
assertEqual
(
3
,
len
(
change_info
))
self
.
assertEqual
(
old_name
,
change_info
[
0
])
self
.
assertEqual
(
old_name
,
change_info
[
0
])
self
.
assertEqual
(
"Name change requested through account API
"
,
change_info
[
1
])
self
.
assertEqual
(
"Name change requested through account API
by {}"
.
format
(
requester
)
,
change_info
[
1
])
self
.
assertIsNotNone
(
change_info
[
2
])
self
.
assertIsNotNone
(
change_info
[
2
])
# Verify the new name was also stored.
# Verify the new name was also stored.
get_response
=
self
.
send_get
(
self
.
client
)
get_response
=
self
.
send_get
(
self
.
client
)
...
@@ -328,13 +401,13 @@ class TestAccountAPI(UserAPITestCase):
...
@@ -328,13 +401,13 @@ class TestAccountAPI(UserAPITestCase):
# First change the name as the user and verify meta information.
# First change the name as the user and verify meta information.
self
.
send_patch
(
self
.
client
,
{
"name"
:
"Mickey Mouse"
})
self
.
send_patch
(
self
.
client
,
{
"name"
:
"Mickey Mouse"
})
name_change_info
=
get_name_change_info
(
1
)
name_change_info
=
get_name_change_info
(
1
)
verify_change_info
(
name_change_info
[
0
],
old_name
,
"Mickey Mouse"
)
verify_change_info
(
name_change_info
[
0
],
old_name
,
self
.
user
.
username
,
"Mickey Mouse"
)
# Now change the name again and verify meta information.
# Now change the name again and verify meta information.
self
.
send_patch
(
self
.
client
,
{
"name"
:
"Donald Duck"
})
self
.
send_patch
(
self
.
client
,
{
"name"
:
"Donald Duck"
})
name_change_info
=
get_name_change_info
(
2
)
name_change_info
=
get_name_change_info
(
2
)
verify_change_info
(
name_change_info
[
0
],
old_name
,
"Donald Duck"
,)
verify_change_info
(
name_change_info
[
0
],
old_name
,
self
.
user
.
username
,
"Donald Duck"
,)
verify_change_info
(
name_change_info
[
1
],
"Mickey Mouse"
,
"Donald Duck"
)
verify_change_info
(
name_change_info
[
1
],
"Mickey Mouse"
,
self
.
user
.
username
,
"Donald Duck"
)
def
test_patch_email
(
self
):
def
test_patch_email
(
self
):
"""
"""
...
...
openedx/core/djangoapps/user_api/accounts/views.py
View file @
c8a20df2
...
@@ -7,6 +7,7 @@ https://openedx.atlassian.net/wiki/display/TNL/User+API
...
@@ -7,6 +7,7 @@ https://openedx.atlassian.net/wiki/display/TNL/User+API
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
from
django.conf
import
settings
import
datetime
import
datetime
from
pytz
import
UTC
from
pytz
import
UTC
...
@@ -17,11 +18,14 @@ from rest_framework.authentication import OAuth2Authentication, SessionAuthentic
...
@@ -17,11 +18,14 @@ from rest_framework.authentication import OAuth2Authentication, SessionAuthentic
from
rest_framework
import
permissions
from
rest_framework
import
permissions
from
openedx.core.djangoapps.user_api.accounts.serializers
import
AccountLegacyProfileSerializer
,
AccountUserSerializer
from
openedx.core.djangoapps.user_api.accounts.serializers
import
AccountLegacyProfileSerializer
,
AccountUserSerializer
from
openedx.core.djangoapps.user_api.api.account
import
AccountUserNotFound
,
AccountUpdateError
from
openedx.core.djangoapps.user_api.api.account
import
AccountUserNotFound
,
AccountUpdateError
,
AccountNotAuthorized
from
openedx.core.lib.api.parsers
import
MergePatchParser
from
openedx.core.lib.api.parsers
import
MergePatchParser
from
openedx.core.lib.api.permissions
import
IsUserInUrlOrStaff
from
openedx.core.lib.api.permissions
import
IsUserInUrlOrStaff
from
student.models
import
UserProfile
from
student.models
import
UserProfile
from
student.views
import
do_email_change_request
from
student.views
import
do_email_change_request
from
..models
import
UserPreference
from
.
import
ACCOUNT_VISIBILITY_PREF_KEY
,
ALL_USERS_VISIBILITY
class
AccountView
(
APIView
):
class
AccountView
(
APIView
):
...
@@ -79,7 +83,7 @@ class AccountView(APIView):
...
@@ -79,7 +83,7 @@ class AccountView(APIView):
"""
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,
IsUserInUrlOrStaff
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
parser_classes
=
(
MergePatchParser
,)
parser_classes
=
(
MergePatchParser
,)
def
get
(
self
,
request
,
username
):
def
get
(
self
,
request
,
username
):
...
@@ -87,34 +91,75 @@ class AccountView(APIView):
...
@@ -87,34 +91,75 @@ class AccountView(APIView):
GET /api/user/v0/accounts/{username}/
GET /api/user/v0/accounts/{username}/
"""
"""
try
:
try
:
account_settings
=
AccountView
.
get_serialized_account
(
username
)
account_settings
=
AccountView
.
get_serialized_account
(
request
.
user
,
username
,
view
=
request
.
QUERY_PARAMS
.
get
(
'view'
)
)
except
AccountUserNotFound
:
except
AccountUserNotFound
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
account_settings
)
return
Response
(
account_settings
)
@staticmethod
@staticmethod
def
get_serialized_account
(
usernam
e
):
def
get_serialized_account
(
requesting_user
,
username
=
None
,
configuration
=
None
,
view
=
Non
e
):
"""Returns
the user's account information
serialized as JSON.
"""Returns
account information for a user
serialized as JSON.
Note:
Note:
This method does not perform authentication so it is up to the caller
If `requesting_user.username` != `username`, this method will return differing amounts of information
to ensure that only the user themselves or staff can access the account
.
based on who `requesting_user` is and the privacy settings of the user associated with `username`
.
Args:
Args:
username (str): The username for the desired account.
requesting_user (User): The user requesting the account information. Only the user with username
`username` or users with "is_staff" privileges can get full account information.
Other users will get the account fields that the user has elected to share.
username (str): Optional username for the desired account information. If not specified,
`requesting_user.username` is assumed.
configuration (dict): an optional configuration specifying which fields in the account
can be shared, and the default visibility settings. If not present, the setting value with
key ACCOUNT_VISIBILITY_CONFIGURATION is used.
view (str): An optional string allowing "is_staff" users and users requesting their own
account information to get just the fields that are shared with everyone. If view is
"shared", only shared account information will be returned, regardless of `requesting_user`.
Returns:
Returns:
A dict containing each of the account's
fields.
A dict containing account
fields.
Raises:
Raises:
AccountUserNotFound: raised if there is no account for the specified
username.
AccountUserNotFound: `username` was specified, but no user exists with that
username.
"""
"""
if
username
is
None
:
username
=
requesting_user
.
username
has_full_access
=
requesting_user
.
username
==
username
or
requesting_user
.
is_staff
return_all_fields
=
has_full_access
and
view
!=
'shared'
existing_user
,
existing_user_profile
=
AccountView
.
_get_user_and_profile
(
username
)
existing_user
,
existing_user_profile
=
AccountView
.
_get_user_and_profile
(
username
)
user_serializer
=
AccountUserSerializer
(
existing_user
)
user_serializer
=
AccountUserSerializer
(
existing_user
)
legacy_profile_serializer
=
AccountLegacyProfileSerializer
(
existing_user_profile
)
legacy_profile_serializer
=
AccountLegacyProfileSerializer
(
existing_user_profile
)
return
dict
(
user_serializer
.
data
,
**
legacy_profile_serializer
.
data
)
account_settings
=
dict
(
user_serializer
.
data
,
**
legacy_profile_serializer
.
data
)
if
return_all_fields
:
return
account_settings
if
not
configuration
:
configuration
=
settings
.
ACCOUNT_VISIBILITY_CONFIGURATION
visible_settings
=
{}
profile_privacy
=
UserPreference
.
get_preference
(
existing_user
,
ACCOUNT_VISIBILITY_PREF_KEY
)
privacy_setting
=
profile_privacy
if
profile_privacy
else
configuration
.
get
(
'default_visibility'
)
if
privacy_setting
==
ALL_USERS_VISIBILITY
:
field_names
=
configuration
.
get
(
'shareable_fields'
)
else
:
field_names
=
configuration
.
get
(
'public_fields'
)
for
field_name
in
field_names
:
visible_settings
[
field_name
]
=
account_settings
.
get
(
field_name
,
None
)
return
visible_settings
def
patch
(
self
,
request
,
username
):
def
patch
(
self
,
request
,
username
):
"""
"""
...
@@ -124,13 +169,9 @@ class AccountView(APIView):
...
@@ -124,13 +169,9 @@ class AccountView(APIView):
https://tools.ietf.org/html/rfc7396. The content_type must be "application/merge-patch+json" or
https://tools.ietf.org/html/rfc7396. The content_type must be "application/merge-patch+json" or
else an error response with status code 415 will be returned.
else an error response with status code 415 will be returned.
"""
"""
# Disallow users with is_staff access from calling patch on any account.
if
request
.
user
.
username
!=
username
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
try
:
try
:
AccountView
.
update_account
(
username
,
request
.
DATA
)
AccountView
.
update_account
(
request
.
user
,
request
.
DATA
,
username
=
username
)
except
AccountUserNotFound
:
except
(
AccountUserNotFound
,
AccountNotAuthorized
)
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
except
AccountUpdateError
as
err
:
except
AccountUpdateError
as
err
:
return
Response
(
err
.
error_info
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
err
.
error_info
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
...
@@ -138,23 +179,33 @@ class AccountView(APIView):
...
@@ -138,23 +179,33 @@ class AccountView(APIView):
return
Response
(
status
=
status
.
HTTP_204_NO_CONTENT
)
return
Response
(
status
=
status
.
HTTP_204_NO_CONTENT
)
@staticmethod
@staticmethod
def
update_account
(
username
,
updat
e
):
def
update_account
(
requesting_user
,
update
,
username
=
Non
e
):
"""Update
the account for the given username
.
"""Update
user account information
.
Note:
Note:
No authorization or permissions checks are done in this method. It is up to the caller
It is up to the caller of this method to enforce the contract that this method is only called
of this method to enforce the contract that this method is only called
with the user who made the request.
by the user with the specified username.
Arguments:
Arguments:
username (string): the username associated with the account to change
requesting_user (User): The user requesting to modify account information. Only the user with username
update (dict): the updated account field values
'username' has permissions to modify account information.
update (dict): The updated account field values.
username (string): Optional username specifying which account should be updated. If not specified,
`requesting_user.username` is assumed.
Raises:
Raises:
AccountUserNotFound:
no user exists with the specified username
AccountUserNotFound:
`username` was specified, but no user exists with that username.
AccountUpdateError: the update could not be completed, usually due to validation errors
AccountUpdateError: the update could not be completed, usually due to validation errors
(for example, read-only fields were specified or field values are not legal)
(for example, read-only fields were specified or field values are not legal)
AccountNotAuthorized: the requesting_user does not have access to change the account
associated with `username`.
"""
"""
if
username
is
None
:
username
=
requesting_user
.
username
if
requesting_user
.
username
!=
username
:
raise
AccountNotAuthorized
()
existing_user
,
existing_user_profile
=
AccountView
.
_get_user_and_profile
(
username
)
existing_user
,
existing_user_profile
=
AccountView
.
_get_user_and_profile
(
username
)
# 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.
...
@@ -203,14 +254,15 @@ class AccountView(APIView):
...
@@ -203,14 +254,15 @@ class AccountView(APIView):
raise
AccountUpdateError
(
validation_errors
)
raise
AccountUpdateError
(
validation_errors
)
serializer
.
save
()
serializer
.
save
()
# If the name was changed, store information about the change operation.
# If the name was changed, store information about the change operation. This is outside of the
# serializer so that we can store who requested the change.
if
old_name
:
if
old_name
:
meta
=
existing_user_profile
.
get_meta
()
meta
=
existing_user_profile
.
get_meta
()
if
'old_names'
not
in
meta
:
if
'old_names'
not
in
meta
:
meta
[
'old_names'
]
=
[]
meta
[
'old_names'
]
=
[]
meta
[
'old_names'
]
.
append
([
meta
[
'old_names'
]
.
append
([
old_name
,
old_name
,
"Name change requested through account API
"
,
"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
)
...
...
openedx/core/djangoapps/user_api/api/profile.py
View file @
c8a20df2
...
@@ -90,14 +90,14 @@ def update_preferences(username, **kwargs):
...
@@ -90,14 +90,14 @@ def update_preferences(username, **kwargs):
@intercept_errors
(
ProfileInternalError
,
ignore_errors
=
[
ProfileRequestError
])
@intercept_errors
(
ProfileInternalError
,
ignore_errors
=
[
ProfileRequestError
])
def
update_email_opt_in
(
user
name
,
org
,
optin
):
def
update_email_opt_in
(
user
,
org
,
optin
):
"""Updates a user's preference for receiving org-wide emails.
"""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
Sets a User Org Tag defining the choice to opt in or opt out of organization-wide
emails.
emails.
Arguments:
Arguments:
user
name (st
r): The user to set a preference for.
user
(Use
r): The user to set a preference for.
org (str): The org is used to determine the organization this setting is related to.
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
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.
the correct age to receive emails, email-optin is set to False regardless.
...
@@ -105,11 +105,8 @@ def update_email_opt_in(username, org, optin):
...
@@ -105,11 +105,8 @@ def update_email_opt_in(username, org, optin):
Returns:
Returns:
None
None
Raises:
AccountUserNotFound: Raised when the username specified is not associated with a user.
"""
"""
account_settings
=
AccountView
.
get_serialized_account
(
user
name
)
account_settings
=
AccountView
.
get_serialized_account
(
user
)
year_of_birth
=
account_settings
[
'year_of_birth'
]
year_of_birth
=
account_settings
[
'year_of_birth'
]
of_age
=
(
of_age
=
(
year_of_birth
is
None
or
# If year of birth is not set, we assume user is of age.
year_of_birth
is
None
or
# If year of birth is not set, we assume user is of age.
...
@@ -118,7 +115,6 @@ def update_email_opt_in(username, org, optin):
...
@@ -118,7 +115,6 @@ def update_email_opt_in(username, org, optin):
)
)
try
:
try
:
user
=
User
.
objects
.
get
(
username
=
username
)
preference
,
_
=
UserOrgTag
.
objects
.
get_or_create
(
preference
,
_
=
UserOrgTag
.
objects
.
get_or_create
(
user
=
user
,
org
=
org
,
key
=
'email-optin'
user
=
user
,
org
=
org
,
key
=
'email-optin'
)
)
...
...
openedx/core/djangoapps/user_api/management/tests/test_email_opt_in_list.py
View file @
c8a20df2
...
@@ -297,7 +297,7 @@ class EmailOptInListTest(ModuleStoreTestCase):
...
@@ -297,7 +297,7 @@ class EmailOptInListTest(ModuleStoreTestCase):
None
None
"""
"""
profile_api
.
update_email_opt_in
(
user
.
username
,
org
,
is_opted_in
)
profile_api
.
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/profiles/__init__.py
deleted
100644 → 0
View file @
450d9e37
"""
Profile constants
"""
PROFILE_VISIBILITY_PREF_KEY
=
'profile_privacy'
# Indicates the user's preference that all users can view their profile.
ALL_USERS_VISIBILITY
=
'all_users'
# Indicates the user's preference that their profile be private.
PRIVATE_VISIBILITY
=
'private'
openedx/core/djangoapps/user_api/profiles/tests/__init__.py
deleted
100644 → 0
View file @
450d9e37
openedx/core/djangoapps/user_api/profiles/tests/test_views.py
deleted
100644 → 0
View file @
450d9e37
"""
Unit tests for profile APIs.
"""
import
ddt
import
unittest
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
mock
import
patch
from
openedx.core.djangoapps.user_api.accounts.tests.test_views
import
UserAPITestCase
from
openedx.core.djangoapps.user_api.models
import
UserPreference
from
openedx.core.djangoapps.user_api.profiles
import
PROFILE_VISIBILITY_PREF_KEY
from
..
import
PRIVATE_VISIBILITY
@ddt.ddt
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Profile APIs are only supported in LMS'
)
class
TestProfileAPI
(
UserAPITestCase
):
"""
Unit tests for the profile API.
"""
def
setUp
(
self
):
super
(
TestProfileAPI
,
self
)
.
setUp
()
self
.
url
=
reverse
(
"profiles_api"
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
def
test_get_profile_anonymous_user
(
self
):
"""
Test that an anonymous client (not logged in) cannot call get.
"""
self
.
send_get
(
self
.
anonymous_client
,
expected_status
=
401
)
def
_verify_full_profile_response
(
self
,
response
):
"""
Verify that all of the profile's fields are returned
"""
data
=
response
.
data
self
.
assertEqual
(
6
,
len
(
data
))
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertEqual
(
"US"
,
data
[
"country"
])
self
.
assertIsNone
(
data
[
"profile_image"
])
self
.
assertIsNone
(
data
[
"time_zone"
])
self
.
assertIsNone
(
data
[
"languages"
])
self
.
assertIsNone
(
data
[
"bio"
])
def
_verify_private_profile_response
(
self
,
response
):
"""
Verify that only the public fields are returned for a private user's profile
"""
data
=
response
.
data
self
.
assertEqual
(
2
,
len
(
data
))
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertIsNone
(
data
[
"profile_image"
])
@ddt.data
(
(
"client"
,
"user"
),
(
"different_client"
,
"different_user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
# Note: using getattr so that the patching works even if there is no configuration.
# This is needed when testing CMS as the patching is still executed even though the
# suite is skipped.
@patch.dict
(
getattr
(
settings
,
"PROFILE_CONFIGURATION"
,
{}),
{
"default_visibility"
:
"all_users"
})
def
test_get_default_profile
(
self
,
api_client
,
username
):
"""
Test that any logged in user can get the main test user's public profile information.
"""
client
=
self
.
login_client
(
api_client
,
username
)
self
.
create_mock_profile
(
self
.
user
)
response
=
self
.
send_get
(
client
)
self
.
_verify_full_profile_response
(
response
)
@ddt.data
(
(
"client"
,
"user"
),
(
"different_client"
,
"different_user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
# Note: using getattr so that the patching works even if there is no configuration.
# This is needed when testing CMS as the patching is still executed even though the
# suite is skipped.
@patch.dict
(
getattr
(
settings
,
"PROFILE_CONFIGURATION"
,
{}),
{
"default_visibility"
:
"private"
})
def
test_get_default_private_profile
(
self
,
api_client
,
username
):
"""
Test that any logged in user gets only the public fields for a profile
if the default visibility is 'private'.
"""
client
=
self
.
login_client
(
api_client
,
username
)
self
.
create_mock_profile
(
self
.
user
)
response
=
self
.
send_get
(
client
)
self
.
_verify_private_profile_response
(
response
)
@ddt.data
(
(
"client"
,
"user"
),
(
"different_client"
,
"different_user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_get_private_profile
(
self
,
api_client
,
requesting_username
):
"""
Test that private profile information is only available to the test user themselves.
"""
client
=
self
.
login_client
(
api_client
,
requesting_username
)
# Verify that a user with a private profile only returns the public fields
UserPreference
.
set_preference
(
self
.
user
,
PROFILE_VISIBILITY_PREF_KEY
,
PRIVATE_VISIBILITY
)
self
.
create_mock_profile
(
self
.
user
)
response
=
self
.
send_get
(
client
)
self
.
_verify_private_profile_response
(
response
)
# Verify that only the public fields are returned if 'include_all' parameter is specified as false
response
=
self
.
send_get
(
client
,
query_parameters
=
'include_all=false'
)
self
.
_verify_private_profile_response
(
response
)
# Verify that all fields are returned for the user themselves if
# the 'include_all' parameter is specified as true.
response
=
self
.
send_get
(
client
,
query_parameters
=
'include_all=true'
)
if
requesting_username
==
"user"
:
self
.
_verify_full_profile_response
(
response
)
else
:
self
.
_verify_private_profile_response
(
response
)
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_get_profile_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
(
"profiles_api"
,
kwargs
=
{
'username'
:
"does_not_exist"
}))
self
.
assertEqual
(
404
,
response
.
status_code
)
openedx/core/djangoapps/user_api/profiles/views.py
deleted
100644 → 0
View file @
450d9e37
"""
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
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
rest_framework
import
status
from
rest_framework.views
import
APIView
from
rest_framework.response
import
Response
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework
import
permissions
from
..accounts.views
import
AccountView
from
..api.account
import
AccountUserNotFound
from
..models
import
UserPreference
from
.
import
PROFILE_VISIBILITY_PREF_KEY
,
ALL_USERS_VISIBILITY
class
ProfileView
(
APIView
):
"""
**Use Cases**
Get the user's public profile information.
**Example Requests**:
GET /api/user/v0/profiles/{username}/[?include_all={true | false}]
**Response Values for GET**
Returns the same responses as for the AccountView API for the
subset of fields that have been configured to be in a profile.
The fields are additionally filtered based upon the user's
specified privacy permissions.
If the parameter 'include_all' is passed as 'true' then a user
can receive all fields for their own account, ignoring
any field visibility preferences. If the parameter is not
specified or if the user is requesting information for a
different account then the privacy filtering will be applied.
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
get
(
self
,
request
,
username
):
"""
GET /api/user/v0/profiles/{username}/[?include_all={true | false}]
Note:
The include_all query parameter will only be honored if the user is making
the request for their own username. It defaults to false, but if true
then all the profile fields will be returned even for a user with
a private profile.
"""
if
request
.
user
.
username
==
username
:
include_all_fields
=
self
.
request
.
QUERY_PARAMS
.
get
(
'include_all'
)
==
'true'
else
:
include_all_fields
=
False
try
:
profile_settings
=
ProfileView
.
get_serialized_profile
(
username
,
settings
.
PROFILE_CONFIGURATION
,
include_all_fields
=
include_all_fields
,
)
except
AccountUserNotFound
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
profile_settings
)
@staticmethod
def
get_serialized_profile
(
username
,
configuration
=
None
,
include_all_fields
=
False
):
"""Returns the user's public profile settings serialized as JSON.
The fields returned are by default governed by the user's privacy preference.
If the user has a private profile, then only the fields that are always
public are returned. If the user is sharing their profile with all users
then all profile fields are returned.
Note:
This method does not perform authentication so it is up to the caller
to ensure that only edX users can access the profile. In addition, only
the user themselves should be able to access all fields of a private
profile through 'include_all_fields' being true.
Args:
username (str): The username for the desired account.
configuration (dict): A dictionary specifying three profile configuration settings:
default_visibility: the default visibility level for user's with no preference
all_fields: the list of all fields that can be shown on a profile
public_fields: the list of profile fields that are public
include_all_fields (bool): If true, ignores the user's privacy setting.
Returns:
A dict containing each of the user's profile fields.
Raises:
AccountUserNotFound: raised if there is no account for the specified username.
"""
if
not
configuration
:
configuration
=
settings
.
PROFILE_CONFIGURATION
account_settings
=
AccountView
.
get_serialized_account
(
username
)
profile
=
{}
privacy_setting
=
ProfileView
.
_get_user_profile_privacy
(
username
,
configuration
)
if
include_all_fields
or
privacy_setting
==
ALL_USERS_VISIBILITY
:
field_names
=
configuration
.
get
(
'all_fields'
)
else
:
field_names
=
configuration
.
get
(
'public_fields'
)
for
field_name
in
field_names
:
profile
[
field_name
]
=
account_settings
.
get
(
field_name
,
None
)
return
profile
@staticmethod
def
_get_user_profile_privacy
(
username
,
configuration
):
"""
Returns the profile privacy preference for the specified user.
"""
user
=
User
.
objects
.
get
(
username
=
username
)
profile_privacy
=
UserPreference
.
get_preference
(
user
,
PROFILE_VISIBILITY_PREF_KEY
)
return
profile_privacy
if
profile_privacy
else
configuration
.
get
(
'default_visibility'
)
openedx/core/djangoapps/user_api/tests/test_profile_api.py
View file @
c8a20df2
...
@@ -29,7 +29,8 @@ class ProfileApiTest(ModuleStoreTestCase):
...
@@ -29,7 +29,8 @@ class ProfileApiTest(ModuleStoreTestCase):
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
account_api
.
create_account
(
self
.
USERNAME
,
self
.
PASSWORD
,
self
.
EMAIL
)
# Retrieve the account settings
# Retrieve the account settings
account_settings
=
AccountView
.
get_serialized_account
(
self
.
USERNAME
)
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
account_settings
=
AccountView
.
get_serialized_account
(
user
)
# Expect a date joined field but remove it to simplify the following comparison
# Expect a date joined field but remove it to simplify the following comparison
self
.
assertIsNotNone
(
account_settings
[
'date_joined'
])
self
.
assertIsNotNone
(
account_settings
[
'date_joined'
])
...
@@ -87,7 +88,7 @@ class ProfileApiTest(ModuleStoreTestCase):
...
@@ -87,7 +88,7 @@ class ProfileApiTest(ModuleStoreTestCase):
profile
.
year_of_birth
=
year_of_birth
profile
.
year_of_birth
=
year_of_birth
profile
.
save
()
profile
.
save
()
profile_api
.
update_email_opt_in
(
self
.
USERNAME
,
course
.
id
.
org
,
option
)
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'
)
result_obj
=
UserOrgTag
.
objects
.
get
(
user
=
user
,
org
=
course
.
id
.
org
,
key
=
'email-optin'
)
self
.
assertEqual
(
result_obj
.
value
,
expected_result
)
self
.
assertEqual
(
result_obj
.
value
,
expected_result
)
...
@@ -99,7 +100,7 @@ class ProfileApiTest(ModuleStoreTestCase):
...
@@ -99,7 +100,7 @@ class ProfileApiTest(ModuleStoreTestCase):
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
profile_api
.
update_email_opt_in
(
self
.
USERNAME
,
course
.
id
.
org
,
True
)
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'
)
result_obj
=
UserOrgTag
.
objects
.
get
(
user
=
user
,
org
=
course
.
id
.
org
,
key
=
'email-optin'
)
self
.
assertEqual
(
result_obj
.
value
,
u"True"
)
self
.
assertEqual
(
result_obj
.
value
,
u"True"
)
...
@@ -130,8 +131,8 @@ class ProfileApiTest(ModuleStoreTestCase):
...
@@ -130,8 +131,8 @@ class ProfileApiTest(ModuleStoreTestCase):
profile
.
year_of_birth
=
year_of_birth
profile
.
year_of_birth
=
year_of_birth
profile
.
save
()
profile
.
save
()
profile_api
.
update_email_opt_in
(
self
.
USERNAME
,
course
.
id
.
org
,
option
)
profile_api
.
update_email_opt_in
(
user
,
course
.
id
.
org
,
option
)
profile_api
.
update_email_opt_in
(
self
.
USERNAME
,
course
.
id
.
org
,
second_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'
)
result_obj
=
UserOrgTag
.
objects
.
get
(
user
=
user
,
org
=
course
.
id
.
org
,
key
=
'email-optin'
)
self
.
assertEqual
(
result_obj
.
value
,
expected_result
)
self
.
assertEqual
(
result_obj
.
value
,
expected_result
)
...
...
openedx/core/djangoapps/user_api/tests/test_views.py
View file @
c8a20df2
...
@@ -8,6 +8,7 @@ import re
...
@@ -8,6 +8,7 @@ import re
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.core
import
mail
from
django.core
import
mail
from
django.contrib.auth.models
import
User
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
from
unittest
import
skipUnless
from
unittest
import
skipUnless
...
@@ -1247,7 +1248,8 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -1247,7 +1248,8 @@ class RegistrationViewTest(ApiTestCase):
)
)
# Verify that the user's full name is set
# Verify that the user's full name is set
account_settings
=
AccountView
.
get_serialized_account
(
self
.
USERNAME
)
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
account_settings
=
AccountView
.
get_serialized_account
(
user
)
self
.
assertEqual
(
account_settings
[
"name"
],
self
.
NAME
)
self
.
assertEqual
(
account_settings
[
"name"
],
self
.
NAME
)
# Verify that we've been logged in
# Verify that we've been logged in
...
@@ -1280,7 +1282,8 @@ class RegistrationViewTest(ApiTestCase):
...
@@ -1280,7 +1282,8 @@ class RegistrationViewTest(ApiTestCase):
self
.
assertHttpOK
(
response
)
self
.
assertHttpOK
(
response
)
# Verify the user's account
# Verify the user's account
account_settings
=
AccountView
.
get_serialized_account
(
self
.
USERNAME
)
user
=
User
.
objects
.
get
(
username
=
self
.
USERNAME
)
account_settings
=
AccountView
.
get_serialized_account
(
user
)
self
.
assertEqual
(
account_settings
[
"level_of_education"
],
self
.
EDUCATION
)
self
.
assertEqual
(
account_settings
[
"level_of_education"
],
self
.
EDUCATION
)
self
.
assertEqual
(
account_settings
[
"mailing_address"
],
self
.
ADDRESS
)
self
.
assertEqual
(
account_settings
[
"mailing_address"
],
self
.
ADDRESS
)
self
.
assertEqual
(
account_settings
[
"year_of_birth"
],
int
(
self
.
YEAR_OF_BIRTH
))
self
.
assertEqual
(
account_settings
[
"year_of_birth"
],
int
(
self
.
YEAR_OF_BIRTH
))
...
...
openedx/core/djangoapps/user_api/urls.py
View file @
c8a20df2
...
@@ -3,7 +3,6 @@ Defines the URL routes for this app.
...
@@ -3,7 +3,6 @@ Defines the URL routes for this app.
"""
"""
from
.accounts.views
import
AccountView
from
.accounts.views
import
AccountView
from
.profiles.views
import
ProfileView
from
django.conf.urls
import
patterns
,
url
from
django.conf.urls
import
patterns
,
url
...
@@ -16,9 +15,4 @@ urlpatterns = patterns(
...
@@ -16,9 +15,4 @@ urlpatterns = patterns(
AccountView
.
as_view
(),
AccountView
.
as_view
(),
name
=
"accounts_api"
name
=
"accounts_api"
),
),
url
(
r'^v0/profiles/'
+
USERNAME_PATTERN
+
'$'
,
ProfileView
.
as_view
(),
name
=
"profiles_api"
),
)
)
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