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
5349b55b
Commit
5349b55b
authored
Dec 11, 2014
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
MA-199 Course Authorization framework in mobile API.
parent
8d577563
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
640 additions
and
549 deletions
+640
-549
lms/djangoapps/courseware/access.py
+36
-0
lms/djangoapps/mobile_api/course_info/tests.py
+61
-59
lms/djangoapps/mobile_api/course_info/views.py
+15
-25
lms/djangoapps/mobile_api/tests.py
+17
-25
lms/djangoapps/mobile_api/testutils.py
+204
-0
lms/djangoapps/mobile_api/users/tests.py
+129
-224
lms/djangoapps/mobile_api/users/views.py
+44
-98
lms/djangoapps/mobile_api/utils.py
+77
-17
lms/djangoapps/mobile_api/video_outlines/tests.py
+47
-71
lms/djangoapps/mobile_api/video_outlines/views.py
+10
-30
No files found.
lms/djangoapps/courseware/access.py
View file @
5349b55b
...
@@ -19,6 +19,7 @@ from xblock.core import XBlock
...
@@ -19,6 +19,7 @@ from xblock.core import XBlock
from
external_auth.models
import
ExternalAuthMap
from
external_auth.models
import
ExternalAuthMap
from
courseware.masquerade
import
is_masquerading_as_student
from
courseware.masquerade
import
is_masquerading_as_student
from
django.utils.timezone
import
UTC
from
django.utils.timezone
import
UTC
from
student
import
auth
from
student.roles
import
(
from
student.roles
import
(
GlobalStaff
,
CourseStaffRole
,
CourseInstructorRole
,
GlobalStaff
,
CourseStaffRole
,
CourseInstructorRole
,
OrgStaffRole
,
OrgInstructorRole
,
CourseBetaTesterRole
OrgStaffRole
,
OrgInstructorRole
,
CourseBetaTesterRole
...
@@ -46,6 +47,7 @@ def has_access(user, action, obj, course_key=None):
...
@@ -46,6 +47,7 @@ def has_access(user, action, obj, course_key=None):
- visible_to_staff_only for modules
- visible_to_staff_only for modules
- DISABLE_START_DATES
- DISABLE_START_DATES
- different access for instructor, staff, course staff, and students.
- different access for instructor, staff, course staff, and students.
- mobile_available flag for course modules
user: a Django user object. May be anonymous. If none is passed,
user: a Django user object. May be anonymous. If none is passed,
anonymous is assumed
anonymous is assumed
...
@@ -108,6 +110,8 @@ def _has_access_course_desc(user, action, course):
...
@@ -108,6 +110,8 @@ def _has_access_course_desc(user, action, course):
'load' -- load the courseware, see inside the course
'load' -- load the courseware, see inside the course
'load_forum' -- can load and contribute to the forums (one access level for now)
'load_forum' -- can load and contribute to the forums (one access level for now)
'load_mobile' -- can load from a mobile context
'load_mobile_no_enrollment_check' -- can load from a mobile context without checking for enrollment
'enroll' -- enroll. Checks for enrollment window,
'enroll' -- enroll. Checks for enrollment window,
ACCESS_REQUIRE_STAFF_FOR_COURSE,
ACCESS_REQUIRE_STAFF_FOR_COURSE,
'see_exists' -- can see that the course exists.
'see_exists' -- can see that the course exists.
...
@@ -136,6 +140,36 @@ def _has_access_course_desc(user, action, course):
...
@@ -136,6 +140,36 @@ def _has_access_course_desc(user, action, course):
)
)
)
)
def
can_load_mobile
():
"""
Can this user access this course from a mobile device?
"""
return
(
# check mobile requirements
can_load_mobile_no_enroll_check
()
and
# check enrollment
(
CourseEnrollment
.
is_enrolled
(
user
,
course
.
id
)
or
_has_staff_access_to_descriptor
(
user
,
course
,
course
.
id
)
)
)
def
can_load_mobile_no_enroll_check
():
"""
Can this enrolled user access this course from a mobile device?
Note: does not check for enrollment since it is assumed the caller has done so.
"""
return
(
# check start date
can_load
()
and
# check mobile_available flag
(
course
.
mobile_available
or
auth
.
has_access
(
user
,
CourseBetaTesterRole
(
course
.
id
))
or
_has_staff_access_to_descriptor
(
user
,
course
,
course
.
id
)
)
)
def
can_enroll
():
def
can_enroll
():
"""
"""
First check if restriction of enrollment by login method is enabled, both
First check if restriction of enrollment by login method is enabled, both
...
@@ -234,6 +268,8 @@ def _has_access_course_desc(user, action, course):
...
@@ -234,6 +268,8 @@ def _has_access_course_desc(user, action, course):
checkers
=
{
checkers
=
{
'load'
:
can_load
,
'load'
:
can_load
,
'load_forum'
:
can_load_forum
,
'load_forum'
:
can_load_forum
,
'load_mobile'
:
can_load_mobile
,
'load_mobile_no_enrollment_check'
:
can_load_mobile_no_enroll_check
,
'enroll'
:
can_enroll
,
'enroll'
:
can_enroll
,
'see_exists'
:
see_exists
,
'see_exists'
:
see_exists
,
'staff'
:
lambda
:
_has_staff_access_to_descriptor
(
user
,
course
,
course
.
id
),
'staff'
:
lambda
:
_has_staff_access_to_descriptor
(
user
,
course
,
course
.
id
),
...
...
lms/djangoapps/mobile_api/course_info/tests.py
View file @
5349b55b
"""
"""
Tests for course_info
Tests for course_info
"""
"""
import
json
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
rest_framework.test
import
APITestCase
from
courseware.tests.factories
import
UserFactory
from
xmodule.html_module
import
CourseInfoModule
from
xmodule.html_module
import
CourseInfoModule
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.xml_importer
import
import_from_xml
from
xmodule.modulestore.xml_importer
import
import_from_xml
from
..testutils
import
(
MobileAPITestCase
,
MobileCourseAccessTestMixin
,
MobileEnrolledCourseAccessTestMixin
,
MobileAuthTestMixin
)
class
TestCourseInfo
(
APITestCase
):
class
TestAbout
(
MobileAPITestCase
,
MobileAuthTestMixin
,
MobileCourseAccessTestMixin
):
"""
"""
Tests for /api/mobile/v0.5/course_info/
...
Tests for /api/mobile/v0.5/course_info/
{course_id}/about
"""
"""
def
setUp
(
self
):
REVERSE_INFO
=
{
'name'
:
'course-about-detail'
,
'params'
:
[
'course_id'
]}
super
(
TestCourseInfo
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
def
verify_success
(
self
,
response
):
self
.
course
=
CourseFactory
.
create
(
mobile_available
=
True
)
super
(
TestAbout
,
self
)
.
verify_success
(
response
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
'test'
)
self
.
assertTrue
(
'overview'
in
response
.
data
)
def
test_about
(
self
):
def
init_course_access
(
self
,
course_id
=
None
):
url
=
reverse
(
'course-about-detail'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
# override this method since enrollment is not required for the About endpoint.
response
=
self
.
client
.
get
(
url
)
self
.
login
()
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertTrue
(
'overview'
in
response
.
data
)
# pylint: disable=maybe-no-member
def
test_about_static_rewrite
(
self
):
self
.
login
()
def
test_updates
(
self
):
url
=
reverse
(
'course-updates-list'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
data
,
[])
# pylint: disable=maybe-no-member
def
test_about_static_rewrites
(
self
):
about_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'about'
,
'overview'
)
about_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'about'
,
'overview'
)
about_module
=
modulestore
()
.
get_item
(
about_usage_key
)
about_module
=
modulestore
()
.
get_item
(
about_usage_key
)
underlying_about_html
=
about_module
.
data
underlying_about_html
=
about_module
.
data
...
@@ -45,16 +38,24 @@ class TestCourseInfo(APITestCase):
...
@@ -45,16 +38,24 @@ class TestCourseInfo(APITestCase):
# check that we start with relative static assets
# check that we start with relative static assets
self
.
assertIn
(
'
\"
/static/'
,
underlying_about_html
)
self
.
assertIn
(
'
\"
/static/'
,
underlying_about_html
)
url
=
reverse
(
'course-about-detail'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
get
(
url
)
json_data
=
json
.
loads
(
response
.
content
)
about_html
=
json_data
[
'overview'
]
# but shouldn't finish with any
# but shouldn't finish with any
self
.
assertEqual
(
response
.
status_code
,
200
)
response
=
self
.
api_response
()
self
.
assertNotIn
(
'
\"
/static/'
,
about_html
)
self
.
assertNotIn
(
'
\"
/static/'
,
response
.
data
[
'overview'
])
class
TestUpdates
(
MobileAPITestCase
,
MobileAuthTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
"""
Tests for /api/mobile/v0.5/course_info/{course_id}/updates
"""
REVERSE_INFO
=
{
'name'
:
'course-updates-list'
,
'params'
:
[
'course_id'
]}
def
verify_success
(
self
,
response
):
super
(
TestUpdates
,
self
)
.
verify_success
(
response
)
self
.
assertEqual
(
response
.
data
,
[])
def
test_updates_static_rewrite
(
self
):
self
.
login_and_enroll
()
def
test_updates_rewrite
(
self
):
updates_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'course_info'
,
'updates'
)
updates_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'course_info'
,
'updates'
)
course_updates
=
modulestore
()
.
create_item
(
course_updates
=
modulestore
()
.
create_item
(
self
.
user
.
id
,
self
.
user
.
id
,
...
@@ -72,50 +73,51 @@ class TestCourseInfo(APITestCase):
...
@@ -72,50 +73,51 @@ class TestCourseInfo(APITestCase):
course_updates
.
items
=
[
course_update_data
]
course_updates
.
items
=
[
course_update_data
]
modulestore
()
.
update_item
(
course_updates
,
self
.
user
.
id
)
modulestore
()
.
update_item
(
course_updates
,
self
.
user
.
id
)
url
=
reverse
(
'course-updates-list'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
api_response
()
response
=
self
.
client
.
get
(
url
)
content
=
response
.
data
[
0
][
"content"
]
# pylint: disable=maybe-no-member
content
=
response
.
data
[
0
][
"content"
]
# pylint: disable=maybe-no-member
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertNotIn
(
"
\"
/static/"
,
content
)
self
.
assertNotIn
(
"
\"
/static/"
,
content
)
underlying_updates_module
=
modulestore
()
.
get_item
(
updates_usage_key
)
underlying_updates_module
=
modulestore
()
.
get_item
(
updates_usage_key
)
self
.
assertIn
(
"
\"
/static/"
,
underlying_updates_module
.
items
[
0
][
'content'
])
self
.
assertIn
(
"
\"
/static/"
,
underlying_updates_module
.
items
[
0
][
'content'
])
class
TestHandout
Info
(
ModuleStoreTestCase
,
APITestCase
):
class
TestHandout
s
(
MobileAPITestCase
,
MobileAuthTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
"""
"""
Tests for /api/mobile/v0.5/course_info/{course_id}/handouts
Tests for /api/mobile/v0.5/course_info/{course_id}/handouts
"""
"""
REVERSE_INFO
=
{
'name'
:
'course-handouts-list'
,
'params'
:
[
'course_id'
]}
def
setUp
(
self
):
def
setUp
(
self
):
super
(
TestHandout
Info
,
self
)
.
setUp
()
super
(
TestHandout
s
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
'test'
)
# use toy course with handouts, and make it mobile_available
course_items
=
import_from_xml
(
self
.
store
,
self
.
user
.
id
,
settings
.
COMMON_TEST_DATA_ROOT
,
[
'toy'
])
course_items
=
import_from_xml
(
self
.
store
,
self
.
user
.
id
,
settings
.
COMMON_TEST_DATA_ROOT
,
[
'toy'
])
self
.
course
=
course_items
[
0
]
self
.
course
=
course_items
[
0
]
self
.
course
.
mobile_available
=
True
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
def
verify_success
(
self
,
response
):
super
(
TestHandouts
,
self
)
.
verify_success
(
response
)
self
.
assertIn
(
'Sample'
,
response
.
data
[
'handouts_html'
])
def
test_no_handouts
(
self
):
def
test_no_handouts
(
self
):
empty_course
=
CourseFactory
.
create
(
mobile_available
=
True
)
self
.
login_and_enroll
()
url
=
reverse
(
'course-handouts-list'
,
kwargs
=
{
'course_id'
:
unicode
(
empty_course
.
id
)})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_handout_exists
(
self
):
# delete handouts in course
url
=
reverse
(
'course-handouts-list'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
handouts_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'course_info'
,
'handouts'
)
response
=
self
.
client
.
get
(
url
)
with
self
.
store
.
branch_setting
(
ModuleStoreEnum
.
Branch
.
draft_preferred
,
self
.
course
.
id
):
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
store
.
delete_item
(
handouts_usage_key
,
self
.
user
.
id
)
self
.
api_response
(
expected_response_code
=
404
)
def
test_handouts_static_rewrites
(
self
):
self
.
login_and_enroll
()
def
test_handout_static_rewrites
(
self
):
# check that we start with relative static assets
# check that we start with relative static assets
handouts_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'course_info'
,
'handouts'
)
handouts_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'course_info'
,
'handouts'
)
underlying_handouts
=
self
.
store
.
get_item
(
handouts_usage_key
)
underlying_handouts
=
self
.
store
.
get_item
(
handouts_usage_key
)
self
.
assertIn
(
'
\'
/static/'
,
underlying_handouts
.
data
)
self
.
assertIn
(
'
\'
/static/'
,
underlying_handouts
.
data
)
url
=
reverse
(
'course-handouts-list'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
get
(
url
)
json_data
=
json
.
loads
(
response
.
content
)
handouts_html
=
json_data
[
'handouts_html'
]
# but shouldn't finish with any
# but shouldn't finish with any
self
.
assertNotIn
(
'
\'
/static/'
,
handouts_html
)
response
=
self
.
api_response
(
)
self
.
assert
Equal
(
response
.
status_code
,
200
)
self
.
assert
NotIn
(
'
\'
/static/'
,
response
.
data
[
'handouts_html'
]
)
lms/djangoapps/mobile_api/course_info/views.py
View file @
5349b55b
...
@@ -2,17 +2,16 @@
...
@@ -2,17 +2,16 @@
Views for course info API
Views for course info API
"""
"""
from
django.http
import
Http404
from
django.http
import
Http404
from
rest_framework
import
generics
,
permissions
from
rest_framework
import
generics
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
courseware.courses
import
get_course_about_section
,
get_course_info_section_module
from
courseware.courses
import
get_course_about_section
,
get_course_info_section_module
from
opaque_keys.edx.keys
import
CourseKey
from
xmodule.modulestore.django
import
modulestore
from
static_replace
import
make_static_urls_absolute
,
replace_static_urls
from
static_replace
import
make_static_urls_absolute
,
replace_static_urls
from
..utils
import
MobileView
,
mobile_course_access
@MobileView
()
class
CourseUpdatesList
(
generics
.
ListAPIView
):
class
CourseUpdatesList
(
generics
.
ListAPIView
):
"""
"""
**Use Case**
**Use Case**
...
@@ -35,12 +34,9 @@ class CourseUpdatesList(generics.ListAPIView):
...
@@ -35,12 +34,9 @@ class CourseUpdatesList(generics.ListAPIView):
* id: The unique identifier of the update.
* id: The unique identifier of the update.
"""
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
@mobile_course_access
()
course_id
=
CourseKey
.
from_string
(
kwargs
[
'course_id'
])
def
list
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
course
=
modulestore
()
.
get_course
(
course_id
)
course_updates_module
=
get_course_info_section_module
(
request
,
course
,
'updates'
)
course_updates_module
=
get_course_info_section_module
(
request
,
course
,
'updates'
)
update_items
=
reversed
(
getattr
(
course_updates_module
,
'items'
,
[]))
update_items
=
reversed
(
getattr
(
course_updates_module
,
'items'
,
[]))
...
@@ -53,13 +49,14 @@ class CourseUpdatesList(generics.ListAPIView):
...
@@ -53,13 +49,14 @@ class CourseUpdatesList(generics.ListAPIView):
content
=
item
[
'content'
]
content
=
item
[
'content'
]
content
=
replace_static_urls
(
content
=
replace_static_urls
(
content
,
content
,
course_id
=
course
_
id
,
course_id
=
course
.
id
,
static_asset_path
=
course
.
static_asset_path
)
static_asset_path
=
course
.
static_asset_path
)
item
[
'content'
]
=
make_static_urls_absolute
(
request
,
content
)
item
[
'content'
]
=
make_static_urls_absolute
(
request
,
content
)
return
Response
(
updates_to_show
)
return
Response
(
updates_to_show
)
@MobileView
()
class
CourseHandoutsList
(
generics
.
ListAPIView
):
class
CourseHandoutsList
(
generics
.
ListAPIView
):
"""
"""
**Use Case**
**Use Case**
...
@@ -74,27 +71,24 @@ class CourseHandoutsList(generics.ListAPIView):
...
@@ -74,27 +71,24 @@ class CourseHandoutsList(generics.ListAPIView):
* handouts_html: The HTML for course handouts.
* handouts_html: The HTML for course handouts.
"""
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
@mobile_course_access
()
course_id
=
CourseKey
.
from_string
(
kwargs
[
'course_id'
])
def
list
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
course
=
modulestore
()
.
get_course
(
course_id
)
course_handouts_module
=
get_course_info_section_module
(
request
,
course
,
'handouts'
)
course_handouts_module
=
get_course_info_section_module
(
request
,
course
,
'handouts'
)
if
course_handouts_module
:
if
course_handouts_module
:
handouts_html
=
course_handouts_module
.
data
handouts_html
=
course_handouts_module
.
data
handouts_html
=
replace_static_urls
(
handouts_html
=
replace_static_urls
(
handouts_html
,
handouts_html
,
course_id
=
course
_
id
,
course_id
=
course
.
id
,
static_asset_path
=
course
.
static_asset_path
)
static_asset_path
=
course
.
static_asset_path
)
handouts_html
=
make_static_urls_absolute
(
self
.
request
,
handouts_html
)
handouts_html
=
make_static_urls_absolute
(
self
.
request
,
handouts_html
)
return
Response
({
'handouts_html'
:
handouts_html
})
return
Response
({
'handouts_html'
:
handouts_html
})
else
:
else
:
# course_handouts_module could be None if there are no handouts
# course_handouts_module could be None if there are no handouts
# (such as while running tests)
raise
Http404
(
u"No handouts for {}"
.
format
(
unicode
(
course
.
id
)))
raise
Http404
(
u"No handouts for {}"
.
format
(
unicode
(
course_id
)))
@MobileView
()
class
CourseAboutDetail
(
generics
.
RetrieveAPIView
):
class
CourseAboutDetail
(
generics
.
RetrieveAPIView
):
"""
"""
**Use Case**
**Use Case**
...
@@ -109,13 +103,9 @@ class CourseAboutDetail(generics.RetrieveAPIView):
...
@@ -109,13 +103,9 @@ class CourseAboutDetail(generics.RetrieveAPIView):
* overview: The HTML for the course About page.
* overview: The HTML for the course About page.
"""
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
course_id
=
CourseKey
.
from_string
(
kwargs
[
'course_id'
])
course
=
modulestore
()
.
get_course
(
course_id
)
@mobile_course_access
(
verify_enrolled
=
False
)
def
get
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
# There are other fields, but they don't seem to be in use.
# There are other fields, but they don't seem to be in use.
# see courses.py:get_course_about_section.
# see courses.py:get_course_about_section.
#
#
...
...
lms/djangoapps/mobile_api/tests.py
View file @
5349b55b
"""
"""
Tests for mobile API utilities
Tests for mobile API utilities
.
"""
"""
import
ddt
import
ddt
from
rest_framework.test
import
APITestCase
from
mock
import
patch
from
courseware.tests.factories
import
UserFactory
from
student
import
auth
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
.utils
import
mobile_available_when_enrolled
from
.utils
import
mobile_access_when_enrolled
from
.testutils
import
MobileAPITestCase
,
ROLE_CASES
ROLE_CASES
=
(
(
auth
.
CourseBetaTesterRole
,
True
),
(
auth
.
CourseStaffRole
,
True
),
(
auth
.
CourseInstructorRole
,
True
),
(
None
,
False
)
)
@ddt.ddt
@ddt.ddt
class
TestMobileApiUtils
(
Mo
duleStoreTestCase
,
APITestCase
):
class
TestMobileApiUtils
(
Mo
bile
APITestCase
):
"""
"""
Tests for mobile API utilities
Tests for mobile API utilities
"""
"""
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
()
@ddt.data
(
*
ROLE_CASES
)
@ddt.data
(
*
ROLE_CASES
)
@ddt.unpack
@ddt.unpack
def
test_mobile_role_access
(
self
,
role
,
should_have_access
):
def
test_mobile_role_access
(
self
,
role
,
should_have_access
):
"""
"""
Verifies that our mobile access function properly handles using roles to grant access
Verifies that our mobile access function properly handles using roles to grant access
"""
"""
course
=
CourseFactory
.
create
(
mobile_available
=
False
)
non_mobile_
course
=
CourseFactory
.
create
(
mobile_available
=
False
)
if
role
:
if
role
:
role
(
course
.
id
)
.
add_users
(
self
.
user
)
role
(
non_mobile_
course
.
id
)
.
add_users
(
self
.
user
)
self
.
assertEqual
(
should_have_access
,
mobile_a
vailable_when_enrolled
(
course
,
self
.
user
))
self
.
assertEqual
(
should_have_access
,
mobile_a
ccess_when_enrolled
(
non_mobile_
course
,
self
.
user
))
def
test_mobile_explicit_access
(
self
):
def
test_mobile_explicit_access
(
self
):
"""
"""
Verifies that our mobile access function listens to the mobile_available flag as it should
Verifies that our mobile access function listens to the mobile_available flag as it should
"""
"""
course
=
CourseFactory
.
create
(
mobile_available
=
True
)
self
.
assertTrue
(
mobile_access_when_enrolled
(
self
.
course
,
self
.
user
))
self
.
assertTrue
(
mobile_available_when_enrolled
(
course
,
self
.
user
))
def
test_missing_course
(
self
):
def
test_missing_course
(
self
):
"""
"""
Verifies that we handle the case where a course doesn't exist
Verifies that we handle the case where a course doesn't exist
"""
"""
self
.
assertFalse
(
mobile_available_when_enrolled
(
None
,
self
.
user
))
self
.
assertFalse
(
mobile_access_when_enrolled
(
None
,
self
.
user
))
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'DISABLE_START_DATES'
:
False
})
def
test_unreleased_course
(
self
):
"""
Verifies that we handle the case where a course hasn't started
"""
self
.
assertFalse
(
mobile_access_when_enrolled
(
self
.
course
,
self
.
user
))
lms/djangoapps/mobile_api/testutils.py
0 → 100644
View file @
5349b55b
"""
Test utilities for mobile API tests:
MobileAPITestCase - Common base class with helper methods and common functionality.
No tests are implemented in this base class.
Test Mixins to be included by concrete test classes and provide implementation of common test methods:
MobileAuthTestMixin - tests for APIs with MobileView/mobile_view and is_user=False.
MobileAuthUserTestMixin - tests for APIs with MobileView/mobile_view and is_user=True.
MobileCourseAccessTestMixin - tests for APIs with mobile_course_access and verify_enrolled=False.
MobileEnrolledCourseAccessTestMixin - tests for APIs with mobile_course_access and verify_enrolled=True.
"""
# pylint: disable=no-member
import
ddt
from
mock
import
patch
from
rest_framework.test
import
APITestCase
from
django.core.urlresolvers
import
reverse
from
opaque_keys.edx.keys
import
CourseKey
from
courseware.tests.factories
import
UserFactory
from
student
import
auth
from
student.models
import
CourseEnrollment
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
# A tuple of Role Types and Boolean values that indicate whether access should be given to that role.
ROLE_CASES
=
(
(
auth
.
CourseBetaTesterRole
,
True
),
(
auth
.
CourseStaffRole
,
True
),
(
auth
.
CourseInstructorRole
,
True
),
(
None
,
False
)
)
class
MobileAPITestCase
(
ModuleStoreTestCase
,
APITestCase
):
"""
Base class for testing Mobile APIs.
Subclasses are expected to define REVERSE_INFO to be used for django reverse URL, of the form:
REVERSE_INFO = {'name': <django reverse name>, 'params': [<list of params in the URL>]}
They may also override any of the methods defined in this class to control the behavior of the TestMixins.
"""
def
setUp
(
self
):
super
(
MobileAPITestCase
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
(
mobile_available
=
True
)
self
.
user
=
UserFactory
.
create
()
self
.
password
=
'test'
self
.
username
=
self
.
user
.
username
def
tearDown
(
self
):
super
(
MobileAPITestCase
,
self
)
.
tearDown
()
self
.
logout
()
def
login
(
self
):
"""Login test user."""
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
def
logout
(
self
):
"""Logout test user."""
self
.
client
.
logout
()
def
enroll
(
self
,
course_id
=
None
):
"""Enroll test user in test course."""
CourseEnrollment
.
enroll
(
self
.
user
,
course_id
or
self
.
course
.
id
)
def
unenroll
(
self
,
course_id
=
None
):
"""Unenroll test user in test course."""
CourseEnrollment
.
unenroll
(
self
.
user
,
course_id
or
self
.
course
.
id
)
def
login_and_enroll
(
self
,
course_id
=
None
):
"""Shortcut for both login and enrollment of the user."""
self
.
login
()
self
.
enroll
(
course_id
)
def
api_response
(
self
,
reverse_args
=
None
,
expected_response_code
=
200
,
**
kwargs
):
"""
Helper method for calling endpoint, verifying and returning response.
If expected_response_code is None, doesn't verify the response' status_code.
"""
url
=
self
.
reverse_url
(
reverse_args
,
**
kwargs
)
response
=
self
.
url_method
(
url
,
**
kwargs
)
if
expected_response_code
is
not
None
:
self
.
assertEqual
(
response
.
status_code
,
expected_response_code
)
return
response
def
reverse_url
(
self
,
reverse_args
=
None
,
**
kwargs
):
# pylint: disable=unused-argument
"""Base implementation that returns URL for endpoint that's being tested."""
reverse_args
=
reverse_args
or
{}
if
'course_id'
in
self
.
REVERSE_INFO
[
'params'
]:
reverse_args
.
update
({
'course_id'
:
unicode
(
kwargs
.
get
(
'course_id'
,
self
.
course
.
id
))})
if
'username'
in
self
.
REVERSE_INFO
[
'params'
]:
reverse_args
.
update
({
'username'
:
kwargs
.
get
(
'username'
,
self
.
user
.
username
)})
return
reverse
(
self
.
REVERSE_INFO
[
'name'
],
kwargs
=
reverse_args
)
def
url_method
(
self
,
url
,
**
kwargs
):
# pylint: disable=unused-argument
"""Base implementation that returns response from the GET method of the URL."""
return
self
.
client
.
get
(
url
)
class
MobileAuthTestMixin
(
object
):
"""
Test Mixin for testing APIs decorated with MobileView or mobile_view.
"""
def
test_no_auth
(
self
):
self
.
logout
()
self
.
api_response
(
expected_response_code
=
401
)
class
MobileAuthUserTestMixin
(
MobileAuthTestMixin
):
"""
Test Mixin for testing APIs related to users: mobile_view or MobileView with is_user=True.
"""
def
test_invalid_user
(
self
):
self
.
login_and_enroll
()
self
.
api_response
(
expected_response_code
=
403
,
username
=
'no_user'
)
def
test_other_user
(
self
):
# login and enroll as the test user
self
.
login_and_enroll
()
self
.
logout
()
# login and enroll as another user
other
=
UserFactory
.
create
()
self
.
client
.
login
(
username
=
other
.
username
,
password
=
'test'
)
self
.
enroll
()
self
.
logout
()
# now login and call the API as the test user
self
.
login
()
self
.
api_response
(
expected_response_code
=
403
,
username
=
other
.
username
)
@ddt.ddt
class
MobileCourseAccessTestMixin
(
object
):
"""
Test Mixin for testing APIs marked with mobile_course_access.
(Use MobileEnrolledCourseAccessTestMixin when verify_enrolled is set to True.)
Subclasses are expected to inherit from MobileAPITestCase.
Subclasses can override verify_success, verify_failure, and init_course_access methods.
"""
def
verify_success
(
self
,
response
):
"""Base implementation of verifying a successful response."""
self
.
assertEqual
(
response
.
status_code
,
200
)
def
verify_failure
(
self
,
response
):
"""Base implementation of verifying a failed response."""
self
.
assertEqual
(
response
.
status_code
,
404
)
def
init_course_access
(
self
,
course_id
=
None
):
"""Base implementation of initializing the user for each test."""
self
.
login_and_enroll
(
course_id
)
def
test_success
(
self
):
self
.
init_course_access
()
response
=
self
.
api_response
(
expected_response_code
=
None
)
self
.
verify_success
(
response
)
# allow subclasses to override verification
def
test_course_not_found
(
self
):
non_existent_course_id
=
CourseKey
.
from_string
(
'a/b/c'
)
self
.
init_course_access
(
course_id
=
non_existent_course_id
)
response
=
self
.
api_response
(
expected_response_code
=
None
,
course_id
=
non_existent_course_id
)
self
.
verify_failure
(
response
)
# allow subclasses to override verification
@patch.dict
(
'django.conf.settings.FEATURES'
,
{
'DISABLE_START_DATES'
:
False
})
def
test_unreleased_course
(
self
):
self
.
init_course_access
()
response
=
self
.
api_response
(
expected_response_code
=
None
)
self
.
verify_failure
(
response
)
# allow subclasses to override verification
@ddt.data
(
*
ROLE_CASES
)
@ddt.unpack
def
test_non_mobile_available
(
self
,
role
,
should_succeed
):
self
.
init_course_access
()
# set mobile_available to False for the test course
self
.
course
.
mobile_available
=
False
self
.
store
.
update_item
(
self
.
course
,
self
.
user
.
id
)
# set user's role in the course
if
role
:
role
(
self
.
course
.
id
)
.
add_users
(
self
.
user
)
# call API and verify response
response
=
self
.
api_response
(
expected_response_code
=
None
)
if
should_succeed
:
self
.
verify_success
(
response
)
else
:
self
.
verify_failure
(
response
)
class
MobileEnrolledCourseAccessTestMixin
(
MobileCourseAccessTestMixin
):
"""
Test Mixin for testing APIs marked with mobile_course_access with verify_enrolled=True.
"""
def
test_unenrolled_user
(
self
):
self
.
login
()
self
.
unenroll
()
response
=
self
.
api_response
(
expected_response_code
=
None
)
self
.
verify_failure
(
response
)
lms/djangoapps/mobile_api/users/tests.py
View file @
5349b55b
...
@@ -3,149 +3,74 @@ Tests for users API
...
@@ -3,149 +3,74 @@ Tests for users API
"""
"""
import
datetime
import
datetime
import
ddt
from
django.utils
import
timezone
import
json
from
rest_framework.test
import
APITestCase
from
xmodule.modulestore.tests.factories
import
ItemFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
courseware.tests.factories
import
UserFactory
from
django.core.urlresolvers
import
reverse
from
django.utils
import
timezone
from
mobile_api.users.serializers
import
CourseEnrollmentSerializer
from
mobile_api
import
errors
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
mobile_api.tests
import
ROLE_CASES
from
..
import
errors
from
..testutils
import
MobileAPITestCase
,
MobileAuthTestMixin
,
MobileAuthUserTestMixin
,
MobileEnrolledCourseAccessTestMixin
from
.serializers
import
CourseEnrollmentSerializer
@ddt.ddt
class
TestUserApi
(
ModuleStoreTestCase
,
APITestCase
):
"""
Test the user info API
"""
def
setUp
(
self
):
super
(
TestUserApi
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
(
mobile_available
=
True
)
self
.
user
=
UserFactory
.
create
()
self
.
password
=
'test'
self
.
username
=
self
.
user
.
username
def
tearDown
(
self
):
super
(
TestUserApi
,
self
)
.
tearDown
()
self
.
client
.
logout
()
def
_enrollment_url
(
self
):
"""
api url that gets the current user's course enrollments
"""
return
reverse
(
'courseenrollment-detail'
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
def
_enroll
(
self
,
course
):
class
TestUserDetailApi
(
MobileAPITestCase
,
MobileAuthUserTestMixin
):
"""
"""
enroll test user in test course
Tests for /api/mobile/v0.5/users/<user_name>...
"""
"""
resp
=
self
.
client
.
post
(
reverse
(
'change_enrollment'
),
{
REVERSE_INFO
=
{
'name'
:
'user-detail'
,
'params'
:
[
'username'
]}
'enrollment_action'
:
'enroll'
,
'course_id'
:
course
.
id
.
to_deprecated_string
(),
def
test_success
(
self
):
'check_access'
:
True
,
self
.
login
()
})
self
.
assertEqual
(
resp
.
status_code
,
200
)
response
=
self
.
api_response
()
self
.
assertEqual
(
response
.
data
[
'username'
],
self
.
user
.
username
)
def
_verify_single_course_enrollment
(
self
,
course
,
should_succeed
):
self
.
assertEqual
(
response
.
data
[
'email'
],
self
.
user
.
email
)
class
TestUserInfoApi
(
MobileAPITestCase
,
MobileAuthTestMixin
):
"""
"""
check that enrolling in course adds us to it
Tests for /api/mobile/v0.5/my_user_info
"""
"""
def
reverse_url
(
self
,
reverse_args
=
None
,
**
kwargs
):
return
'/api/mobile/v0.5/my_user_info'
url
=
self
.
_enrollment_url
()
def
test_success
(
self
):
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
"""Verify the endpoint redirects to the user detail endpoint"""
self
.
_enroll
(
course
)
self
.
login
()
response
=
self
.
client
.
get
(
url
)
courses
=
response
.
data
# pylint: disable=maybe-no-member
response
=
self
.
api_response
(
expected_response_code
=
302
)
self
.
assertTrue
(
self
.
username
in
response
[
'location'
])
self
.
assertEqual
(
response
.
status_code
,
200
)
if
should_succeed
:
class
TestUserEnrollmentApi
(
MobileAPITestCase
,
MobileAuthUserTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
"""
Tests for /api/mobile/v0.5/users/<user_name>/course_enrollments/
"""
REVERSE_INFO
=
{
'name'
:
'courseenrollment-detail'
,
'params'
:
[
'username'
]}
def
verify_success
(
self
,
response
):
super
(
TestUserEnrollmentApi
,
self
)
.
verify_success
(
response
)
courses
=
response
.
data
self
.
assertEqual
(
len
(
courses
),
1
)
self
.
assertEqual
(
len
(
courses
),
1
)
found_course
=
courses
[
0
][
'course'
]
found_course
=
courses
[
0
][
'course'
]
self
.
assertTrue
(
'video_outline'
in
found_course
)
self
.
assertTrue
(
'video_outline'
in
found_course
)
self
.
assertTrue
(
'course_handouts'
in
found_course
)
self
.
assertTrue
(
'course_handouts'
in
found_course
)
self
.
assertEqual
(
found_course
[
'id'
],
unicode
(
course
.
id
))
self
.
assertEqual
(
found_course
[
'id'
],
unicode
(
self
.
course
.
id
))
self
.
assertEqual
(
courses
[
0
][
'mode'
],
'honor'
)
self
.
assertEqual
(
courses
[
0
][
'mode'
],
'honor'
)
else
:
self
.
assertEqual
(
len
(
courses
),
0
)
@ddt.data
(
*
ROLE_CASES
)
@ddt.unpack
def
test_non_mobile_enrollments
(
self
,
role
,
should_succeed
):
non_mobile_course
=
CourseFactory
.
create
(
mobile_available
=
False
)
if
role
:
role
(
non_mobile_course
.
id
)
.
add_users
(
self
.
user
)
self
.
_verify_single_course_enrollment
(
non_mobile_course
,
should_succeed
)
def
verify_failure
(
self
,
response
):
def
test_mobile_enrollments
(
self
):
self
.
_verify_single_course_enrollment
(
self
.
course
,
True
)
def
test_user_overview
(
self
):
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
reverse
(
'user-detail'
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
data
=
response
.
data
# pylint: disable=maybe-no-member
courses
=
response
.
data
self
.
assertEqual
(
data
[
'username'
],
self
.
user
.
username
)
self
.
assertEqual
(
len
(
courses
),
0
)
self
.
assertEqual
(
data
[
'email'
],
self
.
user
.
email
)
def
test_overview_anon
(
self
):
# anonymous disallowed
url
=
reverse
(
'user-detail'
,
kwargs
=
{
'username'
:
self
.
user
.
username
})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
401
)
# can't get info on someone else
other
=
UserFactory
.
create
()
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
response
=
self
.
client
.
get
(
reverse
(
'user-detail'
,
kwargs
=
{
'username'
:
other
.
username
}))
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_redirect_userinfo
(
self
):
url
=
'/api/mobile/v0.5/my_user_info'
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
401
)
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
302
)
self
.
assertTrue
(
self
.
username
in
response
[
'location'
])
def
test_course_serializer
(
self
):
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
self
.
_enroll
(
self
.
course
)
serialized
=
CourseEnrollmentSerializer
(
CourseEnrollment
.
enrollments_for_user
(
self
.
user
)[
0
])
.
data
# pylint: disable=no-member
self
.
assertEqual
(
serialized
[
'course'
][
'video_outline'
],
None
)
self
.
assertEqual
(
serialized
[
'course'
][
'name'
],
self
.
course
.
display_name
)
self
.
assertEqual
(
serialized
[
'course'
][
'number'
],
self
.
course
.
id
.
course
)
self
.
assertEqual
(
serialized
[
'course'
][
'org'
],
self
.
course
.
id
.
org
)
def
test_course_serializer_with_display_overrides
(
self
):
self
.
course
.
display_coursenumber
=
"overridden_number"
self
.
course
.
display_organization
=
"overridden_org"
modulestore
()
.
update_item
(
self
.
course
,
self
.
user
.
id
)
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
self
.
_enroll
(
self
.
course
)
serialized
=
CourseEnrollmentSerializer
(
CourseEnrollment
.
enrollments_for_user
(
self
.
user
)[
0
])
.
data
# pylint: disable=no-member
self
.
assertEqual
(
serialized
[
'course'
][
'number'
],
self
.
course
.
display_coursenumber
)
self
.
assertEqual
(
serialized
[
'course'
][
'org'
],
self
.
course
.
display_organization
)
# Tests for user-course-status
def
_course_status_url
(
self
):
class
CourseStatusAPITestCase
(
MobileAPITestCase
):
"""
"""
Convenience to fetch the url for our user and course
Base test class for /api/mobile/v0.5/users/<user_name>/course_status_info/{course_id}
"""
"""
return
reverse
(
'user-course-status'
,
kwargs
=
{
'username'
:
self
.
username
,
'course_id'
:
unicode
(
self
.
course
.
id
)})
REVERSE_INFO
=
{
'name'
:
'user-course-status'
,
'params'
:
[
'username'
,
'course_id'
]}
def
_setup_course_skeleton
(
self
):
def
_setup_course_skeleton
(
self
):
"""
"""
...
@@ -163,154 +88,134 @@ class TestUserApi(ModuleStoreTestCase, APITestCase):
...
@@ -163,154 +88,134 @@ class TestUserApi(ModuleStoreTestCase, APITestCase):
other_unit
=
ItemFactory
.
create
(
other_unit
=
ItemFactory
.
create
(
parent_location
=
sub_section
.
location
,
parent_location
=
sub_section
.
location
,
)
)
return
section
,
sub_section
,
unit
,
other_unit
return
section
,
sub_section
,
unit
,
other_unit
def
test_course_status_course_not_found
(
self
):
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
reverse
(
'user-course-status'
,
kwargs
=
{
'username'
:
self
.
username
,
'course_id'
:
'a/b/c'
})
response
=
self
.
client
.
get
(
url
)
json_data
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response
.
status_code
,
404
)
self
.
assertEqual
(
json_data
,
errors
.
ERROR_INVALID_COURSE_ID
)
def
test_course_status_wrong_user
(
self
):
url
=
reverse
(
'user-course-status'
,
kwargs
=
{
'username'
:
'other_user'
,
'course_id'
:
unicode
(
self
.
course
.
id
)})
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
403
)
def
test_course_status_no_auth
(
self
):
url
=
self
.
_course_status_url
()
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
401
)
def
test_default_value
(
self
):
(
section
,
sub_section
,
unit
,
__
)
=
self
.
_setup_course_skeleton
()
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
self
.
_course_status_url
()
class
TestCourseStatusGET
(
CourseStatusAPITestCase
,
MobileAuthUserTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
result
=
self
.
client
.
get
(
url
)
"""
json_data
=
json
.
loads
(
result
.
content
)
Tests for GET of /api/mobile/v0.5/users/<user_name>/course_status_info/{course_id}
"""
def
test_success
(
self
):
self
.
login_and_enroll
()
(
section
,
sub_section
,
unit
,
__
)
=
self
.
_setup_course_skeleton
()
self
.
assertEqual
(
result
.
status_code
,
200
)
response
=
self
.
api_response
(
)
self
.
assertEqual
(
json_
data
[
"last_visited_module_id"
],
unicode
(
unit
.
location
))
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
unit
.
location
))
self
.
assertEqual
(
self
.
assertEqual
(
json_
data
[
"last_visited_module_path"
],
response
.
data
[
"last_visited_module_path"
],
[
unicode
(
module
.
location
)
for
module
in
[
unit
,
sub_section
,
section
,
self
.
course
]]
[
unicode
(
module
.
location
)
for
module
in
[
unit
,
sub_section
,
section
,
self
.
course
]]
)
)
def
test_course_update_no_args
(
self
):
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
self
.
_course_status_url
()
class
TestCourseStatusPATCH
(
CourseStatusAPITestCase
,
MobileAuthUserTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
result
=
self
.
client
.
patch
(
url
)
# pylint: disable=no-member
"""
self
.
assertEqual
(
result
.
status_code
,
200
)
Tests for PATCH of /api/mobile/v0.5/users/<user_name>/course_status_info/{course_id}
"""
def
url_method
(
self
,
url
,
**
kwargs
):
# override implementation to use PATCH method.
return
self
.
client
.
patch
(
url
,
data
=
kwargs
.
get
(
'data'
,
None
))
# pylint: disable=no-member
def
test_course_update
(
self
):
def
test_success
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
__
,
other_unit
)
=
self
.
_setup_course_skeleton
()
(
__
,
__
,
__
,
other_unit
)
=
self
.
_setup_course_skeleton
()
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
self
.
_course_status_url
()
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
)})
result
=
self
.
client
.
patch
(
# pylint: disable=no-member
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
other_unit
.
location
))
url
,
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
)}
def
test_invalid_module
(
self
):
)
self
.
login_and_enroll
()
self
.
assertEqual
(
result
.
status_code
,
200
)
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
"abc"
},
expected_response_code
=
400
)
result
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
data
,
errors
.
ERROR_INVALID_MODULE_ID
)
json_data
=
json
.
loads
(
result
.
content
)
self
.
assertEqual
(
result
.
status_code
,
200
)
self
.
assertEqual
(
json_data
[
"last_visited_module_id"
],
unicode
(
other_unit
.
location
))
def
test_course_update_bad_module
(
self
):
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
self
.
_course_status_url
()
result
=
self
.
client
.
patch
(
# pylint: disable=no-member
url
,
{
"last_visited_module_id"
:
"abc"
},
)
json_data
=
json
.
loads
(
result
.
content
)
self
.
assertEqual
(
result
.
status_code
,
400
)
self
.
assertEqual
(
json_data
,
errors
.
ERROR_INVALID_MODULE_ID
)
def
test_course_update_no_timezone
(
self
):
def
test_nonexistent_module
(
self
):
self
.
login_and_enroll
()
non_existent_key
=
self
.
course
.
id
.
make_usage_key
(
'video'
,
'non-existent'
)
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
non_existent_key
},
expected_response_code
=
400
)
self
.
assertEqual
(
response
.
data
,
errors
.
ERROR_INVALID_MODULE_ID
)
def
test_no_timezone
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
__
,
other_unit
)
=
self
.
_setup_course_skeleton
()
(
__
,
__
,
__
,
other_unit
)
=
self
.
_setup_course_skeleton
()
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
self
.
_course_status_url
()
past_date
=
datetime
.
datetime
.
now
()
past_date
=
datetime
.
datetime
.
now
()
result
=
self
.
client
.
patch
(
# pylint: disable=no-member
response
=
self
.
api_response
(
url
,
data
=
{
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
),
"last_visited_module_id"
:
unicode
(
other_unit
.
location
),
"modification_date"
:
past_date
.
isoformat
()
# pylint: disable=maybe-no-member
"modification_date"
:
past_date
.
isoformat
()
# pylint: disable=maybe-no-member
},
},
expected_response_code
=
400
)
)
self
.
assertEqual
(
response
.
data
,
errors
.
ERROR_INVALID_MODIFICATION_DATE
)
json_data
=
json
.
loads
(
result
.
content
)
def
_date_sync
(
self
,
date
,
initial_unit
,
update_unit
,
expected_unit
):
self
.
assertEqual
(
result
.
status_code
,
400
)
self
.
assertEqual
(
json_data
,
errors
.
ERROR_INVALID_MODIFICATION_DATE
)
def
_test_course_update_date_sync
(
self
,
date
,
initial_unit
,
update_unit
,
expected_unit
):
"""
"""
Helper for test cases that use a modification to decide whether
Helper for test cases that use a modification to decide whether
to update the course status
to update the course status
"""
"""
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
self
.
login_and_enroll
(
)
url
=
self
.
_course_status_url
()
# save something so we have an initial date
# save something so we have an initial date
self
.
client
.
patch
(
# pylint: disable=no-member
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
initial_unit
.
location
)})
url
,
{
"last_visited_module_id"
:
unicode
(
initial_unit
.
location
)}
)
# now actually update it
# now actually update it
result
=
self
.
client
.
patch
(
# pylint: disable=no-member
response
=
self
.
api_response
(
url
,
data
=
{
{
"last_visited_module_id"
:
unicode
(
update_unit
.
location
),
"last_visited_module_id"
:
unicode
(
update_unit
.
location
),
"modification_date"
:
date
.
isoformat
()
"modification_date"
:
date
.
isoformat
()
}
,
}
)
)
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
expected_unit
.
location
))
json_data
=
json
.
loads
(
result
.
content
)
def
test_old_date
(
self
):
self
.
assertEqual
(
result
.
status_code
,
200
)
self
.
login_and_enroll
()
self
.
assertEqual
(
json_data
[
"last_visited_module_id"
],
unicode
(
expected_unit
.
location
))
def
test_course_update_old_date
(
self
):
(
__
,
__
,
unit
,
other_unit
)
=
self
.
_setup_course_skeleton
()
(
__
,
__
,
unit
,
other_unit
)
=
self
.
_setup_course_skeleton
()
date
=
timezone
.
now
()
+
datetime
.
timedelta
(
days
=-
100
)
date
=
timezone
.
now
()
+
datetime
.
timedelta
(
days
=-
100
)
self
.
_
test_course_update_
date_sync
(
date
,
unit
,
other_unit
,
unit
)
self
.
_date_sync
(
date
,
unit
,
other_unit
,
unit
)
def
test_course_update_new_date
(
self
):
def
test_new_date
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
unit
,
other_unit
)
=
self
.
_setup_course_skeleton
()
(
__
,
__
,
unit
,
other_unit
)
=
self
.
_setup_course_skeleton
()
date
=
timezone
.
now
()
+
datetime
.
timedelta
(
days
=
100
)
date
=
timezone
.
now
()
+
datetime
.
timedelta
(
days
=
100
)
self
.
_
test_course_update_
date_sync
(
date
,
unit
,
other_unit
,
other_unit
)
self
.
_date_sync
(
date
,
unit
,
other_unit
,
other_unit
)
def
test_course_update_no_initial_date
(
self
):
def
test_no_initial_date
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
_
,
other_unit
)
=
self
.
_setup_course_skeleton
()
(
__
,
__
,
_
,
other_unit
)
=
self
.
_setup_course_skeleton
()
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
response
=
self
.
api_response
(
url
=
self
.
_course_status_url
()
data
=
{
result
=
self
.
client
.
patch
(
# pylint: disable=no-member
url
,
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
),
"last_visited_module_id"
:
unicode
(
other_unit
.
location
),
"modification_date"
:
timezone
.
now
()
.
isoformat
()
"modification_date"
:
timezone
.
now
()
.
isoformat
()
}
}
)
)
json_data
=
json
.
loads
(
result
.
content
)
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
other_unit
.
location
))
self
.
assertEqual
(
result
.
status_code
,
200
)
self
.
assertEqual
(
json_data
[
"last_visited_module_id"
],
unicode
(
other_unit
.
location
))
def
test_course_update_invalid_date
(
self
):
def
test_invalid_date
(
self
):
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
self
.
login_and_enroll
()
response
=
self
.
api_response
(
data
=
{
"modification_date"
:
"abc"
},
expected_response_code
=
400
)
self
.
assertEqual
(
response
.
data
,
errors
.
ERROR_INVALID_MODIFICATION_DATE
)
url
=
self
.
_course_status_url
()
result
=
self
.
client
.
patch
(
# pylint: disable=no-member
class
TestCourseEnrollmentSerializer
(
MobileAPITestCase
):
url
,
"""
{
"modification_date"
:
"abc"
}
Test the course enrollment serializer
)
"""
json_data
=
json
.
loads
(
result
.
content
)
def
test_success
(
self
):
self
.
assertEqual
(
result
.
status_code
,
400
)
self
.
login_and_enroll
()
self
.
assertEqual
(
json_data
,
errors
.
ERROR_INVALID_MODIFICATION_DATE
)
serialized
=
CourseEnrollmentSerializer
(
CourseEnrollment
.
enrollments_for_user
(
self
.
user
)[
0
])
.
data
# pylint: disable=no-member
self
.
assertEqual
(
serialized
[
'course'
][
'video_outline'
],
None
)
self
.
assertEqual
(
serialized
[
'course'
][
'name'
],
self
.
course
.
display_name
)
self
.
assertEqual
(
serialized
[
'course'
][
'number'
],
self
.
course
.
id
.
course
)
self
.
assertEqual
(
serialized
[
'course'
][
'org'
],
self
.
course
.
id
.
org
)
def
test_with_display_overrides
(
self
):
self
.
login_and_enroll
()
self
.
course
.
display_coursenumber
=
"overridden_number"
self
.
course
.
display_organization
=
"overridden_org"
modulestore
()
.
update_item
(
self
.
course
,
self
.
user
.
id
)
serialized
=
CourseEnrollmentSerializer
(
CourseEnrollment
.
enrollments_for_user
(
self
.
user
)[
0
])
.
data
# pylint: disable=no-member
self
.
assertEqual
(
serialized
[
'course'
][
'number'
],
self
.
course
.
display_coursenumber
)
self
.
assertEqual
(
serialized
[
'course'
][
'org'
],
self
.
course
.
display_organization
)
lms/djangoapps/mobile_api/users/views.py
View file @
5349b55b
...
@@ -8,39 +8,28 @@ from courseware.module_render import get_module_for_descriptor
...
@@ -8,39 +8,28 @@ from courseware.module_render import get_module_for_descriptor
from
django.shortcuts
import
redirect
from
django.shortcuts
import
redirect
from
django.utils
import
dateparse
from
django.utils
import
dateparse
from
rest_framework
import
generics
,
permissions
,
views
from
rest_framework
import
generics
,
views
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework.decorators
import
api_view
from
rest_framework.decorators
import
api_view
,
authentication_classes
,
permission_classes
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
courseware.views
import
get_current_child
,
save_positions_recursively_up
from
courseware.views
import
get_current_child
,
save_positions_recursively_up
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
from
opaque_keys.edx.keys
import
UsageKey
from
opaque_keys
import
InvalidKeyError
from
opaque_keys
import
InvalidKeyError
from
student.models
import
CourseEnrollment
,
User
from
student.models
import
CourseEnrollment
,
User
from
mobile_api.utils
import
mobile_available_when_enrolled
from
xblock.fields
import
Scope
from
xblock.fields
import
Scope
from
xblock.runtime
import
KeyValueStore
from
xblock.runtime
import
KeyValueStore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
.serializers
import
CourseEnrollmentSerializer
,
UserSerializer
from
.serializers
import
CourseEnrollmentSerializer
,
UserSerializer
from
mobile_api
import
errors
from
mobile_api
import
errors
from
mobile_api.utils
import
mobile_access_when_enrolled
,
mobile_view
,
MobileView
,
mobile_course_access
class
IsUser
(
permissions
.
BasePermission
):
@MobileView
(
is_user
=
True
)
"""
Permission that checks to see if the request user matches the User models
"""
def
has_object_permission
(
self
,
request
,
view
,
obj
):
return
request
.
user
==
obj
class
UserDetail
(
generics
.
RetrieveAPIView
):
class
UserDetail
(
generics
.
RetrieveAPIView
):
"""
"""
**Use Case**
**Use Case**
...
@@ -70,8 +59,6 @@ class UserDetail(generics.RetrieveAPIView):
...
@@ -70,8 +59,6 @@ class UserDetail(generics.RetrieveAPIView):
* course_enrollments: The URI to list the courses the currently logged
* course_enrollments: The URI to list the courses the currently logged
in user is enrolled in.
in user is enrolled in.
"""
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,
IsUser
)
queryset
=
(
queryset
=
(
User
.
objects
.
all
()
User
.
objects
.
all
()
.
select_related
(
'profile'
,
'course_enrollments'
)
.
select_related
(
'profile'
,
'course_enrollments'
)
...
@@ -80,8 +67,7 @@ class UserDetail(generics.RetrieveAPIView):
...
@@ -80,8 +67,7 @@ class UserDetail(generics.RetrieveAPIView):
lookup_field
=
'username'
lookup_field
=
'username'
@authentication_classes
((
OAuth2Authentication
,
SessionAuthentication
))
@MobileView
(
is_user
=
True
)
@permission_classes
((
IsAuthenticated
,))
class
UserCourseStatus
(
views
.
APIView
):
class
UserCourseStatus
(
views
.
APIView
):
"""
"""
Endpoints for getting and setting meta data
Endpoints for getting and setting meta data
...
@@ -113,28 +99,7 @@ class UserCourseStatus(views.APIView):
...
@@ -113,28 +99,7 @@ class UserCourseStatus(views.APIView):
path
.
reverse
()
path
.
reverse
()
return
path
return
path
def
_process_arguments
(
self
,
request
,
username
,
course_id
,
course_handler
):
def
_get_course_info
(
self
,
request
,
course
):
"""
Checks and processes the arguments to our endpoint
then passes the processed and verified arguments on to something that
does the work specific to the individual case
"""
if
username
!=
request
.
user
.
username
:
return
Response
(
errors
.
ERROR_INVALID_USER_ID
,
status
=
403
)
course
=
None
try
:
course_key
=
CourseKey
.
from_string
(
course_id
)
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
None
)
except
InvalidKeyError
:
pass
if
not
course
:
return
Response
(
errors
.
ERROR_INVALID_COURSE_ID
,
status
=
404
)
# pylint: disable=lost-exception
return
course_handler
(
course
)
def
get_course_info
(
self
,
request
,
course
):
"""
"""
Returns the course status
Returns the course status
"""
"""
...
@@ -145,33 +110,16 @@ class UserCourseStatus(views.APIView):
...
@@ -145,33 +110,16 @@ class UserCourseStatus(views.APIView):
"last_visited_module_path"
:
path_ids
,
"last_visited_module_path"
:
path_ids
,
})
})
def
get
(
self
,
request
,
username
,
course_id
):
"""
**Use Case**
Get meta data about user's status within a specific course
**Example request**:
GET /api/mobile/v0.5/users/{username}/course_status_info/{course_id}
**Response Values**
* last_visited_module_id: The id of the last module visited by the user in the given course
* last_visited_module_path: The ids of the modules in the path from the last visited module
to the course module
"""
return
self
.
_process_arguments
(
request
,
username
,
course_id
,
lambda
course
:
self
.
get_course_info
(
request
,
course
))
def
_update_last_visited_module_id
(
self
,
request
,
course
,
module_key
,
modification_date
):
def
_update_last_visited_module_id
(
self
,
request
,
course
,
module_key
,
modification_date
):
"""
"""
Saves the module id if the found modification_date is less recent than the passed modification date
Saves the module id if the found modification_date is less recent than the passed modification date
"""
"""
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course
.
id
,
request
.
user
,
course
,
depth
=
2
)
course
.
id
,
request
.
user
,
course
,
depth
=
2
)
try
:
module_descriptor
=
modulestore
()
.
get_item
(
module_key
)
module_descriptor
=
modulestore
()
.
get_item
(
module_key
)
except
ItemNotFoundError
:
return
Response
(
errors
.
ERROR_INVALID_MODULE_ID
,
status
=
400
)
module
=
get_module_for_descriptor
(
request
.
user
,
request
,
module_descriptor
,
field_data_cache
,
course
.
id
)
module
=
get_module_for_descriptor
(
request
.
user
,
request
,
module_descriptor
,
field_data_cache
,
course
.
id
)
if
modification_date
:
if
modification_date
:
...
@@ -186,15 +134,34 @@ class UserCourseStatus(views.APIView):
...
@@ -186,15 +134,34 @@ class UserCourseStatus(views.APIView):
original_store_date
=
student_module
.
modified
original_store_date
=
student_module
.
modified
if
modification_date
<
original_store_date
:
if
modification_date
<
original_store_date
:
# old modification date so skip update
# old modification date so skip update
return
self
.
get_course_info
(
request
,
course
)
return
self
.
_
get_course_info
(
request
,
course
)
if
module
:
save_positions_recursively_up
(
request
.
user
,
request
,
field_data_cache
,
module
)
save_positions_recursively_up
(
request
.
user
,
request
,
field_data_cache
,
module
)
return
self
.
get_course_info
(
request
,
course
)
return
self
.
_get_course_info
(
request
,
course
)
else
:
return
Response
(
errors
.
ERROR_INVALID_MODULE_ID
,
status
=
400
)
@mobile_course_access
()
def
get
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
**Use Case**
Get meta data about user's status within a specific course
**Example request**:
GET /api/mobile/v0.5/users/{username}/course_status_info/{course_id}
**Response Values**
def
patch
(
self
,
request
,
username
,
course_id
):
* last_visited_module_id: The id of the last module visited by the user in the given course
* last_visited_module_path: The ids of the modules in the path from the last visited module
to the course module
"""
return
self
.
_get_course_info
(
request
,
course
)
@mobile_course_access
()
def
patch
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
# pylint: disable=unused-argument
"""
"""
**Use Case**
**Use Case**
...
@@ -215,10 +182,6 @@ class UserCourseStatus(views.APIView):
...
@@ -215,10 +182,6 @@ class UserCourseStatus(views.APIView):
The same as doing a GET on this path
The same as doing a GET on this path
"""
"""
def
handle_course
(
course
):
"""
Updates the course_status once the arguments are checked
"""
module_id
=
request
.
DATA
.
get
(
"last_visited_module_id"
)
module_id
=
request
.
DATA
.
get
(
"last_visited_module_id"
)
modification_date_string
=
request
.
DATA
.
get
(
"modification_date"
)
modification_date_string
=
request
.
DATA
.
get
(
"modification_date"
)
modification_date
=
None
modification_date
=
None
...
@@ -236,11 +199,10 @@ class UserCourseStatus(views.APIView):
...
@@ -236,11 +199,10 @@ class UserCourseStatus(views.APIView):
return
self
.
_update_last_visited_module_id
(
request
,
course
,
module_key
,
modification_date
)
return
self
.
_update_last_visited_module_id
(
request
,
course
,
module_key
,
modification_date
)
else
:
else
:
# The arguments are optional, so if there's no argument just succeed
# The arguments are optional, so if there's no argument just succeed
return
self
.
get_course_info
(
request
,
course
)
return
self
.
_get_course_info
(
request
,
course
)
return
self
.
_process_arguments
(
request
,
username
,
course_id
,
handle_course
)
@MobileView
(
is_user
=
True
)
class
UserCourseEnrollmentsList
(
generics
.
ListAPIView
):
class
UserCourseEnrollmentsList
(
generics
.
ListAPIView
):
"""
"""
**Use Case**
**Use Case**
...
@@ -274,38 +236,22 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
...
@@ -274,38 +236,22 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
* start: The data and time the course starts.
* start: The data and time the course starts.
* course_image: The path to the course image.
* course_image: The path to the course image.
"""
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,
IsUser
)
queryset
=
CourseEnrollment
.
objects
.
all
()
queryset
=
CourseEnrollment
.
objects
.
all
()
serializer_class
=
CourseEnrollmentSerializer
serializer_class
=
CourseEnrollmentSerializer
lookup_field
=
'username'
lookup_field
=
'username'
def
get_queryset
(
self
):
def
get_queryset
(
self
):
qset
=
self
.
queryset
.
filter
(
enrollments
=
self
.
queryset
.
filter
(
user__username
=
self
.
kwargs
[
'username'
],
is_active
=
True
)
.
order_by
(
'created'
)
user__username
=
self
.
kwargs
[
'username'
],
is_active
=
True
return
[
)
.
order_by
(
'created'
)
enrollment
for
enrollment
in
enrollments
return
mobile_course_enrollments
(
qset
,
self
.
request
.
user
)
if
mobile_access_when_enrolled
(
enrollment
.
course
,
self
.
request
.
user
)
]
@api_view
([
"GET"
])
@api_view
([
"GET"
])
@authentication_classes
((
OAuth2Authentication
,
SessionAuthentication
))
@mobile_view
()
@permission_classes
((
IsAuthenticated
,))
def
my_user_info
(
request
):
def
my_user_info
(
request
):
"""
"""
Redirect to the currently-logged-in user's info page
Redirect to the currently-logged-in user's info page
"""
"""
return
redirect
(
"user-detail"
,
username
=
request
.
user
.
username
)
return
redirect
(
"user-detail"
,
username
=
request
.
user
.
username
)
def
mobile_course_enrollments
(
enrollments
,
user
):
"""
Return enrollments only if courses are mobile_available (or if the user has
privileged (beta, staff, instructor) access)
:param enrollments is a list of CourseEnrollments.
"""
for
enr
in
enrollments
:
course
=
enr
.
course
if
mobile_available_when_enrolled
(
course
,
user
):
yield
enr
lms/djangoapps/mobile_api/utils.py
View file @
5349b55b
"""
"""
Tests for video outline API
Common utility methods and decorators for Mobile APIs.
"""
"""
from
courseware
import
access
import
functools
from
student.roles
import
CourseBetaTesterRole
from
django.http
import
Http404
from
student
import
auth
from
opaque_keys.edx.keys
import
CourseKey
from
courseware.courses
import
get_course_with_access
from
rest_framework
import
permissions
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
def
mobile_available_when_enrolled
(
course
,
user
):
def
mobile_course_access
(
depth
=
0
,
verify_enrolled
=
True
):
"""
"""
Determines whether a user has access to a course in a mobile context.
Method decorator for a mobile API endpoint that verifies the user has access to the course in a mobile context.
Checks if the course is marked as mobile_available or the user has extra permissions
"""
that gives them access anyway.
def
_decorator
(
func
):
Does not check if the user is actually enrolled in the course
"""Outer method decorator."""
@functools.wraps
(
func
)
def
_wrapper
(
self
,
request
,
*
args
,
**
kwargs
):
"""
"""
Expects kwargs to contain 'course_id'.
Passes the course descriptor to the given decorated function.
Raises 404 if access to course is disallowed.
"""
course_id
=
CourseKey
.
from_string
(
kwargs
.
pop
(
'course_id'
))
course
=
get_course_with_access
(
request
.
user
,
'load_mobile'
if
verify_enrolled
else
'load_mobile_no_enrollment_check'
,
course_id
,
depth
=
depth
)
return
func
(
self
,
request
,
course
=
course
,
*
args
,
**
kwargs
)
return
_wrapper
return
_decorator
def
mobile_access_when_enrolled
(
course
,
user
):
"""
Determines whether a user has access to a course in a mobile context.
Checks the mobile_available flag and the start_date.
Note: Does not check if the user is actually enrolled in the course.
"""
# The course doesn't always really exist -- we can have bad data in the enrollments
# The course doesn't always really exist -- we can have bad data in the enrollments
# pointing to non-existent (or removed) courses, in which case `course` is None.
# pointing to non-existent (or removed) courses, in which case `course` is None.
if
not
course
:
if
not
course
:
return
None
return
False
try
:
return
get_course_with_access
(
user
,
'load_mobile_no_enrollment_check'
,
course
.
id
)
is
not
None
except
Http404
:
return
False
# Implicitly includes instructor role via the following has_access check
beta_tester_role
=
CourseBetaTesterRole
(
course
.
id
)
return
(
def
mobile_view
(
is_user
=
False
):
course
.
mobile_available
"""
or
auth
.
has_access
(
user
,
beta_tester_role
)
Function decorator that abstracts the authentication and permission checks for mobile api views.
or
access
.
has_access
(
user
,
'staff'
,
course
)
"""
)
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
):
"""
Requires either OAuth2 or Session-based authentication.
If is_user is True, also requires username in URL matches the request user.
"""
func
.
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
func
.
permission_classes
=
(
permissions
.
IsAuthenticated
,)
if
is_user
:
func
.
permission_classes
+=
(
IsUser
,)
return
func
return
_decorator
class
MobileView
(
object
):
"""
Class decorator that abstracts the authentication and permission checks for mobile api views.
"""
def
__init__
(
self
,
is_user
=
False
):
self
.
is_user
=
is_user
def
__call__
(
self
,
cls
):
class
_Decorator
(
cls
):
"""Inner decorator class to wrap the given class."""
mobile_view
(
self
.
is_user
)(
cls
)
return
_Decorator
lms/djangoapps/mobile_api/video_outlines/tests.py
View file @
5349b55b
"""
"""
Tests for video outline API
Tests for video outline API
"""
"""
import
copy
import
ddt
from
uuid
import
uuid4
from
uuid
import
uuid4
from
collections
import
namedtuple
from
collections
import
namedtuple
from
django.core.urlresolvers
import
reverse
from
django.test.utils
import
override_settings
from
django.conf
import
settings
from
edxval
import
api
from
edxval
import
api
from
rest_framework.test
import
APITestCase
from
xmodule.modulestore.tests.factories
import
ItemFactory
from
courseware.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
TEST_DATA_MOCK_MODULESTORE
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.video_module
import
transcripts_utils
from
xmodule.video_module
import
transcripts_utils
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
mobile_api.tests
import
ROLE_CASES
from
..testutils
import
MobileAPITestCase
,
MobileAuthTestMixin
,
MobileEnrolledCourseAccessTestMixin
TEST_DATA_CONTENTSTORE
=
copy
.
deepcopy
(
settings
.
CONTENTSTORE
)
TEST_DATA_CONTENTSTORE
[
'DOC_STORE_CONFIG'
][
'db'
]
=
'test_xcontent_
%
s'
%
uuid4
()
.
hex
@ddt.ddt
class
TestVideoAPITestCase
(
MobileAPITestCase
):
@override_settings
(
MODULESTORE
=
TEST_DATA_MOCK_MODULESTORE
,
CONTENTSTORE
=
TEST_DATA_CONTENTSTORE
)
class
TestVideoOutline
(
ModuleStoreTestCase
,
APITestCase
):
"""
"""
Tests for /api/mobile/v0.5/video_outlines/
Base test class for video related mobile APIs
"""
"""
def
setUp
(
self
):
def
setUp
(
self
):
super
(
TestVideoOutline
,
self
)
.
setUp
()
super
(
TestVideoAPITestCase
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
course
=
CourseFactory
.
create
(
mobile_available
=
True
)
self
.
section
=
ItemFactory
.
create
(
self
.
section
=
ItemFactory
.
create
(
parent_location
=
self
.
course
.
location
,
parent_location
=
self
.
course
.
location
,
category
=
"chapter"
,
category
=
"chapter"
,
...
@@ -105,32 +88,6 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
...
@@ -105,32 +88,6 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
}
}
]})
]})
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
'test'
)
@ddt.data
(
*
ROLE_CASES
)
@ddt.unpack
def
test_non_mobile_access
(
self
,
role
,
should_succeed
):
nonmobile
=
CourseFactory
.
create
(
mobile_available
=
False
)
if
role
:
role
(
nonmobile
.
id
)
.
add_users
(
self
.
user
)
url
=
reverse
(
'video-summary-list'
,
kwargs
=
{
'course_id'
:
unicode
(
nonmobile
.
id
)})
response
=
self
.
client
.
get
(
url
)
if
should_succeed
:
self
.
assertEqual
(
response
.
status_code
,
200
)
else
:
self
.
assertEqual
(
response
.
status_code
,
403
)
def
_get_video_summary_list
(
self
):
"""
Calls the video-summary-list endpoint, expecting a success response
"""
url
=
reverse
(
'video-summary-list'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
return
response
.
data
# pylint: disable=maybe-no-member
def
_create_video_with_subs
(
self
):
def
_create_video_with_subs
(
self
):
"""
"""
Creates and returns a video with stored subtitles.
Creates and returns a video with stored subtitles.
...
@@ -156,7 +113,15 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
...
@@ -156,7 +113,15 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
sub
=
subid
sub
=
subid
)
)
class
TestVideoSummaryList
(
TestVideoAPITestCase
,
MobileAuthTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
"""
Tests for /api/mobile/v0.5/video_outlines/courses/{course_id}..
"""
REVERSE_INFO
=
{
'name'
:
'video-summary-list'
,
'params'
:
[
'course_id'
]}
def
test_course_list
(
self
):
def
test_course_list
(
self
):
self
.
login_and_enroll
()
self
.
_create_video_with_subs
()
self
.
_create_video_with_subs
()
ItemFactory
.
create
(
ItemFactory
.
create
(
parent_location
=
self
.
other_unit
.
location
,
parent_location
=
self
.
other_unit
.
location
,
...
@@ -178,7 +143,7 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
...
@@ -178,7 +143,7 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
visible_to_staff_only
=
True
,
visible_to_staff_only
=
True
,
)
)
course_outline
=
self
.
_get_video_summary_list
()
course_outline
=
self
.
api_response
()
.
data
self
.
assertEqual
(
len
(
course_outline
),
3
)
self
.
assertEqual
(
len
(
course_outline
),
3
)
vid
=
course_outline
[
0
]
vid
=
course_outline
[
0
]
self
.
assertTrue
(
'test_subsection_omega_
%
CE
%
A9'
in
vid
[
'section_url'
])
self
.
assertTrue
(
'test_subsection_omega_
%
CE
%
A9'
in
vid
[
'section_url'
])
...
@@ -195,18 +160,20 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
...
@@ -195,18 +160,20 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
self
.
assertEqual
(
course_outline
[
2
][
'summary'
][
'video_url'
],
self
.
html5_video_url
)
self
.
assertEqual
(
course_outline
[
2
][
'summary'
][
'video_url'
],
self
.
html5_video_url
)
self
.
assertEqual
(
course_outline
[
2
][
'summary'
][
'size'
],
0
)
self
.
assertEqual
(
course_outline
[
2
][
'summary'
][
'size'
],
0
)
def
test_course_list_with_nameless_unit
(
self
):
def
test_with_nameless_unit
(
self
):
self
.
login_and_enroll
()
ItemFactory
.
create
(
ItemFactory
.
create
(
parent_location
=
self
.
nameless_unit
.
location
,
parent_location
=
self
.
nameless_unit
.
location
,
category
=
"video"
,
category
=
"video"
,
edx_video_id
=
self
.
edx_video_id
,
edx_video_id
=
self
.
edx_video_id
,
display_name
=
u"test draft video omega 2
\u03a9
"
display_name
=
u"test draft video omega 2
\u03a9
"
)
)
course_outline
=
self
.
_get_video_summary_list
()
course_outline
=
self
.
api_response
()
.
data
self
.
assertEqual
(
len
(
course_outline
),
1
)
self
.
assertEqual
(
len
(
course_outline
),
1
)
self
.
assertEqual
(
course_outline
[
0
][
'path'
][
2
][
'name'
],
self
.
nameless_unit
.
location
.
block_id
)
self
.
assertEqual
(
course_outline
[
0
][
'path'
][
2
][
'name'
],
self
.
nameless_unit
.
location
.
block_id
)
def
test_course_list_with_hidden_blocks
(
self
):
def
test_with_hidden_blocks
(
self
):
self
.
login_and_enroll
()
hidden_subsection
=
ItemFactory
.
create
(
hidden_subsection
=
ItemFactory
.
create
(
parent_location
=
self
.
section
.
location
,
parent_location
=
self
.
section
.
location
,
category
=
"sequential"
,
category
=
"sequential"
,
...
@@ -231,10 +198,11 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
...
@@ -231,10 +198,11 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
category
=
"video"
,
category
=
"video"
,
edx_video_id
=
self
.
edx_video_id
,
edx_video_id
=
self
.
edx_video_id
,
)
)
course_outline
=
self
.
_get_video_summary_list
()
course_outline
=
self
.
api_response
()
.
data
self
.
assertEqual
(
len
(
course_outline
),
0
)
self
.
assertEqual
(
len
(
course_outline
),
0
)
def
test_course_list_language
(
self
):
def
test_language
(
self
):
self
.
login_and_enroll
()
video
=
ItemFactory
.
create
(
video
=
ItemFactory
.
create
(
parent_location
=
self
.
nameless_unit
.
location
,
parent_location
=
self
.
nameless_unit
.
location
,
category
=
"video"
,
category
=
"video"
,
...
@@ -258,11 +226,12 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
...
@@ -258,11 +226,12 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
for
case
in
language_cases
:
for
case
in
language_cases
:
video
.
transcripts
=
case
.
transcripts
video
.
transcripts
=
case
.
transcripts
modulestore
()
.
update_item
(
video
,
self
.
user
.
id
)
modulestore
()
.
update_item
(
video
,
self
.
user
.
id
)
course_outline
=
self
.
_get_video_summary_list
()
course_outline
=
self
.
api_response
()
.
data
self
.
assertEqual
(
len
(
course_outline
),
1
)
self
.
assertEqual
(
len
(
course_outline
),
1
)
self
.
assertEqual
(
course_outline
[
0
][
'summary'
][
'language'
],
case
.
expected_language
)
self
.
assertEqual
(
course_outline
[
0
][
'summary'
][
'language'
],
case
.
expected_language
)
def
test_course_list_transcripts
(
self
):
def
test_transcripts
(
self
):
self
.
login_and_enroll
()
video
=
ItemFactory
.
create
(
video
=
ItemFactory
.
create
(
parent_location
=
self
.
nameless_unit
.
location
,
parent_location
=
self
.
nameless_unit
.
location
,
category
=
"video"
,
category
=
"video"
,
...
@@ -290,25 +259,32 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
...
@@ -290,25 +259,32 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
video
.
transcripts
=
case
.
transcripts
video
.
transcripts
=
case
.
transcripts
video
.
sub
=
case
.
english_subtitle
video
.
sub
=
case
.
english_subtitle
modulestore
()
.
update_item
(
video
,
self
.
user
.
id
)
modulestore
()
.
update_item
(
video
,
self
.
user
.
id
)
course_outline
=
self
.
_get_video_summary_list
()
course_outline
=
self
.
api_response
()
.
data
self
.
assertEqual
(
len
(
course_outline
),
1
)
self
.
assertEqual
(
len
(
course_outline
),
1
)
self
.
assertSetEqual
(
self
.
assertSetEqual
(
set
(
course_outline
[
0
][
'summary'
][
'transcripts'
]
.
keys
()),
set
(
course_outline
[
0
][
'summary'
][
'transcripts'
]
.
keys
()),
set
(
case
.
expected_transcripts
)
set
(
case
.
expected_transcripts
)
)
)
def
test_transcripts_detail
(
self
):
video
=
self
.
_create_video_with_subs
()
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
),
'block_id'
:
unicode
(
video
.
scope_ids
.
usage_id
.
block_id
),
'lang'
:
'pl'
}
url
=
reverse
(
'video-transcripts-detail'
,
kwargs
=
kwargs
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
kwargs
[
'lang'
]
=
'en'
class
TestTranscriptsDetail
(
TestVideoAPITestCase
,
MobileAuthTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
url
=
reverse
(
'video-transcripts-detail'
,
kwargs
=
kwargs
)
"""
response
=
self
.
client
.
get
(
url
)
Tests for /api/mobile/v0.5/video_outlines/transcripts/{course_id}..
self
.
assertEqual
(
response
.
status_code
,
200
)
"""
REVERSE_INFO
=
{
'name'
:
'video-transcripts-detail'
,
'params'
:
[
'course_id'
]}
def
setUp
(
self
):
super
(
TestTranscriptsDetail
,
self
)
.
setUp
()
self
.
video
=
self
.
_create_video_with_subs
()
def
reverse_url
(
self
,
reverse_args
=
None
,
**
kwargs
):
reverse_args
=
reverse_args
or
{}
reverse_args
.
update
({
'block_id'
:
self
.
video
.
location
.
block_id
,
'lang'
:
kwargs
.
get
(
'lang'
,
'en'
),
})
return
super
(
TestTranscriptsDetail
,
self
)
.
reverse_url
(
reverse_args
,
**
kwargs
)
def
test_incorrect_language
(
self
):
self
.
login_and_enroll
()
self
.
api_response
(
expected_response_code
=
404
,
lang
=
'pl'
)
lms/djangoapps/mobile_api/video_outlines/views.py
View file @
5349b55b
...
@@ -10,21 +10,18 @@ from functools import partial
...
@@ -10,21 +10,18 @@ from functools import partial
from
django.http
import
Http404
,
HttpResponse
from
django.http
import
Http404
,
HttpResponse
from
rest_framework
import
generics
,
permissions
from
rest_framework
import
generics
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework.response
import
Response
from
rest_framework.response
import
Response
from
rest_framework.exceptions
import
PermissionDenied
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.locator
import
BlockUsageLocator
from
opaque_keys.edx.locator
import
BlockUsageLocator
from
xmodule.exceptions
import
NotFoundError
from
xmodule.exceptions
import
NotFoundError
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
mobile_api.utils
import
mobile_available_when_enrolled
from
..utils
import
MobileView
,
mobile_course_access
from
.serializers
import
BlockOutline
,
video_summary
from
.serializers
import
BlockOutline
,
video_summary
@MobileView
()
class
VideoSummaryList
(
generics
.
ListAPIView
):
class
VideoSummaryList
(
generics
.
ListAPIView
):
"""
"""
**Use Case**
**Use Case**
...
@@ -78,16 +75,12 @@ class VideoSummaryList(generics.ListAPIView):
...
@@ -78,16 +75,12 @@ class VideoSummaryList(generics.ListAPIView):
* size: The size of the video file
* size: The size of the video file
"""
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
course_id
=
CourseKey
.
from_string
(
kwargs
[
'course_id'
])
course
=
get_mobile_course
(
course_id
,
request
.
user
)
@mobile_course_access
(
depth
=
None
)
def
list
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
video_outline
=
list
(
video_outline
=
list
(
BlockOutline
(
BlockOutline
(
course
_
id
,
course
.
id
,
course
,
course
,
{
"video"
:
partial
(
video_summary
,
course
)},
{
"video"
:
partial
(
video_summary
,
course
)},
request
,
request
,
...
@@ -96,6 +89,7 @@ class VideoSummaryList(generics.ListAPIView):
...
@@ -96,6 +89,7 @@ class VideoSummaryList(generics.ListAPIView):
return
Response
(
video_outline
)
return
Response
(
video_outline
)
@MobileView
()
class
VideoTranscripts
(
generics
.
RetrieveAPIView
):
class
VideoTranscripts
(
generics
.
RetrieveAPIView
):
"""
"""
**Use Case**
**Use Case**
...
@@ -111,16 +105,14 @@ class VideoTranscripts(generics.RetrieveAPIView):
...
@@ -111,16 +105,14 @@ class VideoTranscripts(generics.RetrieveAPIView):
An HttpResponse with an SRT file download.
An HttpResponse with an SRT file download.
"""
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
@mobile_course_access
()
course_key
=
CourseKey
.
from_string
(
kwargs
[
'course_id'
])
def
get
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
block_id
=
kwargs
[
'block_id'
]
block_id
=
kwargs
[
'block_id'
]
lang
=
kwargs
[
'lang'
]
lang
=
kwargs
[
'lang'
]
usage_key
=
BlockUsageLocator
(
usage_key
=
BlockUsageLocator
(
course
_key
,
block_type
=
"video"
,
block_id
=
block_id
course
.
id
,
block_type
=
"video"
,
block_id
=
block_id
)
)
try
:
try
:
video_descriptor
=
modulestore
()
.
get_item
(
usage_key
)
video_descriptor
=
modulestore
()
.
get_item
(
usage_key
)
...
@@ -132,15 +124,3 @@ class VideoTranscripts(generics.RetrieveAPIView):
...
@@ -132,15 +124,3 @@ class VideoTranscripts(generics.RetrieveAPIView):
response
[
'Content-Disposition'
]
=
'attachment; filename="{}"'
.
format
(
filename
)
response
[
'Content-Disposition'
]
=
'attachment; filename="{}"'
.
format
(
filename
)
return
response
return
response
def
get_mobile_course
(
course_id
,
user
):
"""
Return only a CourseDescriptor if the course is mobile-ready or if the
requesting user is a staff member.
"""
course
=
modulestore
()
.
get_course
(
course_id
,
depth
=
None
)
if
mobile_available_when_enrolled
(
course
,
user
):
return
course
raise
PermissionDenied
(
detail
=
"Course not available on mobile."
)
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