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
9d956e03
Commit
9d956e03
authored
Feb 25, 2015
by
Christina Roberts
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7053 from edx/christina/account-api
User account API
parents
8bdd90a5
ae0333cb
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
503 additions
and
10 deletions
+503
-10
lms/djangoapps/mobile_api/testutils.py
+2
-2
lms/djangoapps/mobile_api/utils.py
+2
-8
lms/urls.py
+2
-0
openedx/core/djangoapps/user_api/accounts/__init__.py
+0
-0
openedx/core/djangoapps/user_api/accounts/serializers.py
+42
-0
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+248
-0
openedx/core/djangoapps/user_api/accounts/urls.py
+14
-0
openedx/core/djangoapps/user_api/accounts/views.py
+161
-0
openedx/core/lib/api/parsers.py
+8
-0
openedx/core/lib/api/permissions.py
+24
-0
No files found.
lms/djangoapps/mobile_api/testutils.py
View file @
9d956e03
...
@@ -105,7 +105,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
...
@@ -105,7 +105,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
"""
"""
def
test_invalid_user
(
self
):
def
test_invalid_user
(
self
):
self
.
login_and_enroll
()
self
.
login_and_enroll
()
self
.
api_response
(
expected_response_code
=
40
3
,
username
=
'no_user'
)
self
.
api_response
(
expected_response_code
=
40
4
,
username
=
'no_user'
)
def
test_other_user
(
self
):
def
test_other_user
(
self
):
# login and enroll as the test user
# login and enroll as the test user
...
@@ -120,7 +120,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
...
@@ -120,7 +120,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
# now login and call the API as the test user
# now login and call the API as the test user
self
.
login
()
self
.
login
()
self
.
api_response
(
expected_response_code
=
40
3
,
username
=
other
.
username
)
self
.
api_response
(
expected_response_code
=
40
4
,
username
=
other
.
username
)
@ddt.ddt
@ddt.ddt
...
...
lms/djangoapps/mobile_api/utils.py
View file @
9d956e03
...
@@ -10,6 +10,7 @@ from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2Au
...
@@ -10,6 +10,7 @@ from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2Au
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
courseware.courses
import
get_course_with_access
from
courseware.courses
import
get_course_with_access
from
openedx.core.lib.api.permissions
import
IsUserInUrl
def
mobile_course_access
(
depth
=
0
,
verify_enrolled
=
True
):
def
mobile_course_access
(
depth
=
0
,
verify_enrolled
=
True
):
...
@@ -42,13 +43,6 @@ def mobile_view(is_user=False):
...
@@ -42,13 +43,6 @@ def mobile_view(is_user=False):
"""
"""
Function and class decorator that abstracts the authentication and permission checks for mobile api views.
Function and class decorator that abstracts the authentication and permission checks for mobile api views.
"""
"""
class
IsUser
(
permissions
.
BasePermission
):
"""
Permission that checks to see if the request user matches the user in the URL.
"""
def
has_permission
(
self
,
request
,
view
):
return
request
.
user
.
username
==
request
.
parser_context
.
get
(
'kwargs'
,
{})
.
get
(
'username'
,
None
)
def
_decorator
(
func_or_class
):
def
_decorator
(
func_or_class
):
"""
"""
Requires either OAuth2 or Session-based authentication.
Requires either OAuth2 or Session-based authentication.
...
@@ -60,6 +54,6 @@ def mobile_view(is_user=False):
...
@@ -60,6 +54,6 @@ def mobile_view(is_user=False):
)
)
func_or_class
.
permission_classes
=
(
permissions
.
IsAuthenticated
,)
func_or_class
.
permission_classes
=
(
permissions
.
IsAuthenticated
,)
if
is_user
:
if
is_user
:
func_or_class
.
permission_classes
+=
(
IsUser
,)
func_or_class
.
permission_classes
+=
(
IsUser
InUrl
,)
return
func_or_class
return
func_or_class
return
_decorator
return
_decorator
lms/urls.py
View file @
9d956e03
...
@@ -61,6 +61,8 @@ urlpatterns = ('', # nopep8
...
@@ -61,6 +61,8 @@ urlpatterns = ('', # nopep8
url
(
r'^user_api/'
,
include
(
'openedx.core.djangoapps.user_api.urls'
)),
url
(
r'^user_api/'
,
include
(
'openedx.core.djangoapps.user_api.urls'
)),
url
(
r'^api/user/'
,
include
(
'openedx.core.djangoapps.user_api.accounts.urls'
)),
url
(
r'^notifier_api/'
,
include
(
'notifier_api.urls'
)),
url
(
r'^notifier_api/'
,
include
(
'notifier_api.urls'
)),
url
(
r'^lang_pref/'
,
include
(
'lang_pref.urls'
)),
url
(
r'^lang_pref/'
,
include
(
'lang_pref.urls'
)),
...
...
openedx/core/djangoapps/user_api/accounts/__init__.py
0 → 100644
View file @
9d956e03
openedx/core/djangoapps/user_api/accounts/serializers.py
0 → 100644
View file @
9d956e03
from
rest_framework
import
serializers
from
django.contrib.auth.models
import
User
from
student.models
import
UserProfile
class
AccountUserSerializer
(
serializers
.
HyperlinkedModelSerializer
):
"""
Class that serializes the portion of User model needed for account information.
"""
class
Meta
:
model
=
User
fields
=
(
"username"
,
"email"
,
"date_joined"
)
read_only_fields
=
(
"username"
,
"email"
,
"date_joined"
)
class
AccountLegacyProfileSerializer
(
serializers
.
HyperlinkedModelSerializer
):
"""
Class that serializes the portion of UserProfile model needed for account information.
"""
class
Meta
:
model
=
UserProfile
fields
=
(
"name"
,
"gender"
,
"goals"
,
"year_of_birth"
,
"level_of_education"
,
"language"
,
"country"
,
"mailing_address"
)
read_only_fields
=
(
"name"
,)
def
transform_gender
(
self
,
obj
,
value
):
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
return
AccountLegacyProfileSerializer
.
convert_empty_to_None
(
value
)
def
transform_country
(
self
,
obj
,
value
):
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
return
AccountLegacyProfileSerializer
.
convert_empty_to_None
(
value
)
def
transform_level_of_education
(
self
,
obj
,
value
):
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
return
AccountLegacyProfileSerializer
.
convert_empty_to_None
(
value
)
@staticmethod
def
convert_empty_to_None
(
value
):
""" Helper method to convert empty string to None (other values pass through). """
return
None
if
value
==
""
else
value
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
0 → 100644
View file @
9d956e03
import
unittest
import
ddt
import
json
from
datetime
import
datetime
from
django.test
import
TestCase
from
django.core.urlresolvers
import
reverse
from
django.conf
import
settings
from
rest_framework.test
import
APITestCase
,
APIClient
from
student.tests.factories
import
UserFactory
from
student.models
import
UserProfile
TEST_PASSWORD
=
"test"
@ddt.ddt
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
TestAccountAPI
(
APITestCase
):
def
setUp
(
self
):
super
(
TestAccountAPI
,
self
)
.
setUp
()
self
.
anonymous_client
=
APIClient
()
self
.
different_user
=
UserFactory
.
create
(
password
=
TEST_PASSWORD
)
self
.
different_client
=
APIClient
()
self
.
staff_user
=
UserFactory
(
is_staff
=
True
,
password
=
TEST_PASSWORD
)
self
.
staff_client
=
APIClient
()
self
.
user
=
UserFactory
.
create
(
password
=
TEST_PASSWORD
)
self
.
url
=
reverse
(
"accounts_api"
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
def
test_get_account_anonymous_user
(
self
):
"""
Test that an anonymous client (not logged in) cannot call get.
"""
self
.
send_get
(
self
.
anonymous_client
,
expected_status
=
401
)
def
test_get_account_different_user
(
self
):
"""
Test that a client (logged in) cannot get the account information for a different client.
"""
self
.
different_client
.
login
(
username
=
self
.
different_user
.
username
,
password
=
TEST_PASSWORD
)
self
.
send_get
(
self
.
different_client
,
expected_status
=
404
)
def
test_get_account_default
(
self
):
"""
Test that a client (logged in) can get her own account information (using default legacy profile information,
as created by the test UserFactory).
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
response
=
self
.
send_get
(
self
.
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"
])
for
empty_field
in
(
"year_of_birth"
,
"level_of_education"
,
"mailing_address"
):
self
.
assertIsNone
(
data
[
empty_field
])
self
.
assertIsNone
(
data
[
"country"
])
# TODO: what should the format of this be?
self
.
assertEqual
(
""
,
data
[
"language"
])
self
.
assertEqual
(
"m"
,
data
[
"gender"
])
self
.
assertEqual
(
"World domination"
,
data
[
"goals"
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
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.
"""
# Create some test profile values.
legacy_profile
=
UserProfile
.
objects
.
get
(
id
=
self
.
user
.
id
)
legacy_profile
.
country
=
"US"
legacy_profile
.
level_of_education
=
"m"
legacy_profile
.
year_of_birth
=
1900
legacy_profile
.
goals
=
"world peace"
legacy_profile
.
mailing_address
=
"Park Ave"
legacy_profile
.
save
()
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
):
"""
Test the conversion of empty strings to None for certain fields.
"""
legacy_profile
=
UserProfile
.
objects
.
get
(
id
=
self
.
user
.
id
)
legacy_profile
.
country
=
""
legacy_profile
.
level_of_education
=
""
legacy_profile
.
gender
=
""
legacy_profile
.
save
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
response
=
self
.
send_get
(
self
.
client
)
for
empty_field
in
(
"level_of_education"
,
"gender"
,
"country"
):
self
.
assertIsNone
(
response
.
data
[
empty_field
])
@ddt.data
(
(
"client"
,
"user"
,
"gender"
,
"f"
,
"not a gender"
,
"Select a valid choice. not a gender is not one of the available choices."
),
(
"client"
,
"user"
,
"level_of_education"
,
"none"
,
"x"
,
"Select a valid choice. x is not one of the available choices."
),
(
"client"
,
"user"
,
"country"
,
"GB"
,
"XY"
,
"Select a valid choice. XY is not one of the available choices."
),
(
"client"
,
"user"
,
"year_of_birth"
,
2009
,
"not_an_int"
,
"Enter a whole number."
),
(
"client"
,
"user"
,
"language"
,
"Creole"
),
(
"client"
,
"user"
,
"goals"
,
"Smell the roses"
),
(
"client"
,
"user"
,
"mailing_address"
,
"Sesame Street"
),
# All of the fields can be edited by is_staff, but iterating through all of them again seems like overkill.
# Just test a representative field.
(
"staff_client"
,
"staff_user"
,
"goals"
,
"Smell the roses"
),
)
@ddt.unpack
def
test_patch_account
(
self
,
api_client
,
user
,
field
,
value
,
fails_validation_value
=
None
,
developer_validation_message
=
None
):
"""
Test the behavior of patch, when using the correct content_type.
"""
client
=
self
.
login_client
(
api_client
,
user
)
self
.
send_patch
(
client
,
{
field
:
value
})
get_response
=
self
.
send_get
(
client
)
self
.
assertEqual
(
value
,
get_response
.
data
[
field
])
if
fails_validation_value
:
error_response
=
self
.
send_patch
(
client
,
{
field
:
fails_validation_value
},
expected_status
=
400
)
self
.
assertEqual
(
"Value '{0}' is not valid for field '{1}'."
.
format
(
fails_validation_value
,
field
),
error_response
.
data
[
"field_errors"
][
field
][
"user_message"
]
)
self
.
assertEqual
(
developer_validation_message
,
error_response
.
data
[
"field_errors"
][
field
][
"developer_message"
]
)
else
:
# If there are no values that would fail validation, then empty string should be supported.
self
.
send_patch
(
client
,
{
field
:
""
})
get_response
=
self
.
send_get
(
client
)
self
.
assertEqual
(
""
,
get_response
.
data
[
field
])
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_patch_account_noneditable
(
self
,
api_client
,
user
):
"""
Tests the behavior of patch when a read-only field is attempted to be edited.
"""
client
=
self
.
login_client
(
api_client
,
user
)
def
verify_error_response
(
field_name
,
data
):
self
.
assertEqual
(
"This field is not editable via this API"
,
data
[
"field_errors"
][
field_name
][
"developer_message"
]
)
self
.
assertEqual
(
"Field '{0}' cannot be edited."
.
format
(
field_name
),
data
[
"field_errors"
][
field_name
][
"user_message"
]
)
for
field_name
in
[
"username"
,
"email"
,
"date_joined"
,
"name"
]:
response
=
self
.
send_patch
(
client
,
{
field_name
:
"will_error"
,
"gender"
:
"f"
},
expected_status
=
400
)
verify_error_response
(
field_name
,
response
.
data
)
# Make sure that gender did not change.
response
=
self
.
send_get
(
client
)
self
.
assertEqual
(
"m"
,
response
.
data
[
"gender"
])
# Test error message with multiple read-only items
response
=
self
.
send_patch
(
client
,
{
"username"
:
"will_error"
,
"email"
:
"xx"
},
expected_status
=
400
)
self
.
assertEqual
(
2
,
len
(
response
.
data
[
"field_errors"
]))
verify_error_response
(
"username"
,
response
.
data
)
verify_error_response
(
"email"
,
response
.
data
)
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
=
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_patch_account_empty_string
(
self
):
"""
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.
"""
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
TEST_PASSWORD
)
for
field_name
in
[
"gender"
,
"level_of_education"
,
"country"
]:
self
.
send_patch
(
self
.
client
,
{
field_name
:
""
})
response
=
self
.
send_get
(
self
.
client
)
# Although throwing a 400 might be reasonable, the default DRF behavior with ModelSerializer
# is to convert to None, which also seems acceptable (and is difficult to override).
self
.
assertIsNone
(
response
.
data
[
field_name
])
# Verify that the behavior is the same for sending None.
self
.
send_patch
(
self
.
client
,
{
field_name
:
""
})
response
=
self
.
send_get
(
self
.
client
)
self
.
assertIsNone
(
response
.
data
[
field_name
])
def
login_client
(
self
,
api_client
,
user
):
"""Helper method for getting the client and user and logging in. Returns client. """
client
=
getattr
(
self
,
api_client
)
user
=
getattr
(
self
,
user
)
client
.
login
(
username
=
user
.
username
,
password
=
TEST_PASSWORD
)
return
client
def
send_patch
(
self
,
client
,
json_data
,
content_type
=
"application/merge-patch+json"
,
expected_status
=
204
):
"""
Helper method for sending a patch to the server, defaulting to application/merge-patch+json content_type.
Verifies the expected status and returns the response.
"""
response
=
client
.
patch
(
self
.
url
,
data
=
json
.
dumps
(
json_data
),
content_type
=
content_type
)
self
.
assertEqual
(
expected_status
,
response
.
status_code
)
return
response
def
send_get
(
self
,
client
,
expected_status
=
200
):
"""
Helper method for sending a GET to the server. Verifies the expected status and returns the response.
"""
response
=
client
.
get
(
self
.
url
)
self
.
assertEqual
(
expected_status
,
response
.
status_code
)
return
response
openedx/core/djangoapps/user_api/accounts/urls.py
0 → 100644
View file @
9d956e03
from
.views
import
AccountView
from
django.conf.urls
import
include
,
patterns
,
url
USERNAME_PATTERN
=
r'(?P<username>[\w.+-]+)'
urlpatterns
=
patterns
(
''
,
url
(
r'^v0/accounts/'
+
USERNAME_PATTERN
+
'$'
,
AccountView
.
as_view
(),
name
=
"accounts_api"
)
)
openedx/core/djangoapps/user_api/accounts/views.py
0 → 100644
View file @
9d956e03
"""
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.core.exceptions
import
ObjectDoesNotExist
from
django.contrib.auth.models
import
User
from
django.utils.translation
import
ugettext
as
_
from
rest_framework.views
import
APIView
from
rest_framework.response
import
Response
from
rest_framework
import
status
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework
import
permissions
from
rest_framework
import
parsers
from
student.models
import
UserProfile
from
openedx.core.djangoapps.user_api.accounts.serializers
import
AccountLegacyProfileSerializer
,
AccountUserSerializer
from
openedx.core.lib.api.permissions
import
IsUserInUrlOrStaff
from
openedx.core.lib.api.parsers
import
MergePatchParser
class
AccountView
(
APIView
):
"""
**Use Cases**
Get or update the user's account information. Updates are only supported through merge patch.
**Example Requests**:
GET /api/user/v0/accounts/{username}/
PATCH /api/user/v0/accounts/{username}/ with content_type "application/merge-patch+json"
**Response Values for GET**
* username: username associated with the account (not editable)
* name: full name of the user (not editable through this API)
* email: email for the user (not editable through this API)
* date_joined: date this account was created (not editable), in the string format provided by
datetime (for example, "2014-08-26T17:52:11Z")
* gender: null (not set), "m", "f", or "o"
* year_of_birth: null or integer year
* level_of_education: null (not set), or one of the following choices:
* "p" signifying "Doctorate"
* "m" signifying "Master's or professional degree"
* "b" signifying "Bachelor's degree"
* "a" signifying "Associate's degree"
* "hs" signifying "Secondary/high school"
* "jhs" signifying "Junior secondary/junior high/middle school"
* "el" signifying "Elementary/primary school"
* "none" signifying "None"
* "o" signifying "Other"
* language: null or name of preferred language
* country: null (not set), or a Country corresponding to one of the ISO 3166-1 countries
* mailing_address: null or textual representation of mailing address
* goals: null or textual representation of goals
**Response for PATCH**
Returns a 204 status if successful, with no additional content.
If "application/merge-patch+json" is not the specified content_type, returns a 415 status.
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,
IsUserInUrlOrStaff
)
parser_classes
=
(
MergePatchParser
,)
def
get
(
self
,
request
,
username
):
"""
GET /api/user/v0/accounts/{username}/
"""
existing_user
,
existing_user_profile
=
self
.
_get_user_and_profile
(
username
)
user_serializer
=
AccountUserSerializer
(
existing_user
)
legacy_profile_serializer
=
AccountLegacyProfileSerializer
(
existing_user_profile
)
return
Response
(
dict
(
user_serializer
.
data
,
**
legacy_profile_serializer
.
data
))
def
patch
(
self
,
request
,
username
):
"""
PATCH /api/user/v0/accounts/{username}/
Note that this implementation is the "merge patch" implementation proposed in
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.
"""
existing_user
,
existing_user_profile
=
self
.
_get_user_and_profile
(
username
)
# Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
update
=
request
.
DATA
read_only_fields
=
set
(
update
.
keys
())
.
intersection
(
AccountUserSerializer
.
Meta
.
read_only_fields
+
AccountLegacyProfileSerializer
.
Meta
.
read_only_fields
)
if
read_only_fields
:
field_errors
=
{}
for
read_only_field
in
read_only_fields
:
field_errors
[
read_only_field
]
=
{
"developer_message"
:
"This field is not editable via this API"
,
"user_message"
:
_
(
"Field '{field_name}' cannot be edited."
.
format
(
field_name
=
read_only_field
))
}
response_data
=
{
"field_errors"
:
field_errors
}
return
Response
(
response_data
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
user_serializer
=
AccountUserSerializer
(
existing_user
,
data
=
update
)
legacy_profile_serializer
=
AccountLegacyProfileSerializer
(
existing_user_profile
,
data
=
update
)
for
serializer
in
user_serializer
,
legacy_profile_serializer
:
validation_errors
=
self
.
_get_validation_errors
(
update
,
serializer
)
if
validation_errors
:
return
Response
(
validation_errors
,
status
=
status
.
HTTP_400_BAD_REQUEST
)
serializer
.
save
()
return
Response
(
status
=
status
.
HTTP_204_NO_CONTENT
)
def
_get_user_and_profile
(
self
,
username
):
"""
Helper method to return the legacy user and profile objects based on username.
"""
try
:
existing_user
=
User
.
objects
.
get
(
username
=
username
)
except
ObjectDoesNotExist
:
return
Response
({},
status
=
status
.
HTTP_404_NOT_FOUND
)
existing_user_profile
=
UserProfile
.
objects
.
get
(
user
=
existing_user
)
return
existing_user
,
existing_user_profile
def
_get_validation_errors
(
self
,
update
,
serializer
):
"""
Helper method that returns any validation errors that are present.
"""
validation_errors
=
{}
if
not
serializer
.
is_valid
():
field_errors
=
{}
errors
=
serializer
.
errors
for
key
,
value
in
errors
.
iteritems
():
if
isinstance
(
value
,
list
)
and
len
(
value
)
>
0
:
developer_message
=
value
[
0
]
else
:
developer_message
=
"Invalid value: {field_value}'"
.
format
(
field_value
=
update
[
key
])
field_errors
[
key
]
=
{
"developer_message"
:
developer_message
,
"user_message"
:
_
(
"Value '{field_value}' is not valid for field '{field_name}'."
.
format
(
field_value
=
update
[
key
],
field_name
=
key
)
)
}
validation_errors
[
'field_errors'
]
=
field_errors
return
validation_errors
\ No newline at end of file
openedx/core/lib/api/parsers.py
0 → 100644
View file @
9d956e03
from
rest_framework
import
parsers
class
MergePatchParser
(
parsers
.
JSONParser
):
"""
Custom parser to be used with the "merge patch" implementation (https://tools.ietf.org/html/rfc7396).
"""
media_type
=
'application/merge-patch+json'
openedx/core/lib/api/permissions.py
View file @
9d956e03
from
django.conf
import
settings
from
django.conf
import
settings
from
rest_framework
import
permissions
from
rest_framework
import
permissions
from
rest_framework.exceptions
import
PermissionDenied
from
rest_framework.exceptions
import
PermissionDenied
from
django.http
import
Http404
class
ApiKeyHeaderPermission
(
permissions
.
BasePermission
):
class
ApiKeyHeaderPermission
(
permissions
.
BasePermission
):
...
@@ -31,3 +32,26 @@ class IsAuthenticatedOrDebug(permissions.BasePermission):
...
@@ -31,3 +32,26 @@ class IsAuthenticatedOrDebug(permissions.BasePermission):
user
=
getattr
(
request
,
'user'
,
None
)
user
=
getattr
(
request
,
'user'
,
None
)
return
user
and
user
.
is_authenticated
()
return
user
and
user
.
is_authenticated
()
class
IsUserInUrl
(
permissions
.
BasePermission
):
"""
Permission that checks to see if the request user matches the user in the URL.
"""
def
has_permission
(
self
,
request
,
view
):
# 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.
if
request
.
user
.
username
!=
request
.
parser_context
.
get
(
'kwargs'
,
{})
.
get
(
'username'
,
None
):
raise
Http404
()
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