Commit deb9148f by Nimisha Asthagiri

Remove unused Mobile CourseBlocksAndNavigation endpoint.

parent fa8ca11d
......@@ -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'
),
)
""" 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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment