Remove unused Mobile CourseBlocksAndNavigation endpoint.
This commit is contained in:
@@ -3,10 +3,8 @@ 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
|
||||
|
||||
@@ -15,13 +13,11 @@ 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 SharedModuleStoreTestCase
|
||||
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
|
||||
@@ -457,232 +453,3 @@ class CourseGradingPolicyMissingFieldsTests(CourseDetailTestMixin, CourseViewTes
|
||||
}
|
||||
]
|
||||
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(4):
|
||||
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), 5)
|
||||
|
||||
# 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)
|
||||
|
||||
@SharedModuleStoreTestCase.modifies_courseware
|
||||
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)
|
||||
|
||||
def test_multi_device_support(self):
|
||||
"""
|
||||
Verifies the view's response when multi_device support is requested.
|
||||
"""
|
||||
response = self.http_get_for_course(
|
||||
data={'fields': 'multi_device'}
|
||||
)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
for block, expected_multi_device_support in (
|
||||
(self.problem, True),
|
||||
(self.html, True),
|
||||
(self.video, False)
|
||||
):
|
||||
block_response = response.data[self.block_navigation_view_type][unicode(block.location)]
|
||||
self.assertEquals(block_response['multi_device'], expected_multi_device_support)
|
||||
|
||||
|
||||
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']), 4)
|
||||
|
||||
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, 3),
|
||||
)
|
||||
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, SharedModuleStoreTestCase):
|
||||
"""
|
||||
A Test class for testing the Course 'blocks' view.
|
||||
"""
|
||||
block_navigation_view_type = 'blocks'
|
||||
container_fields = ['children']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CourseBlocksTests, cls).setUpClass()
|
||||
cls.create_course_data()
|
||||
|
||||
|
||||
class CourseNavigationTests(CourseBlocksOrNavigationTestMixin, CourseNavigationTestMixin, SharedModuleStoreTestCase):
|
||||
"""
|
||||
A Test class for testing the Course 'navigation' view.
|
||||
"""
|
||||
block_navigation_view_type = 'navigation'
|
||||
container_fields = ['descendants']
|
||||
block_fields = []
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CourseNavigationTests, cls).setUpClass()
|
||||
cls.create_course_data()
|
||||
|
||||
|
||||
class CourseBlocksAndNavigationTests(CourseBlocksOrNavigationTestMixin, CourseBlocksTestMixin,
|
||||
CourseNavigationTestMixin, SharedModuleStoreTestCase):
|
||||
"""
|
||||
A Test class for testing the Course 'blocks+navigation' view.
|
||||
"""
|
||||
block_navigation_view_type = 'blocks+navigation'
|
||||
container_fields = ['children', 'descendants']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CourseBlocksAndNavigationTests, cls).setUpClass()
|
||||
cls.create_course_data()
|
||||
|
||||
@@ -20,27 +20,3 @@ urlpatterns = patterns(
|
||||
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'
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
""" API implementation for course-oriented interactions. """
|
||||
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
@@ -12,20 +10,15 @@ 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
|
||||
|
||||
from course_structure_api.v0 import serializers
|
||||
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.djangoapps.content.course_structures.api.v0 import api, errors
|
||||
from openedx.core.lib.exceptions import CourseNotFoundError
|
||||
from student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from util.module_utils import get_dynamic_descriptor_children
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -297,396 +290,3 @@ 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,multi_device
|
||||
|
||||
**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,multi_device"
|
||||
|
||||
Example: fields=graded,format,multi_device
|
||||
|
||||
* 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.
|
||||
|
||||
* multi_device: (boolean) Whether or not the block's rendering obtained via block_url has support
|
||||
for multiple devices.
|
||||
Returned only if "multi_device" 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,multi_device"
|
||||
|
||||
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(","))
|
||||
|
||||
# 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 'children' in request_info.fields:
|
||||
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,
|
||||
course=request_info.course
|
||||
)
|
||||
|
||||
# verify the user has access to this block
|
||||
if (block_info.block is None or 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)
|
||||
|
||||
# multi-device support
|
||||
if 'multi_device' in request_info.fields:
|
||||
block_info.value['multi_device'] = block_info.block.has_support(
|
||||
getattr(block_info.block, 'student_view', None),
|
||||
'multi_device'
|
||||
)
|
||||
|
||||
# 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_data', None):
|
||||
block_info.value["block_json"] = block_info.block.student_view_data(
|
||||
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),
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -272,9 +272,6 @@ FEATURES = {
|
||||
# ENABLE_OAUTH2_PROVIDER to True
|
||||
'ENABLE_MOBILE_REST_API': False,
|
||||
|
||||
# Enable temporary APIs required for xBlocks on Mobile
|
||||
'ENABLE_COURSE_BLOCKS_NAVIGATION_API': False,
|
||||
|
||||
# Enable the combined login/registration form
|
||||
'ENABLE_COMBINED_LOGIN_REGISTRATION': False,
|
||||
|
||||
|
||||
@@ -294,7 +294,6 @@ OIDC_COURSE_HANDLER_CACHE_TIMEOUT = 0
|
||||
########################### External REST APIs #################################
|
||||
FEATURES['ENABLE_MOBILE_REST_API'] = True
|
||||
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
|
||||
FEATURES['ENABLE_COURSE_BLOCKS_NAVIGATION_API'] = True
|
||||
|
||||
###################### Payment ##############################3
|
||||
# Enable fake payment processing page
|
||||
|
||||
Reference in New Issue
Block a user