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
023e6ec8
Commit
023e6ec8
authored
Feb 20, 2015
by
cahrens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
GET method for user account API.
parent
18a916ba
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
282 additions
and
10 deletions
+282
-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
+26
-0
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
+111
-0
openedx/core/djangoapps/user_api/accounts/urls.py
+14
-0
openedx/core/djangoapps/user_api/accounts/views.py
+101
-0
openedx/core/lib/api/permissions.py
+24
-0
No files found.
lms/djangoapps/mobile_api/testutils.py
View file @
023e6ec8
...
@@ -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 @
023e6ec8
...
@@ -11,6 +11,7 @@ from rest_framework.authentication import OAuth2Authentication, SessionAuthentic
...
@@ -11,6 +11,7 @@ from rest_framework.authentication import OAuth2Authentication, SessionAuthentic
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
):
...
@@ -43,13 +44,6 @@ def mobile_view(is_user=False):
...
@@ -43,13 +44,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.
...
@@ -58,6 +52,6 @@ def mobile_view(is_user=False):
...
@@ -58,6 +52,6 @@ def mobile_view(is_user=False):
func_or_class
.
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
func_or_class
.
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
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 @
023e6ec8
...
@@ -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 @
023e6ec8
openedx/core/djangoapps/user_api/accounts/serializers.py
0 → 100644
View file @
023e6ec8
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"
,
"city"
,
"country"
,
"mailing_address"
)
read_only_fields
=
(
"name"
,)
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
0 → 100644
View file @
023e6ec8
import
unittest
import
ddt
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
):
USERNAME
=
"Christina"
EMAIL
=
"christina@example.com"
PASSWORD
=
TEST_PASSWORD
BAD_USERNAME
=
"Bad"
BAD_EMAIL
=
"bad@example.com"
BAD_PASSWORD
=
TEST_PASSWORD
STAFF_USERNAME
=
"Staff"
STAFF_EMAIL
=
"staff@example.com"
STAFF_PASSWORD
=
TEST_PASSWORD
def
setUp
(
self
):
super
(
TestAccountAPI
,
self
)
.
setUp
()
self
.
anonymous_client
=
APIClient
()
self
.
bad_user
=
UserFactory
.
create
(
password
=
TEST_PASSWORD
)
self
.
bad_client
=
APIClient
()
self
.
staff_user
=
UserFactory
(
is_staff
=
True
,
password
=
TEST_PASSWORD
)
self
.
staff_client
=
APIClient
()
self
.
user
=
UserFactory
.
create
(
password
=
TEST_PASSWORD
)
# Create some test profile values.
legacy_profile
=
UserProfile
.
objects
.
get
(
id
=
self
.
user
.
id
)
legacy_profile
.
city
=
"Indi"
legacy_profile
.
country
=
"US"
legacy_profile
.
year_of_birth
=
1900
legacy_profile
.
level_of_education
=
"m"
legacy_profile
.
goals
=
"world peace"
legacy_profile
.
mailing_address
=
"North Pole"
legacy_profile
.
save
()
self
.
accounts_base_uri
=
reverse
(
"accounts_api"
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
def
test_get_account_anonymous_user
(
self
):
response
=
self
.
anonymous_client
.
get
(
self
.
accounts_base_uri
)
self
.
assert_status_code
(
401
,
response
)
def
test_get_account_bad_user
(
self
):
self
.
bad_client
.
login
(
username
=
self
.
bad_user
.
username
,
password
=
TEST_PASSWORD
)
response
=
self
.
bad_client
.
get
(
self
.
accounts_base_uri
)
self
.
assert_status_code
(
404
,
response
)
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_get_account
(
self
,
api_client
,
user
):
client
=
self
.
login_client
(
api_client
,
user
)
response
=
client
.
get
(
self
.
accounts_base_uri
)
self
.
assert_status_code
(
200
,
response
)
data
=
response
.
data
self
.
assertEqual
(
12
,
len
(
data
))
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
# TODO: should we rename this "full_name"?
self
.
assertEqual
(
self
.
user
.
first_name
+
" "
+
self
.
user
.
last_name
,
data
[
"name"
])
self
.
assertEqual
(
"Indi"
,
data
[
"city"
])
self
.
assertEqual
(
"US"
,
data
[
"country"
])
# TODO: what should the format of this be?
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
(
"North Pole"
,
data
[
'mailing_address'
])
self
.
assertEqual
(
self
.
user
.
email
,
data
[
"email"
])
self
.
assertIsNotNone
(
data
[
"date_joined"
])
@ddt.data
(
(
"client"
,
"user"
),
(
"staff_client"
,
"staff_user"
),
)
@ddt.unpack
def
test_patch_account
(
self
,
api_client
,
user
):
client
=
self
.
login_client
(
api_client
,
user
)
response
=
client
.
patch
(
self
.
accounts_base_uri
,
data
=
{
"usernamae"
:
"willbeignored"
,
"gender"
:
"f"
})
self
.
assert_status_code
(
200
,
response
)
data
=
response
.
data
# Note that username is read-only, so passing it in patch is ignored. We want to change this behavior so it throws an exception.
self
.
assertEqual
(
self
.
user
.
username
,
data
[
"username"
])
self
.
assertEqual
(
"f"
,
data
[
"gender"
])
def
assert_status_code
(
self
,
expected_status_code
,
response
):
"""Assert that the given response has the expected status code"""
self
.
assertEqual
(
expected_status_code
,
response
.
status_code
)
def
login_client
(
self
,
api_client
,
user
):
client
=
getattr
(
self
,
api_client
)
user
=
getattr
(
self
,
user
)
client
.
login
(
username
=
user
.
username
,
password
=
TEST_PASSWORD
)
return
client
openedx/core/djangoapps/user_api/accounts/urls.py
0 → 100644
View file @
023e6ec8
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 @
023e6ec8
from
rest_framework.views
import
APIView
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.contrib.auth.models
import
User
from
rest_framework.response
import
Response
from
rest_framework
import
status
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
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework
import
permissions
class
AccountView
(
APIView
):
"""
**Use Cases**
Get the user's account information.
**Example Requests**:
GET /api/user/v0/accounts/{username}/
**Response Values**
* username: The username associated with the account (not editable).
* name: The full name of the user (not editable through this API).
* email: The email for the user (not editable through this API).
* date_joined: The date this account was created (not editable).
* gender: null, "m", "f", or "o":
* year_of_birth: null or integer year:
* level_of_education: null 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
* city: null or name of city
* country: null 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
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,
IsUserInUrlOrStaff
)
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}/
"""
existing_user
,
existing_user_profile
=
self
.
_get_user_and_profile
(
username
)
user_serializer
=
AccountUserSerializer
(
existing_user
,
data
=
request
.
DATA
)
user_serializer
.
is_valid
()
user_serializer
.
save
()
legacy_profile_serializer
=
AccountLegacyProfileSerializer
(
existing_user_profile
,
data
=
request
.
DATA
)
legacy_profile_serializer
.
is_valid
()
legacy_profile_serializer
.
save
()
return
Response
(
dict
(
user_serializer
.
data
,
**
legacy_profile_serializer
.
data
))
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
(
id
=
existing_user
.
id
)
return
existing_user
,
existing_user_profile
openedx/core/lib/api/permissions.py
View file @
023e6ec8
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