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
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
667 additions
and
576 deletions
+667
-576
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
+136
-231
lms/djangoapps/mobile_api/users/views.py
+64
-118
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
from
external_auth.models
import
ExternalAuthMap
from
courseware.masquerade
import
is_masquerading_as_student
from
django.utils.timezone
import
UTC
from
student
import
auth
from
student.roles
import
(
GlobalStaff
,
CourseStaffRole
,
CourseInstructorRole
,
OrgStaffRole
,
OrgInstructorRole
,
CourseBetaTesterRole
...
...
@@ -46,6 +47,7 @@ def has_access(user, action, obj, course_key=None):
- visible_to_staff_only for modules
- DISABLE_START_DATES
- 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,
anonymous is assumed
...
...
@@ -108,6 +110,8 @@ def _has_access_course_desc(user, action, course):
'load' -- load the courseware, see inside the course
'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,
ACCESS_REQUIRE_STAFF_FOR_COURSE,
'see_exists' -- can see that the course exists.
...
...
@@ -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
():
"""
First check if restriction of enrollment by login method is enabled, both
...
...
@@ -234,6 +268,8 @@ def _has_access_course_desc(user, action, course):
checkers
=
{
'load'
:
can_load
,
'load_forum'
:
can_load_forum
,
'load_mobile'
:
can_load_mobile
,
'load_mobile_no_enrollment_check'
:
can_load_mobile_no_enroll_check
,
'enroll'
:
can_enroll
,
'see_exists'
:
see_exists
,
'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
"""
import
json
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.modulestore
import
ModuleStoreEnum
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
..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
):
super
(
TestCourseInfo
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
course
=
CourseFactory
.
create
(
mobile_available
=
True
)
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
'test'
)
def
test_about
(
self
):
url
=
reverse
(
'course-about-detail'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertTrue
(
'overview'
in
response
.
data
)
# pylint: disable=maybe-no-member
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
):
REVERSE_INFO
=
{
'name'
:
'course-about-detail'
,
'params'
:
[
'course_id'
]}
def
verify_success
(
self
,
response
):
super
(
TestAbout
,
self
)
.
verify_success
(
response
)
self
.
assertTrue
(
'overview'
in
response
.
data
)
def
init_course_access
(
self
,
course_id
=
None
):
# override this method since enrollment is not required for the About endpoint.
self
.
login
()
def
test_about_static_rewrite
(
self
):
self
.
login
()
about_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'about'
,
'overview'
)
about_module
=
modulestore
()
.
get_item
(
about_usage_key
)
underlying_about_html
=
about_module
.
data
...
...
@@ -45,16 +38,24 @@ class TestCourseInfo(APITestCase):
# check that we start with relative static assets
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
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertNotIn
(
'
\"
/static/'
,
about_html
)
response
=
self
.
api_response
()
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'
)
course_updates
=
modulestore
()
.
create_item
(
self
.
user
.
id
,
...
...
@@ -72,50 +73,51 @@ class TestCourseInfo(APITestCase):
course_updates
.
items
=
[
course_update_data
]
modulestore
()
.
update_item
(
course_updates
,
self
.
user
.
id
)
url
=
reverse
(
'course-updates-list'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
get
(
url
)
response
=
self
.
api_response
()
content
=
response
.
data
[
0
][
"content"
]
# pylint: disable=maybe-no-member
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertNotIn
(
"
\"
/static/"
,
content
)
underlying_updates_module
=
modulestore
()
.
get_item
(
updates_usage_key
)
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
"""
REVERSE_INFO
=
{
'name'
:
'course-handouts-list'
,
'params'
:
[
'course_id'
]}
def
setUp
(
self
):
super
(
TestHandout
Info
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
'test'
)
super
(
TestHandout
s
,
self
)
.
setUp
()
# 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'
])
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
):
empty_course
=
CourseFactory
.
create
(
mobile_available
=
True
)
url
=
reverse
(
'course-handouts-list'
,
kwargs
=
{
'course_id'
:
unicode
(
empty_course
.
id
)})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
self
.
login_and_enroll
()
def
test_handout_exists
(
self
):
url
=
reverse
(
'course-handouts-list'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course
.
id
)})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# delete handouts in course
handouts_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'course_info'
,
'handouts'
)
with
self
.
store
.
branch_setting
(
ModuleStoreEnum
.
Branch
.
draft_preferred
,
self
.
course
.
id
):
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
handouts_usage_key
=
self
.
course
.
id
.
make_usage_key
(
'course_info'
,
'handouts'
)
underlying_handouts
=
self
.
store
.
get_item
(
handouts_usage_key
)
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
self
.
assertNotIn
(
'
\'
/static/'
,
handouts_html
)
self
.
assert
Equal
(
response
.
status_code
,
200
)
response
=
self
.
api_response
(
)
self
.
assert
NotIn
(
'
\'
/static/'
,
response
.
data
[
'handouts_html'
]
)
lms/djangoapps/mobile_api/course_info/views.py
View file @
5349b55b
...
...
@@ -2,17 +2,16 @@
Views for course info API
"""
from
django.http
import
Http404
from
rest_framework
import
generics
,
permissions
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework
import
generics
from
rest_framework.response
import
Response
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
..utils
import
MobileView
,
mobile_course_access
@MobileView
()
class
CourseUpdatesList
(
generics
.
ListAPIView
):
"""
**Use Case**
...
...
@@ -35,12 +34,9 @@ class CourseUpdatesList(generics.ListAPIView):
* id: The unique identifier of the update.
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
course_id
=
CourseKey
.
from_string
(
kwargs
[
'course_id'
])
course
=
modulestore
()
.
get_course
(
course_id
)
@mobile_course_access
()
def
list
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
course_updates_module
=
get_course_info_section_module
(
request
,
course
,
'updates'
)
update_items
=
reversed
(
getattr
(
course_updates_module
,
'items'
,
[]))
...
...
@@ -53,13 +49,14 @@ class CourseUpdatesList(generics.ListAPIView):
content
=
item
[
'content'
]
content
=
replace_static_urls
(
content
,
course_id
=
course
_
id
,
course_id
=
course
.
id
,
static_asset_path
=
course
.
static_asset_path
)
item
[
'content'
]
=
make_static_urls_absolute
(
request
,
content
)
return
Response
(
updates_to_show
)
@MobileView
()
class
CourseHandoutsList
(
generics
.
ListAPIView
):
"""
**Use Case**
...
...
@@ -74,27 +71,24 @@ class CourseHandoutsList(generics.ListAPIView):
* handouts_html: The HTML for course handouts.
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
course_id
=
CourseKey
.
from_string
(
kwargs
[
'course_id'
])
course
=
modulestore
()
.
get_course
(
course_id
)
@mobile_course_access
()
def
list
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
course_handouts_module
=
get_course_info_section_module
(
request
,
course
,
'handouts'
)
if
course_handouts_module
:
handouts_html
=
course_handouts_module
.
data
handouts_html
=
replace_static_urls
(
handouts_html
,
course_id
=
course
_
id
,
course_id
=
course
.
id
,
static_asset_path
=
course
.
static_asset_path
)
handouts_html
=
make_static_urls_absolute
(
self
.
request
,
handouts_html
)
return
Response
({
'handouts_html'
:
handouts_html
})
else
:
# 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
):
"""
**Use Case**
...
...
@@ -109,13 +103,9 @@ class CourseAboutDetail(generics.RetrieveAPIView):
* 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.
# 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
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
.utils
import
mobile_available_when_enrolled
ROLE_CASES
=
(
(
auth
.
CourseBetaTesterRole
,
True
),
(
auth
.
CourseStaffRole
,
True
),
(
auth
.
CourseInstructorRole
,
True
),
(
None
,
False
)
)
from
.utils
import
mobile_access_when_enrolled
from
.testutils
import
MobileAPITestCase
,
ROLE_CASES
@ddt.ddt
class
TestMobileApiUtils
(
Mo
duleStoreTestCase
,
APITestCase
):
class
TestMobileApiUtils
(
Mo
bile
APITestCase
):
"""
Tests for mobile API utilities
"""
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
()
@ddt.data
(
*
ROLE_CASES
)
@ddt.unpack
def
test_mobile_role_access
(
self
,
role
,
should_have_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
:
role
(
course
.
id
)
.
add_users
(
self
.
user
)
self
.
assertEqual
(
should_have_access
,
mobile_a
vailable_when_enrolled
(
course
,
self
.
user
))
role
(
non_mobile_
course
.
id
)
.
add_users
(
self
.
user
)
self
.
assertEqual
(
should_have_access
,
mobile_a
ccess_when_enrolled
(
non_mobile_
course
,
self
.
user
))
def
test_mobile_explicit_access
(
self
):
"""
Verifies that our mobile access function listens to the mobile_available flag as it should
"""
course
=
CourseFactory
.
create
(
mobile_available
=
True
)
self
.
assertTrue
(
mobile_available_when_enrolled
(
course
,
self
.
user
))
self
.
assertTrue
(
mobile_access_when_enrolled
(
self
.
course
,
self
.
user
))
def
test_missing_course
(
self
):
"""
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
"""
import
datetime
import
ddt
import
json
from
django.utils
import
timezone
from
rest_framework.test
import
APITestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
ItemFactory
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
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
):
class
TestUserDetailApi
(
MobileAPITestCase
,
MobileAuthUserTestMixin
):
"""
Test
the user info API
Test
s for /api/mobile/v0.5/users/<user_name>...
"""
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
})
REVERSE_INFO
=
{
'name'
:
'user-detail'
,
'params'
:
[
'username'
]}
def
_enroll
(
self
,
course
):
"""
enroll test user in test course
"""
resp
=
self
.
client
.
post
(
reverse
(
'change_enrollment'
),
{
'enrollment_action'
:
'enroll'
,
'course_id'
:
course
.
id
.
to_deprecated_string
(),
'check_access'
:
True
,
})
self
.
assertEqual
(
resp
.
status_code
,
200
)
def
_verify_single_course_enrollment
(
self
,
course
,
should_succeed
):
"""
check that enrolling in course adds us to it
"""
def
test_success
(
self
):
self
.
login
()
url
=
self
.
_enrollment_url
()
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
self
.
_enroll
(
course
)
response
=
self
.
client
.
get
(
url
)
response
=
self
.
api_response
()
self
.
assertEqual
(
response
.
data
[
'username'
],
self
.
user
.
username
)
self
.
assertEqual
(
response
.
data
[
'email'
],
self
.
user
.
email
)
courses
=
response
.
data
# pylint: disable=maybe-no-member
self
.
assertEqual
(
response
.
status_code
,
200
)
class
TestUserInfoApi
(
MobileAPITestCase
,
MobileAuthTestMixin
):
"""
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'
if
should_succeed
:
self
.
assertEqual
(
len
(
courses
),
1
)
found_course
=
courses
[
0
][
'course'
]
self
.
assertTrue
(
'video_outline'
in
found_course
)
self
.
assertTrue
(
'course_handouts'
in
found_course
)
self
.
assertEqual
(
found_course
[
'id'
],
unicode
(
course
.
id
))
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
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
)
data
=
response
.
data
# pylint: disable=maybe-no-member
self
.
assertEqual
(
data
[
'username'
],
self
.
user
.
username
)
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
)
def
test_success
(
self
):
"""Verify the endpoint redirects to the user detail endpoint"""
self
.
login
()
response
=
self
.
api_response
(
expected_response_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
)
class
TestUserEnrollmentApi
(
MobileAPITestCase
,
MobileAuthUserTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
"""
Tests for /api/mobile/v0.5/users/<user_name>/course_enrollments/
"""
REVERSE_INFO
=
{
'name'
:
'courseenrollment-detail'
,
'params'
:
[
'username'
]}
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
)
def
verify_success
(
self
,
response
):
super
(
TestUserEnrollmentApi
,
self
)
.
verify_success
(
response
)
courses
=
response
.
data
self
.
assertEqual
(
len
(
courses
),
1
)
# Tests for user-course-status
found_course
=
courses
[
0
][
'course'
]
self
.
assertTrue
(
'video_outline'
in
found_course
)
self
.
assertTrue
(
'course_handouts'
in
found_course
)
self
.
assertEqual
(
found_course
[
'id'
],
unicode
(
self
.
course
.
id
))
self
.
assertEqual
(
courses
[
0
][
'mode'
],
'honor'
)
def
_course_status_url
(
self
):
"""
Convenience to fetch the url for our user and course
"""
return
reverse
(
'user-course-status'
,
kwargs
=
{
'username'
:
self
.
username
,
'course_id'
:
unicode
(
self
.
course
.
id
)})
def
verify_failure
(
self
,
response
):
self
.
assertEqual
(
response
.
status_code
,
200
)
courses
=
response
.
data
self
.
assertEqual
(
len
(
courses
),
0
)
class
CourseStatusAPITestCase
(
MobileAPITestCase
):
"""
Base test class for /api/mobile/v0.5/users/<user_name>/course_status_info/{course_id}
"""
REVERSE_INFO
=
{
'name'
:
'user-course-status'
,
'params'
:
[
'username'
,
'course_id'
]}
def
_setup_course_skeleton
(
self
):
"""
...
...
@@ -163,154 +88,134 @@ class TestUserApi(ModuleStoreTestCase, APITestCase):
other_unit
=
ItemFactory
.
create
(
parent_location
=
sub_section
.
location
,
)
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
()
result
=
self
.
client
.
get
(
url
)
json_data
=
json
.
loads
(
result
.
content
)
class
TestCourseStatusGET
(
CourseStatusAPITestCase
,
MobileAuthUserTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
"""
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
)
self
.
assertEqual
(
json_
data
[
"last_visited_module_id"
],
unicode
(
unit
.
location
))
response
=
self
.
api_response
(
)
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
unit
.
location
))
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
]]
)
def
test_course_update_no_args
(
self
):
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
self
.
_course_status_url
()
result
=
self
.
client
.
patch
(
url
)
# pylint: disable=no-member
self
.
assertEqual
(
result
.
status_code
,
200
)
class
TestCourseStatusPATCH
(
CourseStatusAPITestCase
,
MobileAuthUserTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
"""
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
()
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"
:
unicode
(
other_unit
.
location
)}
)
self
.
assertEqual
(
result
.
status_code
,
200
)
result
=
self
.
client
.
get
(
url
)
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
)
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
)})
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
other_unit
.
location
))
def
test_invalid_module
(
self
):
self
.
login_and_enroll
()
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
"abc"
},
expected_response_code
=
400
)
self
.
assertEqual
(
response
.
data
,
errors
.
ERROR_INVALID_MODULE_ID
)
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_course_update_no_timezone
(
self
):
def
test_no_timezone
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
__
,
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
()
result
=
self
.
client
.
patch
(
# pylint: disable=no-member
url
,
{
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
),
"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
)
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
):
def
_date_sync
(
self
,
date
,
initial_unit
,
update_unit
,
expected_unit
):
"""
Helper for test cases that use a modification to decide whether
to update the course status
"""
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
self
.
_course_status_url
()
self
.
login_and_enroll
(
)
# save something so we have an initial date
self
.
client
.
patch
(
# pylint: disable=no-member
url
,
{
"last_visited_module_id"
:
unicode
(
initial_unit
.
location
)}
)
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
initial_unit
.
location
)})
# now actually update it
result
=
self
.
client
.
patch
(
# pylint: disable=no-member
url
,
{
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
update_unit
.
location
),
"modification_date"
:
date
.
isoformat
()
}
,
}
)
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
expected_unit
.
location
))
json_data
=
json
.
loads
(
result
.
content
)
self
.
assertEqual
(
result
.
status_code
,
200
)
self
.
assertEqual
(
json_data
[
"last_visited_module_id"
],
unicode
(
expected_unit
.
location
))
def
test_course_update_old_date
(
self
):
def
test_old_date
(
self
):
self
.
login_and_enroll
()
(
__
,
__
,
unit
,
other_unit
)
=
self
.
_setup_course_skeleton
()
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
()
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
()
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
url
=
self
.
_course_status_url
()
result
=
self
.
client
.
patch
(
# pylint: disable=no-member
url
,
{
response
=
self
.
api_response
(
data
=
{
"last_visited_module_id"
:
unicode
(
other_unit
.
location
),
"modification_date"
:
timezone
.
now
()
.
isoformat
()
}
)
json_data
=
json
.
loads
(
result
.
content
)
self
.
assertEqual
(
result
.
status_code
,
200
)
self
.
assertEqual
(
json_data
[
"last_visited_module_id"
],
unicode
(
other_unit
.
location
))
self
.
assertEqual
(
response
.
data
[
"last_visited_module_id"
],
unicode
(
other_unit
.
location
))
def
test_course_update_invalid_date
(
self
):
self
.
client
.
login
(
username
=
self
.
username
,
password
=
self
.
password
)
def
test_invalid_date
(
self
):
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
url
,
{
"modification_date"
:
"abc"
}
)
json_data
=
json
.
loads
(
result
.
content
)
self
.
assertEqual
(
result
.
status_code
,
400
)
self
.
assertEqual
(
json_data
,
errors
.
ERROR_INVALID_MODIFICATION_DATE
)
class
TestCourseEnrollmentSerializer
(
MobileAPITestCase
):
"""
Test the course enrollment serializer
"""
def
test_success
(
self
):
self
.
login_and_enroll
()
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
from
django.shortcuts
import
redirect
from
django.utils
import
dateparse
from
rest_framework
import
generics
,
permissions
,
views
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework.decorators
import
api_view
,
authentication_classes
,
permission_classes
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework
import
generics
,
views
from
rest_framework.decorators
import
api_view
from
rest_framework.response
import
Response
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
student.models
import
CourseEnrollment
,
User
from
mobile_api.utils
import
mobile_available_when_enrolled
from
xblock.fields
import
Scope
from
xblock.runtime
import
KeyValueStore
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
.serializers
import
CourseEnrollmentSerializer
,
UserSerializer
from
mobile_api
import
errors
from
mobile_api.utils
import
mobile_access_when_enrolled
,
mobile_view
,
MobileView
,
mobile_course_access
class
IsUser
(
permissions
.
BasePermission
):
"""
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
@MobileView
(
is_user
=
True
)
class
UserDetail
(
generics
.
RetrieveAPIView
):
"""
**Use Case**
...
...
@@ -70,8 +59,6 @@ class UserDetail(generics.RetrieveAPIView):
* course_enrollments: The URI to list the courses the currently logged
in user is enrolled in.
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,
IsUser
)
queryset
=
(
User
.
objects
.
all
()
.
select_related
(
'profile'
,
'course_enrollments'
)
...
...
@@ -80,8 +67,7 @@ class UserDetail(generics.RetrieveAPIView):
lookup_field
=
'username'
@authentication_classes
((
OAuth2Authentication
,
SessionAuthentication
))
@permission_classes
((
IsAuthenticated
,))
@MobileView
(
is_user
=
True
)
class
UserCourseStatus
(
views
.
APIView
):
"""
Endpoints for getting and setting meta data
...
...
@@ -113,28 +99,7 @@ class UserCourseStatus(views.APIView):
path
.
reverse
()
return
path
def
_process_arguments
(
self
,
request
,
username
,
course_id
,
course_handler
):
"""
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
):
def
_get_course_info
(
self
,
request
,
course
):
"""
Returns the course status
"""
...
...
@@ -145,33 +110,16 @@ class UserCourseStatus(views.APIView):
"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
):
"""
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
(
course
.
id
,
request
.
user
,
course
,
depth
=
2
)
module_descriptor
=
modulestore
()
.
get_item
(
module_key
)
try
:
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
)
if
modification_date
:
...
...
@@ -186,15 +134,34 @@ class UserCourseStatus(views.APIView):
original_store_date
=
student_module
.
modified
if
modification_date
<
original_store_date
:
# 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
)
return
self
.
get_course_info
(
request
,
course
)
else
:
return
Response
(
errors
.
ERROR_INVALID_MODULE_ID
,
status
=
400
)
save_positions_recursively_up
(
request
.
user
,
request
,
field_data_cache
,
module
)
return
self
.
_get_course_info
(
request
,
course
)
def
patch
(
self
,
request
,
username
,
course_id
):
@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**
* 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**
...
...
@@ -212,35 +179,30 @@ class UserCourseStatus(views.APIView):
**Response Values**
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"
)
modification_date_string
=
request
.
DATA
.
get
(
"modification_date"
)
modification_date
=
None
if
modification_date_string
:
modification_date
=
dateparse
.
parse_datetime
(
modification_date_string
)
if
not
modification_date
or
not
modification_date
.
tzinfo
:
return
Response
(
errors
.
ERROR_INVALID_MODIFICATION_DATE
,
status
=
400
)
if
module_id
:
try
:
module_key
=
UsageKey
.
from_string
(
module_id
)
except
InvalidKeyError
:
return
Response
(
errors
.
ERROR_INVALID_MODULE_ID
,
status
=
400
)
return
self
.
_update_last_visited_module_id
(
request
,
course
,
module_key
,
modification_date
)
else
:
# The arguments are optional, so if there's no argument just succeed
return
self
.
get_course_info
(
request
,
course
)
return
self
.
_process_arguments
(
request
,
username
,
course_id
,
handle_course
)
module_id
=
request
.
DATA
.
get
(
"last_visited_module_id"
)
modification_date_string
=
request
.
DATA
.
get
(
"modification_date"
)
modification_date
=
None
if
modification_date_string
:
modification_date
=
dateparse
.
parse_datetime
(
modification_date_string
)
if
not
modification_date
or
not
modification_date
.
tzinfo
:
return
Response
(
errors
.
ERROR_INVALID_MODIFICATION_DATE
,
status
=
400
)
if
module_id
:
try
:
module_key
=
UsageKey
.
from_string
(
module_id
)
except
InvalidKeyError
:
return
Response
(
errors
.
ERROR_INVALID_MODULE_ID
,
status
=
400
)
return
self
.
_update_last_visited_module_id
(
request
,
course
,
module_key
,
modification_date
)
else
:
# The arguments are optional, so if there's no argument just succeed
return
self
.
_get_course_info
(
request
,
course
)
@MobileView
(
is_user
=
True
)
class
UserCourseEnrollmentsList
(
generics
.
ListAPIView
):
"""
**Use Case**
...
...
@@ -274,38 +236,22 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
* start: The data and time the course starts.
* course_image: The path to the course image.
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,
IsUser
)
queryset
=
CourseEnrollment
.
objects
.
all
()
serializer_class
=
CourseEnrollmentSerializer
lookup_field
=
'username'
def
get_queryset
(
self
):
qset
=
self
.
queryset
.
filter
(
user__username
=
self
.
kwargs
[
'username'
],
is_active
=
True
)
.
order_by
(
'created'
)
return
mobile_course_enrollments
(
qset
,
self
.
request
.
user
)
enrollments
=
self
.
queryset
.
filter
(
user__username
=
self
.
kwargs
[
'username'
],
is_active
=
True
)
.
order_by
(
'created'
)
return
[
enrollment
for
enrollment
in
enrollments
if
mobile_access_when_enrolled
(
enrollment
.
course
,
self
.
request
.
user
)
]
@api_view
([
"GET"
])
@authentication_classes
((
OAuth2Authentication
,
SessionAuthentication
))
@permission_classes
((
IsAuthenticated
,))
@mobile_view
()
def
my_user_info
(
request
):
"""
Redirect to the currently-logged-in user's info page
"""
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
from
student.roles
import
CourseBetaTesterRole
from
student
import
auth
import
functools
from
django.http
import
Http404
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.
Checks if the course is marked as mobile_available or the user has extra permissions
that gives them access anyway.
Does not check if the user is actually enrolled in the course
Method decorator for a mobile API endpoint that verifies the user has access to the course in a mobile context.
"""
def
_decorator
(
func
):
"""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
# pointing to non-existent (or removed) courses, in which case `course` is None.
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
)
def
mobile_view
(
is_user
=
False
):
"""
Function 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
):
"""
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
return
(
c
ourse
.
mobile_available
or
auth
.
has_access
(
user
,
beta_tester_role
)
or
access
.
has_access
(
user
,
'staff'
,
course
)
)
def
__call__
(
self
,
cls
):
c
lass
_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
"""
import
copy
import
ddt
from
uuid
import
uuid4
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
rest_framework.test
import
APITestCase
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.modulestore.tests.factories
import
ItemFactory
from
xmodule.video_module
import
transcripts_utils
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.django
import
modulestore
from
mobile_api.tests
import
ROLE_CASES
TEST_DATA_CONTENTSTORE
=
copy
.
deepcopy
(
settings
.
CONTENTSTORE
)
TEST_DATA_CONTENTSTORE
[
'DOC_STORE_CONFIG'
][
'db'
]
=
'test_xcontent_
%
s'
%
uuid4
()
.
hex
from
..testutils
import
MobileAPITestCase
,
MobileAuthTestMixin
,
MobileEnrolledCourseAccessTestMixin
@ddt.ddt
@override_settings
(
MODULESTORE
=
TEST_DATA_MOCK_MODULESTORE
,
CONTENTSTORE
=
TEST_DATA_CONTENTSTORE
)
class
TestVideoOutline
(
ModuleStoreTestCase
,
APITestCase
):
class
TestVideoAPITestCase
(
MobileAPITestCase
):
"""
Tests for /api/mobile/v0.5/video_outlines/
Base test class for video related mobile APIs
"""
def
setUp
(
self
):
super
(
TestVideoOutline
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
course
=
CourseFactory
.
create
(
mobile_available
=
True
)
super
(
TestVideoAPITestCase
,
self
)
.
setUp
()
self
.
section
=
ItemFactory
.
create
(
parent_location
=
self
.
course
.
location
,
category
=
"chapter"
,
...
...
@@ -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
):
"""
Creates and returns a video with stored subtitles.
...
...
@@ -156,7 +113,15 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
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
):
self
.
login_and_enroll
()
self
.
_create_video_with_subs
()
ItemFactory
.
create
(
parent_location
=
self
.
other_unit
.
location
,
...
...
@@ -178,7 +143,7 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
visible_to_staff_only
=
True
,
)
course_outline
=
self
.
_get_video_summary_list
()
course_outline
=
self
.
api_response
()
.
data
self
.
assertEqual
(
len
(
course_outline
),
3
)
vid
=
course_outline
[
0
]
self
.
assertTrue
(
'test_subsection_omega_
%
CE
%
A9'
in
vid
[
'section_url'
])
...
...
@@ -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'
][
'size'
],
0
)
def
test_course_list_with_nameless_unit
(
self
):
def
test_with_nameless_unit
(
self
):
self
.
login_and_enroll
()
ItemFactory
.
create
(
parent_location
=
self
.
nameless_unit
.
location
,
category
=
"video"
,
edx_video_id
=
self
.
edx_video_id
,
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
(
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
(
parent_location
=
self
.
section
.
location
,
category
=
"sequential"
,
...
...
@@ -231,10 +198,11 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
category
=
"video"
,
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
)
def
test_course_list_language
(
self
):
def
test_language
(
self
):
self
.
login_and_enroll
()
video
=
ItemFactory
.
create
(
parent_location
=
self
.
nameless_unit
.
location
,
category
=
"video"
,
...
...
@@ -258,11 +226,12 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
for
case
in
language_cases
:
video
.
transcripts
=
case
.
transcripts
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
(
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
(
parent_location
=
self
.
nameless_unit
.
location
,
category
=
"video"
,
...
...
@@ -290,25 +259,32 @@ class TestVideoOutline(ModuleStoreTestCase, APITestCase):
video
.
transcripts
=
case
.
transcripts
video
.
sub
=
case
.
english_subtitle
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
.
assertSetEqual
(
set
(
course_outline
[
0
][
'summary'
][
'transcripts'
]
.
keys
()),
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'
url
=
reverse
(
'video-transcripts-detail'
,
kwargs
=
kwargs
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
class
TestTranscriptsDetail
(
TestVideoAPITestCase
,
MobileAuthTestMixin
,
MobileEnrolledCourseAccessTestMixin
):
"""
Tests for /api/mobile/v0.5/video_outlines/transcripts/{course_id}..
"""
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
from
django.http
import
Http404
,
HttpResponse
from
rest_framework
import
generics
,
permissions
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework
import
generics
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
xmodule.exceptions
import
NotFoundError
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
@MobileView
()
class
VideoSummaryList
(
generics
.
ListAPIView
):
"""
**Use Case**
...
...
@@ -78,16 +75,12 @@ class VideoSummaryList(generics.ListAPIView):
* 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
(
BlockOutline
(
course
_
id
,
course
.
id
,
course
,
{
"video"
:
partial
(
video_summary
,
course
)},
request
,
...
...
@@ -96,6 +89,7 @@ class VideoSummaryList(generics.ListAPIView):
return
Response
(
video_outline
)
@MobileView
()
class
VideoTranscripts
(
generics
.
RetrieveAPIView
):
"""
**Use Case**
...
...
@@ -111,16 +105,14 @@ class VideoTranscripts(generics.RetrieveAPIView):
An HttpResponse with an SRT file download.
"""
authentication_classes
=
(
OAuth2Authentication
,
SessionAuthentication
)
permission_classes
=
(
permissions
.
IsAuthenticated
,)
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
course_key
=
CourseKey
.
from_string
(
kwargs
[
'course_id'
])
@mobile_course_access
()
def
get
(
self
,
request
,
course
,
*
args
,
**
kwargs
):
block_id
=
kwargs
[
'block_id'
]
lang
=
kwargs
[
'lang'
]
usage_key
=
BlockUsageLocator
(
course
_key
,
block_type
=
"video"
,
block_id
=
block_id
course
.
id
,
block_type
=
"video"
,
block_id
=
block_id
)
try
:
video_descriptor
=
modulestore
()
.
get_item
(
usage_key
)
...
...
@@ -132,15 +124,3 @@ class VideoTranscripts(generics.RetrieveAPIView):
response
[
'Content-Disposition'
]
=
'attachment; filename="{}"'
.
format
(
filename
)
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