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
4921ec48
Commit
4921ec48
authored
Jun 05, 2015
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
MA-792 Course Blocks and Navigation API (user-specific)
parent
037ef3be
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
691 additions
and
34 deletions
+691
-34
common/djangoapps/util/module_utils.py
+8
-4
common/lib/xmodule/xmodule/video_module/video_module.py
+2
-1
lms/djangoapps/course_structure_api/v0/tests.py
+242
-20
lms/djangoapps/course_structure_api/v0/urls.py
+29
-1
lms/djangoapps/course_structure_api/v0/views.py
+394
-3
lms/djangoapps/courseware/entrance_exams.py
+3
-2
lms/djangoapps/courseware/grades.py
+7
-3
lms/djangoapps/mobile_api/video_outlines/serializers.py
+1
-0
lms/envs/common.py
+3
-0
lms/envs/test.py
+2
-0
No files found.
common/djangoapps/util/module_utils.py
View file @
4921ec48
...
...
@@ -3,7 +3,7 @@ Utility library containing operations used/shared by multiple courseware modules
"""
def
yield_dynamic_descriptor_descend
ents
(
descriptor
,
module_creator
):
# pylint: disable=invalid-name
def
yield_dynamic_descriptor_descend
ants
(
descriptor
,
user_id
,
module_creator
):
# pylint: disable=invalid-name
"""
This returns all of the descendants of a descriptor. If the descriptor
has dynamic children, the module will be created using module_creator
...
...
@@ -13,17 +13,21 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator): # pylint:
while
len
(
stack
)
>
0
:
next_descriptor
=
stack
.
pop
()
stack
.
extend
(
get_dynamic_descriptor_children
(
next_descriptor
,
module_creator
))
stack
.
extend
(
get_dynamic_descriptor_children
(
next_descriptor
,
user_id
,
module_creator
))
yield
next_descriptor
def
get_dynamic_descriptor_children
(
descriptor
,
module_creator
,
usage_key_filter
=
None
):
def
get_dynamic_descriptor_children
(
descriptor
,
user_id
,
module_creator
=
None
,
usage_key_filter
=
None
):
"""
Returns the children of the given descriptor, while supporting descriptors with dynamic children.
"""
module_children
=
[]
if
descriptor
.
has_dynamic_children
():
module
=
module_creator
(
descriptor
)
# do not rebind the module if it's already bound to a user.
if
descriptor
.
scope_ids
.
user_id
and
user_id
==
descriptor
.
scope_ids
.
user_id
:
module
=
descriptor
else
:
module
=
module_creator
(
descriptor
)
if
module
is
not
None
:
module_children
=
module
.
get_child_descriptors
()
else
:
...
...
common/lib/xmodule/xmodule/video_module/video_module.py
View file @
4921ec48
...
...
@@ -755,7 +755,8 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler
Returns a JSON representation of the student_view of this XModule.
The contract of the JSON content is between the caller and the particular XModule.
"""
# Honor only_on_web
# If the "only_on_web" field is set on this video, do not return the rest of the video's data
# in this json view, since this video is to be accessed only through its web view."
if
self
.
only_on_web
:
return
{
"only_on_web"
:
True
}
...
...
lms/djangoapps/course_structure_api/v0/tests.py
View file @
4921ec48
...
...
@@ -3,20 +3,26 @@ Run these tests @ Devstack:
paver test_system -s lms --fasttest --verbose --test_id=lms/djangoapps/course_structure_api
"""
# pylint: disable=missing-docstring,invalid-name,maybe-no-member,attribute-defined-outside-init
from
abc
import
ABCMeta
from
datetime
import
datetime
from
mock
import
patch
,
Mock
from
itertools
import
product
from
django.core.urlresolvers
import
reverse
from
django.test.utils
import
override_settings
from
mock
import
patch
,
Mock
from
capa.tests.response_xml_factory
import
MultipleChoiceResponseXMLFactory
from
oauth2_provider.tests.factories
import
AccessTokenFactory
,
ClientFactory
from
opaque_keys.edx.locator
import
CourseLocator
from
xmodule.error_module
import
ErrorDescriptor
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
from
xmodule.modulestore.xml
import
CourseLocationManager
from
xmodule.tests
import
get_test_system
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
courseware.tests.factories
import
GlobalStaffFactory
,
StaffFactory
from
openedx.core.djangoapps.content.course_structures.models
import
CourseStructure
from
openedx.core.djangoapps.content.course_structures.tasks
import
update_course_structure
...
...
@@ -36,8 +42,11 @@ class CourseViewTestsMixin(object):
self
.
create_test_data
()
self
.
create_user_and_access_token
()
def
create_user
_and_access_token
(
self
):
def
create_user
(
self
):
self
.
user
=
GlobalStaffFactory
.
create
()
def
create_user_and_access_token
(
self
):
self
.
create_user
()
self
.
oauth_client
=
ClientFactory
.
create
()
self
.
access_token
=
AccessTokenFactory
.
create
(
user
=
self
.
user
,
client
=
self
.
oauth_client
)
.
token
...
...
@@ -61,7 +70,7 @@ class CourseViewTestsMixin(object):
])
self
.
course_id
=
unicode
(
self
.
course
.
id
)
sequential
=
ItemFactory
.
create
(
se
lf
.
se
quential
=
ItemFactory
.
create
(
category
=
"sequential"
,
parent_location
=
self
.
course
.
location
,
display_name
=
"Lesson 1"
,
...
...
@@ -69,11 +78,21 @@ class CourseViewTestsMixin(object):
graded
=
True
)
factory
=
MultipleChoiceResponseXMLFactory
()
args
=
{
'choices'
:
[
False
,
True
,
False
]}
problem_xml
=
factory
.
build_xml
(
**
args
)
ItemFactory
.
create
(
category
=
"problem"
,
parent_location
=
sequential
.
location
,
parent_location
=
se
lf
.
se
quential
.
location
,
display_name
=
"Problem 1"
,
format
=
"Homework"
format
=
"Homework"
,
data
=
problem_xml
,
)
self
.
video
=
ItemFactory
.
create
(
category
=
"video"
,
parent_location
=
self
.
sequential
.
location
,
display_name
=
"Video 1"
,
)
self
.
empty_course
=
CourseFactory
.
create
(
...
...
@@ -120,6 +139,14 @@ class CourseViewTestsMixin(object):
response
=
self
.
client
.
get
(
uri
,
follow
=
True
,
**
default_headers
)
return
response
def
http_get_for_course
(
self
,
course_id
=
None
,
**
headers
):
"""Submit an HTTP GET request to the view for the given course"""
return
self
.
http_get
(
reverse
(
self
.
view
,
kwargs
=
{
'course_id'
:
course_id
or
self
.
course_id
}),
**
headers
)
def
test_not_authenticated
(
self
):
"""
Verify that access is denied to non-authenticated users.
...
...
@@ -133,23 +160,24 @@ class CourseViewTestsMixin(object):
raise
NotImplementedError
class
CourseDetailMixin
(
object
):
class
CourseDetail
Test
Mixin
(
object
):
"""
Mixin for views utilizing only the course_id kwarg.
"""
view_supports_debug_mode
=
True
def
test_get_invalid_course
(
self
):
"""
The view should return a 404 if the course ID is invalid.
"""
response
=
self
.
http_get
(
reverse
(
self
.
view
,
kwargs
=
{
'course_id'
:
self
.
invalid_course_id
})
)
response
=
self
.
http_get
_for_course
(
self
.
invalid_course_id
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_get
(
self
):
"""
The view should return a 200 if the course ID is valid.
"""
response
=
self
.
http_get
(
reverse
(
self
.
view
,
kwargs
=
{
'course_id'
:
self
.
course_id
})
)
response
=
self
.
http_get
_for_course
(
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Return the response so child classes do not have to repeat the request.
...
...
@@ -158,7 +186,7 @@ class CourseDetailMixin(object):
def
test_not_authenticated
(
self
):
""" The view should return HTTP status 401 if no user is authenticated. """
# HTTP 401 should be returned if the user is not authenticated.
response
=
self
.
http_get
(
reverse
(
self
.
view
,
kwargs
=
{
'course_id'
:
self
.
course_id
}),
HTTP_AUTHORIZATION
=
None
)
response
=
self
.
http_get
_for_course
(
HTTP_AUTHORIZATION
=
None
)
self
.
assertEqual
(
response
.
status_code
,
401
)
def
test_not_authorized
(
self
):
...
...
@@ -167,14 +195,12 @@ class CourseDetailMixin(object):
auth_header
=
'Bearer '
+
access_token
# Access should be granted if the proper access token is supplied.
response
=
self
.
http_get
(
reverse
(
self
.
view
,
kwargs
=
{
'course_id'
:
self
.
course_id
}),
HTTP_AUTHORIZATION
=
auth_header
)
response
=
self
.
http_get_for_course
(
HTTP_AUTHORIZATION
=
auth_header
)
self
.
assertEqual
(
response
.
status_code
,
200
)
# Access should be denied if the user is not course staff.
response
=
self
.
http_get
(
reverse
(
self
.
view
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
empty_course
.
id
)}),
HTTP_AUTHORIZATION
=
auth_header
)
self
.
assertEqual
(
response
.
status_code
,
403
)
response
=
self
.
http_get_for_course
(
course_id
=
unicode
(
self
.
empty_course
.
id
),
HTTP_AUTHORIZATION
=
auth_header
)
self
.
assertEqual
(
response
.
status_code
,
404
)
class
CourseListTests
(
CourseViewTestsMixin
,
ModuleStoreTestCase
):
...
...
@@ -268,7 +294,7 @@ class CourseListTests(CourseViewTestsMixin, ModuleStoreTestCase):
self
.
test_get
()
class
CourseDetailTests
(
CourseDetailMixin
,
CourseViewTestsMixin
,
ModuleStoreTestCase
):
class
CourseDetailTests
(
CourseDetail
Test
Mixin
,
CourseViewTestsMixin
,
ModuleStoreTestCase
):
view
=
'course_structure_api:v0:detail'
def
test_get
(
self
):
...
...
@@ -276,7 +302,7 @@ class CourseDetailTests(CourseDetailMixin, CourseViewTestsMixin, ModuleStoreTest
self
.
assertValidResponseCourse
(
response
.
data
,
self
.
course
)
class
CourseStructureTests
(
CourseDetailMixin
,
CourseViewTestsMixin
,
ModuleStoreTestCase
):
class
CourseStructureTests
(
CourseDetail
Test
Mixin
,
CourseViewTestsMixin
,
ModuleStoreTestCase
):
view
=
'course_structure_api:v0:structure'
def
setUp
(
self
):
...
...
@@ -294,13 +320,13 @@ class CourseStructureTests(CourseDetailMixin, CourseViewTestsMixin, ModuleStoreT
# Attempt to retrieve data for a course without stored structure
CourseStructure
.
objects
.
all
()
.
delete
()
self
.
assertFalse
(
CourseStructure
.
objects
.
filter
(
course_id
=
self
.
course
.
id
)
.
exists
())
response
=
self
.
http_get
(
reverse
(
self
.
view
,
kwargs
=
{
'course_id'
:
self
.
course_id
})
)
response
=
self
.
http_get
_for_course
(
)
self
.
assertEqual
(
response
.
status_code
,
503
)
self
.
assertEqual
(
response
[
'Retry-After'
],
'120'
)
# Course structure generation shouldn't take long. Generate the data and try again.
self
.
assertTrue
(
CourseStructure
.
objects
.
filter
(
course_id
=
self
.
course
.
id
)
.
exists
())
response
=
self
.
http_get
(
reverse
(
self
.
view
,
kwargs
=
{
'course_id'
:
self
.
course_id
})
)
response
=
self
.
http_get
_for_course
(
)
self
.
assertEqual
(
response
.
status_code
,
200
)
blocks
=
{}
...
...
@@ -331,7 +357,7 @@ class CourseStructureTests(CourseDetailMixin, CourseViewTestsMixin, ModuleStoreT
self
.
assertDictEqual
(
response
.
data
,
expected
)
class
CourseGradingPolicyTests
(
CourseDetailMixin
,
CourseViewTestsMixin
,
ModuleStoreTestCase
):
class
CourseGradingPolicyTests
(
CourseDetail
Test
Mixin
,
CourseViewTestsMixin
,
ModuleStoreTestCase
):
view
=
'course_structure_api:v0:grading_policy'
def
test_get
(
self
):
...
...
@@ -355,3 +381,199 @@ class CourseGradingPolicyTests(CourseDetailMixin, CourseViewTestsMixin, ModuleSt
}
]
self
.
assertListEqual
(
response
.
data
,
expected
)
#####################################################################################
#
# The following Mixins/Classes collectively test the CourseBlocksAndNavigation view.
#
# The class hierarchy is:
#
# -----------------> CourseBlocksOrNavigationTestMixin <--------------
# | ^ |
# | | |
# | CourseNavigationTestMixin | CourseBlocksTestMixin |
# | ^ ^ | ^ ^ |
# | | | | | | |
# | | | | | | |
# CourseNavigationTests CourseBlocksAndNavigationTests CourseBlocksTests
#
#
# Each Test Mixin is an abstract class that implements tests specific to its
# corresponding functionality.
#
# The concrete Test classes are expected to define the following class fields:
#
# block_navigation_view_type - The view's name as it should be passed to the django
# reverse method.
# container_fields - A list of fields that are expected to be included in the view's
# response for all container block types.
# block_fields - A list of fields that are expected to be included in the view's
# response for all block types.
#
######################################################################################
class
CourseBlocksOrNavigationTestMixin
(
CourseDetailTestMixin
,
CourseViewTestsMixin
):
"""
A Mixin class for testing all views related to Course blocks and/or navigation.
"""
__metaclass__
=
ABCMeta
view_supports_debug_mode
=
False
def
setUp
(
self
):
"""
Override the base `setUp` method to enroll the user in the course, since these views
require enrollment for non-staff users.
"""
super
(
CourseBlocksOrNavigationTestMixin
,
self
)
.
setUp
()
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
def
create_user
(
self
):
"""
Override the base `create_user` method to test with non-staff users for these views.
"""
self
.
user
=
UserFactory
.
create
()
@property
def
view
(
self
):
"""
Returns the name of the view for testing to use in the django `reverse` call.
"""
return
'course_structure_api:v0:'
+
self
.
block_navigation_view_type
def
test_get
(
self
):
with
check_mongo_calls
(
3
):
response
=
super
(
CourseBlocksOrNavigationTestMixin
,
self
)
.
test_get
()
# verify root element
self
.
assertIn
(
'root'
,
response
.
data
)
root_string
=
unicode
(
self
.
course
.
location
)
self
.
assertEquals
(
response
.
data
[
'root'
],
root_string
)
# verify ~blocks element
self
.
assertTrue
(
self
.
block_navigation_view_type
in
response
.
data
)
blocks
=
response
.
data
[
self
.
block_navigation_view_type
]
# verify number of blocks
self
.
assertEquals
(
len
(
blocks
),
4
)
# verify fields in blocks
for
field
,
block
in
product
(
self
.
block_fields
,
blocks
.
values
()):
self
.
assertIn
(
field
,
block
)
# verify container fields in container blocks
for
field
in
self
.
container_fields
:
self
.
assertIn
(
field
,
blocks
[
root_string
])
def
test_parse_error
(
self
):
"""
Verifies the view returns a 400 when a query parameter is incorrectly formatted.
"""
response
=
self
.
http_get_for_course
(
data
=
{
'block_json'
:
'incorrect'
})
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_no_access_to_block
(
self
):
"""
Verifies the view returns only the top-level course block, excluding the sequential block
and its descendants when the user does not have access to the sequential.
"""
self
.
sequential
.
visible_to_staff_only
=
True
modulestore
()
.
update_item
(
self
.
sequential
,
self
.
user
.
id
)
response
=
super
(
CourseBlocksOrNavigationTestMixin
,
self
)
.
test_get
()
self
.
assertEquals
(
len
(
response
.
data
[
self
.
block_navigation_view_type
]),
1
)
class
CourseBlocksTestMixin
(
object
):
"""
A Mixin class for testing all views related to Course blocks.
"""
__metaclass__
=
ABCMeta
view_supports_debug_mode
=
False
block_fields
=
[
'id'
,
'type'
,
'display_name'
,
'web_url'
,
'block_url'
,
'graded'
,
'format'
]
def
test_block_json
(
self
):
"""
Verifies the view's response when the block_json data is requested.
"""
response
=
self
.
http_get_for_course
(
data
=
{
'block_json'
:
'{"video":{"profiles":["mobile_low"]}}'
}
)
self
.
assertEquals
(
response
.
status_code
,
200
)
video_block
=
response
.
data
[
self
.
block_navigation_view_type
][
unicode
(
self
.
video
.
location
)]
self
.
assertIn
(
'block_json'
,
video_block
)
def
test_block_count
(
self
):
"""
Verifies the view's response when the block_count data is requested.
"""
response
=
self
.
http_get_for_course
(
data
=
{
'block_count'
:
'problem'
}
)
self
.
assertEquals
(
response
.
status_code
,
200
)
root_block
=
response
.
data
[
self
.
block_navigation_view_type
][
unicode
(
self
.
course
.
location
)]
self
.
assertIn
(
'block_count'
,
root_block
)
self
.
assertIn
(
'problem'
,
root_block
[
'block_count'
])
self
.
assertEquals
(
root_block
[
'block_count'
][
'problem'
],
1
)
class
CourseNavigationTestMixin
(
object
):
"""
A Mixin class for testing all views related to Course navigation.
"""
__metaclass__
=
ABCMeta
def
test_depth_zero
(
self
):
"""
Tests that all descendants are bundled into the root block when the navigation_depth is set to 0.
"""
response
=
self
.
http_get_for_course
(
data
=
{
'navigation_depth'
:
'0'
}
)
root_block
=
response
.
data
[
self
.
block_navigation_view_type
][
unicode
(
self
.
course
.
location
)]
self
.
assertIn
(
'descendants'
,
root_block
)
self
.
assertEquals
(
len
(
root_block
[
'descendants'
]),
3
)
def
test_depth
(
self
):
"""
Tests that all container blocks have descendants listed in their data.
"""
response
=
self
.
http_get_for_course
()
container_descendants
=
(
(
self
.
course
.
location
,
1
),
(
self
.
sequential
.
location
,
2
),
)
for
container_location
,
expected_num_descendants
in
container_descendants
:
block
=
response
.
data
[
self
.
block_navigation_view_type
][
unicode
(
container_location
)]
self
.
assertIn
(
'descendants'
,
block
)
self
.
assertEquals
(
len
(
block
[
'descendants'
]),
expected_num_descendants
)
class
CourseBlocksTests
(
CourseBlocksOrNavigationTestMixin
,
CourseBlocksTestMixin
,
ModuleStoreTestCase
):
"""
A Test class for testing the Course 'blocks' view.
"""
block_navigation_view_type
=
'blocks'
container_fields
=
[
'children'
]
class
CourseNavigationTests
(
CourseBlocksOrNavigationTestMixin
,
CourseNavigationTestMixin
,
ModuleStoreTestCase
):
"""
A Test class for testing the Course 'navigation' view.
"""
block_navigation_view_type
=
'navigation'
container_fields
=
[
'descendants'
]
block_fields
=
[]
class
CourseBlocksAndNavigationTests
(
CourseBlocksOrNavigationTestMixin
,
CourseBlocksTestMixin
,
CourseNavigationTestMixin
,
ModuleStoreTestCase
):
"""
A Test class for testing the Course 'blocks+navigation' view.
"""
block_navigation_view_type
=
'blocks+navigation'
container_fields
=
[
'children'
,
'descendants'
]
lms/djangoapps/course_structure_api/v0/urls.py
View file @
4921ec48
...
...
@@ -14,5 +14,33 @@ urlpatterns = patterns(
url
(
r'^courses/$'
,
views
.
CourseList
.
as_view
(),
name
=
'list'
),
url
(
r'^courses/{}/$'
.
format
(
COURSE_ID_PATTERN
),
views
.
CourseDetail
.
as_view
(),
name
=
'detail'
),
url
(
r'^course_structures/{}/$'
.
format
(
COURSE_ID_PATTERN
),
views
.
CourseStructure
.
as_view
(),
name
=
'structure'
),
url
(
r'^grading_policies/{}/$'
.
format
(
COURSE_ID_PATTERN
),
views
.
CourseGradingPolicy
.
as_view
(),
name
=
'grading_policy'
)
url
(
r'^grading_policies/{}/$'
.
format
(
COURSE_ID_PATTERN
),
views
.
CourseGradingPolicy
.
as_view
(),
name
=
'grading_policy'
),
)
if
settings
.
FEATURES
.
get
(
'ENABLE_COURSE_BLOCKS_NAVIGATION_API'
):
# TODO (MA-789) This endpoint still needs to be approved by the arch council.
# TODO (MA-704) This endpoint still needs to be made performant.
urlpatterns
+=
(
url
(
r'^courses/{}/blocks/$'
.
format
(
COURSE_ID_PATTERN
),
views
.
CourseBlocksAndNavigation
.
as_view
(),
{
'return_blocks'
:
True
,
'return_nav'
:
False
},
name
=
'blocks'
),
url
(
r'^courses/{}/navigation/$'
.
format
(
COURSE_ID_PATTERN
),
views
.
CourseBlocksAndNavigation
.
as_view
(),
{
'return_blocks'
:
False
,
'return_nav'
:
True
},
name
=
'navigation'
),
url
(
r'^courses/{}/blocks\+navigation/$'
.
format
(
COURSE_ID_PATTERN
),
views
.
CourseBlocksAndNavigation
.
as_view
(),
{
'return_blocks'
:
True
,
'return_nav'
:
True
},
name
=
'blocks+navigation'
),
)
lms/djangoapps/course_structure_api/v0/views.py
View file @
4921ec48
""" API implementation for course-oriented interactions. """
from
collections
import
namedtuple
import
json
import
logging
from
django.conf
import
settings
from
django.http
import
Http404
from
rest_framework.authentication
import
OAuth2Authentication
,
SessionAuthentication
from
rest_framework.exceptions
import
PermissionDenied
,
AuthenticationFailed
from
rest_framework.exceptions
import
AuthenticationFailed
,
ParseError
from
rest_framework.generics
import
RetrieveAPIView
,
ListAPIView
from
rest_framework.permissions
import
IsAuthenticated
from
rest_framework.response
import
Response
from
rest_framework.reverse
import
reverse
from
xmodule.modulestore.django
import
modulestore
from
opaque_keys.edx.keys
import
CourseKey
...
...
@@ -16,8 +19,12 @@ from course_structure_api.v0 import api, serializers
from
course_structure_api.v0.errors
import
CourseNotFoundError
,
CourseStructureNotAvailableError
from
courseware
import
courses
from
courseware.access
import
has_access
from
courseware.model_data
import
FieldDataCache
from
courseware.module_render
import
get_module_for_descriptor
from
openedx.core.lib.api.view_utils
import
view_course_access
,
view_auth_classes
from
openedx.core.lib.api.serializers
import
PaginationSerializer
from
student.roles
import
CourseInstructorRole
,
CourseStaffRole
from
util.module_utils
import
get_dynamic_descriptor_children
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -83,10 +90,10 @@ class CourseViewMixin(object):
def
check_course_permissions
(
self
,
user
,
course
):
"""
Checks if the request user can access the course.
Raises
PermissionDenied
if the user does not have course access.
Raises
404
if the user does not have course access.
"""
if
not
self
.
user_can_access_course
(
user
,
course
):
raise
PermissionDenied
raise
Http404
def
perform_authentication
(
self
,
request
):
"""
...
...
@@ -290,3 +297,387 @@ class CourseGradingPolicy(CourseViewMixin, ListAPIView):
@CourseViewMixin.course_check
def
get
(
self
,
request
,
**
kwargs
):
return
Response
(
api
.
course_grading_policy
(
self
.
course_key
))
@view_auth_classes
()
class
CourseBlocksAndNavigation
(
ListAPIView
):
"""
**Use Case**
The following endpoints return the content of the course according to the requesting user's access level.
* Blocks - Get the course's blocks.
* Navigation - Get the course's navigation information per the navigation depth requested.
* Blocks+Navigation - Get both the course's blocks and the course's navigation information.
**Example requests**:
GET api/course_structure/v0/courses/{course_id}/blocks/
GET api/course_structure/v0/courses/{course_id}/navigation/
GET api/course_structure/v0/courses/{course_id}/blocks+navigation/
&block_count=video
&block_json={"video":{"profiles":["mobile_low"]}}
&fields=graded,format,responsive_ui
**Parameters**:
* block_json: (dict) Indicates for which block types to return student_view_json data. The key is the block
type and the value is the "context" that is passed to the block's student_view_json method.
Example: block_json={"video":{"profiles":["mobile_high","mobile_low"]}}
* block_count: (list) Indicates for which block types to return the aggregate count of the blocks.
Example: block_count="video,problem"
* fields: (list) Indicates which additional fields to return for each block.
Default is "children,graded,format,responsive_ui"
Example: fields=graded,format,responsive_ui
* navigation_depth (integer) Indicates how far deep to traverse into the course hierarchy before bundling
all the descendants.
Default is 3 since typical navigational views of the course show a maximum of chapter->sequential->vertical.
Example: navigation_depth=3
**Response Values**
The following fields are returned with a successful response.
Only either one of blocks, navigation, or blocks+navigation is returned depending on which endpoint is used.
The "root" field is returned for all endpoints.
* root: The ID of the root node of the course blocks.
* blocks: A dictionary that maps block usage IDs to a collection of information about each block.
Each block contains the following fields. Returned only if using the "blocks" endpoint.
* id: (string) The usage ID of the block.
* type: (string) The type of block. Possible values include course, chapter, sequential, vertical, html,
problem, video, and discussion. The type can also be the name of a custom type of block used for the course.
* display_name: (string) The display name of the block.
* children: (list) If the block has child blocks, a list of IDs of the child blocks.
Returned only if the "children" input parameter is True.
* block_count: (dict) For each block type specified in the block_count parameter to the endpoint, the
aggregate number of blocks of that type for this block and all of its descendants.
Returned only if the "block_count" input parameter contains this block's type.
* block_json: (dict) The JSON data for this block.
Returned only if the "block_json" input parameter contains this block's type.
* block_url: (string) The URL to retrieve the HTML rendering of this block. The HTML could include
CSS and Javascript code. This URL can be used as a fallback if the custom block_json for this
block type is not requested and not supported.
* web_url: (string) The URL to the website location of this block. This URL can be used as a further
fallback if the block_url and the block_json is not supported.
* graded (boolean) Whether or not the block or any of its descendants is graded.
Returned only if "graded" is included in the "fields" parameter.
* format: (string) The assignment type of the block.
Possible values can be "Homework", "Lab", "Midterm Exam", and "Final Exam".
Returned only if "format" is included in the "fields" parameter.
* responsive_ui: (boolean) Whether or not the block's rendering obtained via block_url is responsive.
Returned only if "responsive_ui" is included in the "fields" parameter.
* navigation: A dictionary that maps block IDs to a collection of navigation information about each block.
Each block contains the following fields. Returned only if using the "navigation" endpoint.
* descendants: (list) A list of IDs of the children of the block if the block's depth in the
course hierarchy is less than the navigation_depth. Otherwise, a list of IDs of the aggregate descendants
of the block.
* blocks+navigation: A dictionary that combines both the blocks and navigation data.
Returned only if using the "blocks+navigation" endpoint.
"""
class
RequestInfo
(
object
):
"""
A class for encapsulating the request information, including what optional fields are requested.
"""
DEFAULT_FIELDS
=
"children,graded,format,responsive_ui"
def
__init__
(
self
,
request
,
course
):
self
.
request
=
request
self
.
course
=
course
self
.
field_data_cache
=
None
# check what fields are requested
try
:
# fields
self
.
fields
=
set
(
request
.
GET
.
get
(
'fields'
,
self
.
DEFAULT_FIELDS
)
.
split
(
","
))
# children
self
.
children
=
'children'
in
self
.
fields
self
.
fields
.
discard
(
'children'
)
# block_count
self
.
block_count
=
request
.
GET
.
get
(
'block_count'
,
""
)
self
.
block_count
=
(
self
.
block_count
.
split
(
","
)
if
self
.
block_count
else
[]
)
# navigation_depth
# See docstring for why we default to 3.
self
.
navigation_depth
=
int
(
request
.
GET
.
get
(
'navigation_depth'
,
'3'
))
# block_json
self
.
block_json
=
json
.
loads
(
request
.
GET
.
get
(
'block_json'
,
"{}"
))
if
self
.
block_json
and
not
isinstance
(
self
.
block_json
,
dict
):
raise
ParseError
except
:
raise
ParseError
class
ResultData
(
object
):
"""
A class for encapsulating the result information, specifically the blocks and navigation data.
"""
def
__init__
(
self
,
return_blocks
,
return_nav
):
self
.
blocks
=
{}
self
.
navigation
=
{}
if
return_blocks
and
return_nav
:
self
.
navigation
=
self
.
blocks
def
update_response
(
self
,
response
,
return_blocks
,
return_nav
):
"""
Updates the response object with result information.
"""
if
return_blocks
and
return_nav
:
response
[
"blocks+navigation"
]
=
self
.
blocks
elif
return_blocks
:
response
[
"blocks"
]
=
self
.
blocks
elif
return_nav
:
response
[
"navigation"
]
=
self
.
navigation
class
BlockInfo
(
object
):
"""
A class for encapsulating a block's information as needed during traversal of a block hierarchy.
"""
def
__init__
(
self
,
block
,
request_info
,
parent_block_info
=
None
):
# the block for which the recursion is being computed
self
.
block
=
block
# the type of the block
self
.
type
=
block
.
category
# the block's depth in the block hierarchy
self
.
depth
=
0
# the block's children
self
.
children
=
[]
# descendants_of_parent: the list of descendants for this block's parent
self
.
descendants_of_parent
=
[]
self
.
descendants_of_self
=
[]
# if a parent block was provided, update this block's data based on the parent's data
if
parent_block_info
:
# increment this block's depth value
self
.
depth
=
parent_block_info
.
depth
+
1
# set this blocks' descendants_of_parent
self
.
descendants_of_parent
=
parent_block_info
.
descendants_of_self
# add ourselves to the parent's children, if requested.
if
request_info
.
children
:
parent_block_info
.
value
.
setdefault
(
"children"
,
[])
.
append
(
unicode
(
block
.
location
))
# the block's data to include in the response
self
.
value
=
{
"id"
:
unicode
(
block
.
location
),
"type"
:
self
.
type
,
"display_name"
:
block
.
display_name
,
"web_url"
:
reverse
(
"jump_to"
,
kwargs
=
{
"course_id"
:
unicode
(
request_info
.
course
.
id
),
"location"
:
unicode
(
block
.
location
)},
request
=
request_info
.
request
,
),
"block_url"
:
reverse
(
"courseware.views.render_xblock"
,
kwargs
=
{
"usage_key_string"
:
unicode
(
block
.
location
)},
request
=
request_info
.
request
,
),
}
@view_course_access
(
depth
=
None
)
def
list
(
self
,
request
,
course
,
return_blocks
=
True
,
return_nav
=
True
,
*
args
,
**
kwargs
):
"""
REST API endpoint for listing all the blocks and/or navigation information in the course,
while regarding user access and roles.
Arguments:
request - Django request object
course - course module object
return_blocks - If true, returns the blocks information for the course.
return_nav - If true, returns the navigation information for the course.
"""
# set starting point
start_block
=
course
# initialize request and result objects
request_info
=
self
.
RequestInfo
(
request
,
course
)
result_data
=
self
.
ResultData
(
return_blocks
,
return_nav
)
# create and populate a field data cache by pre-fetching for the course (with depth=None)
request_info
.
field_data_cache
=
FieldDataCache
.
cache_for_descriptor_descendents
(
course
.
id
,
request
.
user
,
course
,
depth
=
None
,
)
# start the recursion with the start_block
self
.
recurse_blocks_nav
(
request_info
,
result_data
,
self
.
BlockInfo
(
start_block
,
request_info
))
# return response
response
=
{
"root"
:
unicode
(
start_block
.
location
)}
result_data
.
update_response
(
response
,
return_blocks
,
return_nav
)
return
Response
(
response
)
def
recurse_blocks_nav
(
self
,
request_info
,
result_data
,
block_info
):
"""
A depth-first recursive function that supports calculation of both the list of blocks in the course
and the navigation information up to the requested navigation_depth of the course.
Arguments:
request_info - Object encapsulating the request information.
result_data - Running result data that is updated during the recursion.
block_info - Information about the current block in the recursion.
"""
# bind user data to the block
block_info
.
block
=
get_module_for_descriptor
(
request_info
.
request
.
user
,
request_info
.
request
,
block_info
.
block
,
request_info
.
field_data_cache
,
request_info
.
course
.
id
)
# verify the user has access to this block
if
not
has_access
(
request_info
.
request
.
user
,
'load'
,
block_info
.
block
,
course_key
=
request_info
.
course
.
id
):
return
# add the block's value to the result
result_data
.
blocks
[
unicode
(
block_info
.
block
.
location
)]
=
block_info
.
value
# descendants
self
.
update_descendants
(
request_info
,
result_data
,
block_info
)
# children: recursively call the function for each of the children, while supporting dynamic children.
if
block_info
.
block
.
has_children
:
block_info
.
children
=
get_dynamic_descriptor_children
(
block_info
.
block
,
request_info
.
request
.
user
.
id
)
for
child
in
block_info
.
children
:
self
.
recurse_blocks_nav
(
request_info
,
result_data
,
self
.
BlockInfo
(
child
,
request_info
,
parent_block_info
=
block_info
)
)
# block count
self
.
update_block_count
(
request_info
,
result_data
,
block_info
)
# block JSON data
self
.
add_block_json
(
request_info
,
block_info
)
# additional fields
self
.
add_additional_fields
(
request_info
,
block_info
)
def
update_descendants
(
self
,
request_info
,
result_data
,
block_info
):
"""
Updates the descendants data for the current block.
The current block is added to its parent's descendants if it is visible in the navigation
(i.e., the 'hide_from_toc' setting is False).
Additionally, the block's depth is compared with the navigation_depth parameter to determine whether the
descendants of the block should be added to its own descendants (if block.depth <= navigation_depth)
or to the descendants of the block's parents (if block.depth > navigation_depth).
block_info.descendants_of_self is the list of descendants that is passed to this block's children.
It should be either:
descendants_of_parent - if this block's depth is greater than the requested navigation_depth.
a dangling [] - if this block's hide_from_toc is True.
a referenced [] in navigation[block.location]["descendants"] - if this block's depth is within
the requested navigation depth.
"""
# Blocks with the 'hide_from_toc' setting are accessible, just not navigatable from the table-of-contents.
# If the 'hide_from_toc' setting is set on the block, do not add this block to the parent's descendants
# list and let the block's descendants add themselves to a dangling (unreferenced) descendants list.
if
not
block_info
.
block
.
hide_from_toc
:
# add this block to the parent's descendants
block_info
.
descendants_of_parent
.
append
(
unicode
(
block_info
.
block
.
location
))
# if this block's depth in the hierarchy is greater than the requested navigation depth,
# have the block's descendants add themselves to the parent's descendants.
if
block_info
.
depth
>
request_info
.
navigation_depth
:
block_info
.
descendants_of_self
=
block_info
.
descendants_of_parent
# otherwise, have the block's descendants add themselves to this block's descendants by
# referencing/attaching descendants_of_self from this block's navigation value.
else
:
result_data
.
navigation
.
setdefault
(
unicode
(
block_info
.
block
.
location
),
{}
)[
"descendants"
]
=
block_info
.
descendants_of_self
def
update_block_count
(
self
,
request_info
,
result_data
,
block_info
):
"""
For all the block types that are requested to be counted, include the count of that block type as
aggregated from the block's descendants.
Arguments:
request_info - Object encapsulating the request information.
result_data - Running result data that is updated during the recursion.
block_info - Information about the current block in the recursion.
"""
for
b_type
in
request_info
.
block_count
:
block_info
.
value
.
setdefault
(
"block_count"
,
{})[
b_type
]
=
(
sum
(
result_data
.
blocks
.
get
(
unicode
(
child
.
location
),
{})
.
get
(
"block_count"
,
{})
.
get
(
b_type
,
0
)
for
child
in
block_info
.
children
)
+
(
1
if
b_type
==
block_info
.
type
else
0
)
)
def
add_block_json
(
self
,
request_info
,
block_info
):
"""
If the JSON data for this block's type is requested, and the block supports the 'student_view_json'
method, add the response from the 'student_view_json" method as the data for the block.
"""
if
block_info
.
type
in
request_info
.
block_json
:
if
getattr
(
block_info
.
block
,
'student_view_json'
,
None
):
block_info
.
value
[
"block_json"
]
=
block_info
.
block
.
student_view_json
(
context
=
request_info
.
block_json
[
block_info
.
type
]
)
# A mapping of API-exposed field names to xBlock field names and API field defaults.
BlockApiField
=
namedtuple
(
'BlockApiField'
,
'block_field_name api_field_default'
)
FIELD_MAP
=
{
'graded'
:
BlockApiField
(
block_field_name
=
'graded'
,
api_field_default
=
False
),
'format'
:
BlockApiField
(
block_field_name
=
'format'
,
api_field_default
=
None
),
'responsive_ui'
:
BlockApiField
(
block_field_name
=
'has_responsive_ui'
,
api_field_default
=
False
),
}
def
add_additional_fields
(
self
,
request_info
,
block_info
):
"""
Add additional field names and values of the block as requested in the request_info.
"""
for
field_name
in
request_info
.
fields
:
if
field_name
in
self
.
FIELD_MAP
:
block_info
.
value
[
field_name
]
=
getattr
(
block_info
.
block
,
self
.
FIELD_MAP
[
field_name
]
.
block_field_name
,
self
.
FIELD_MAP
[
field_name
]
.
api_field_default
,
)
def
perform_authentication
(
self
,
request
):
"""
Ensures that the user is authenticated (e.g. not an AnonymousUser)
"""
super
(
CourseBlocksAndNavigation
,
self
)
.
perform_authentication
(
request
)
if
request
.
user
.
is_anonymous
():
raise
AuthenticationFailed
lms/djangoapps/courseware/entrance_exams.py
View file @
4921ec48
...
...
@@ -9,7 +9,7 @@ from courseware.models import StudentModule
from
opaque_keys.edx.keys
import
UsageKey
from
student.models
import
EntranceExamConfiguration
from
util.milestones_helpers
import
get_required_content
from
util.module_utils
import
yield_dynamic_descriptor_descend
e
nts
from
util.module_utils
import
yield_dynamic_descriptor_descend
a
nts
from
xmodule.modulestore.django
import
modulestore
...
...
@@ -147,8 +147,9 @@ def get_entrance_exam_score(request, course):
course
.
id
)
exam_module_generators
=
yield_dynamic_descriptor_descend
e
nts
(
exam_module_generators
=
yield_dynamic_descriptor_descend
a
nts
(
exam_descriptor
,
request
.
user
.
id
,
inner_get_module
)
exam_modules
=
[
module
for
module
in
exam_module_generators
]
...
...
lms/djangoapps/courseware/grades.py
View file @
4921ec48
...
...
@@ -15,7 +15,7 @@ import dogstats_wrapper as dog_stats_api
from
courseware
import
courses
from
courseware.model_data
import
FieldDataCache
from
student.models
import
anonymous_id_for_user
from
util.module_utils
import
yield_dynamic_descriptor_descend
e
nts
from
util.module_utils
import
yield_dynamic_descriptor_descend
a
nts
from
xmodule
import
graders
from
xmodule.graders
import
Score
from
xmodule.modulestore.django
import
modulestore
...
...
@@ -209,7 +209,9 @@ def _grade(student, request, course, keep_raw_scores):
field_data_cache
=
FieldDataCache
([
descriptor
],
course
.
id
,
student
)
return
get_module_for_descriptor
(
student
,
request
,
descriptor
,
field_data_cache
,
course
.
id
)
for
module_descriptor
in
yield_dynamic_descriptor_descendents
(
section_descriptor
,
create_module
):
for
module_descriptor
in
yield_dynamic_descriptor_descendants
(
section_descriptor
,
student
.
id
,
create_module
):
(
correct
,
total
)
=
get_score
(
course
.
id
,
student
,
module_descriptor
,
create_module
,
scores_cache
=
submissions_scores
...
...
@@ -364,7 +366,9 @@ def _progress_summary(student, request, course):
module_creator
=
section_module
.
xmodule_runtime
.
get_module
for
module_descriptor
in
yield_dynamic_descriptor_descendents
(
section_module
,
module_creator
):
for
module_descriptor
in
yield_dynamic_descriptor_descendants
(
section_module
,
student
.
id
,
module_creator
):
course_id
=
course
.
id
(
correct
,
total
)
=
get_score
(
course_id
,
student
,
module_descriptor
,
module_creator
,
scores_cache
=
submissions_scores
...
...
lms/djangoapps/mobile_api/video_outlines/serializers.py
View file @
4921ec48
...
...
@@ -86,6 +86,7 @@ class BlockOutline(object):
if
curr_block
.
has_children
:
children
=
get_dynamic_descriptor_children
(
curr_block
,
self
.
request
.
user
.
id
,
create_module
,
usage_key_filter
=
parent_or_requested_block_type
)
...
...
lms/envs/common.py
View file @
4921ec48
...
...
@@ -321,7 +321,10 @@ FEATURES = {
# ENABLE_OAUTH2_PROVIDER to True
'ENABLE_MOBILE_REST_API'
:
False
,
'ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES'
:
False
,
# Enable APIs required for xBlocks on Mobile, and supported in general
'ENABLE_RENDER_XBLOCK_API'
:
False
,
'ENABLE_COURSE_BLOCKS_NAVIGATION_API'
:
False
,
# Enable the combined login/registration form
'ENABLE_COMBINED_LOGIN_REGISTRATION'
:
False
,
...
...
lms/envs/test.py
View file @
4921ec48
...
...
@@ -268,6 +268,8 @@ FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
FEATURES
[
'ENABLE_MOBILE_REST_API'
]
=
True
FEATURES
[
'ENABLE_MOBILE_SOCIAL_FACEBOOK_FEATURES'
]
=
True
FEATURES
[
'ENABLE_VIDEO_ABSTRACTION_LAYER_API'
]
=
True
FEATURES
[
'ENABLE_COURSE_BLOCKS_NAVIGATION_API'
]
=
True
FEATURES
[
'ENABLE_RENDER_XBLOCK_API'
]
=
True
###################### Payment ##############################3
# Enable fake payment processing page
...
...
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