Commit 691fed7a by Nimisha Asthagiri

fixup! courses api - new endpoint that takes course_id

parent 6b2e00e9
""" """
TODO TODO
""" """
from transformers.blocks_api import BlocksAPITransformer
from .serializers import BlockSerializer, BlockDictSerializer
from lms.djangoapps.course_blocks.api import get_course_blocks, LMS_COURSE_TRANSFORMERS
def get_blocks(
request,
usage_key,
user = None,
depth = None,
nav_depth = None,
requested_fields = None,
block_counts = None,
student_view_data = None,
return_type = 'dict',
):
# TODO support user=None by returning all blocks, not just user-specific ones
if user is None:
raise NotImplementedError
# transform blocks
blocks_api_transformer = BlocksAPITransformer(
block_counts,
student_view_data,
depth,
nav_depth
)
blocks = get_course_blocks(
user,
usage_key,
transformers=LMS_COURSE_TRANSFORMERS + [blocks_api_transformer],
)
# serialize
serializer_context = {
'request': request,
'block_structure': blocks,
'requested_fields': requested_fields or [],
}
if return_type == 'dict':
serializer = BlockDictSerializer(blocks, context=serializer_context, many=False)
else:
serializer = BlockSerializer(blocks, context=serializer_context, many=True)
# return serialized data
return serializer.data
"""
Tests for Blocks api.py
"""
from django.test.client import RequestFactory
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import SampleCourseFactory
from ..api import get_blocks
class TestGetBlocks(ModuleStoreTestCase):
"""
Tests for the get_block_list method.
"""
def setUp(self):
super(TestGetBlocks, self).setUp()
self.course = SampleCourseFactory.create()
self.user = UserFactory.create()
self.request = RequestFactory().get("/dummy")
self.request.user = self.user
def test_basic(self):
blocks = get_blocks(self.request, self.course.location, self.user)
self.assertEquals(blocks['root'], unicode(self.course.location))
# add 1 for the orphaned course about block
self.assertEquals(len(blocks['blocks'])+1, len(self.store.get_items(self.course.id)))
def test_no_user(self):
with self.assertRaises(NotImplementedError):
get_blocks(self.request, self.course.location)
""" """
Tests for Course Blocks views Tests for Blocks Views
""" """
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -12,16 +12,13 @@ from xmodule.modulestore.tests.factories import ToyCourseFactory ...@@ -12,16 +12,13 @@ from xmodule.modulestore.tests.factories import ToyCourseFactory
from .test_utils import deserialize_usage_key from .test_utils import deserialize_usage_key
class TestCourseBlocksView(SharedModuleStoreTestCase): class TestBlocksViewMixin(object):
""" """
Test class for CourseBlocks view Mixin class for test helpers for BlocksView related classes
""" """
@classmethod @classmethod
def setUpClass(cls): def setup_course(cls):
super(TestCourseBlocksView, cls).setUpClass()
cls.course_key = ToyCourseFactory.create().id cls.course_key = ToyCourseFactory.create().id
cls.course_usage_key = cls.store.make_course_usage_key(cls.course_key)
cls.non_orphaned_block_usage_keys = set( cls.non_orphaned_block_usage_keys = set(
unicode(item.location) unicode(item.location)
...@@ -29,14 +26,8 @@ class TestCourseBlocksView(SharedModuleStoreTestCase): ...@@ -29,14 +26,8 @@ class TestCourseBlocksView(SharedModuleStoreTestCase):
# remove all orphaned items in the course, except for the root 'course' block # remove all orphaned items in the course, except for the root 'course' block
if cls.store.get_parent_location(item.location) or item.category == 'course' if cls.store.get_parent_location(item.location) or item.category == 'course'
) )
cls.url = reverse(
'course_blocks',
kwargs={'usage_key_string': unicode(cls.course_usage_key)}
)
def setUp(self):
super(TestCourseBlocksView, self).setUp()
def setup_user(self):
self.user = UserFactory.create() self.user = UserFactory.create()
self.client.login(username=self.user.username, password='test') self.client.login(username=self.user.username, password='test')
...@@ -74,6 +65,25 @@ class TestCourseBlocksView(SharedModuleStoreTestCase): ...@@ -74,6 +65,25 @@ class TestCourseBlocksView(SharedModuleStoreTestCase):
else: else:
self.assertFalse(expression) self.assertFalse(expression)
class TestBlocksView(TestBlocksViewMixin, SharedModuleStoreTestCase):
"""
Test class for BlocksView
"""
@classmethod
def setUpClass(cls):
super(TestBlocksView, cls).setUpClass()
cls.setup_course()
cls.course_usage_key = cls.store.make_course_usage_key(cls.course_key)
cls.url = reverse(
'blocks_in_block_tree',
kwargs={'usage_key_string': unicode(cls.course_usage_key)}
)
def setUp(self):
super(TestBlocksView, self).setUp()
self.setup_user()
def test_not_authenticated(self): def test_not_authenticated(self):
self.client.logout() self.client.logout()
self.verify_response(401) self.verify_response(401)
...@@ -85,7 +95,7 @@ class TestCourseBlocksView(SharedModuleStoreTestCase): ...@@ -85,7 +95,7 @@ class TestCourseBlocksView(SharedModuleStoreTestCase):
def test_non_existent_course(self): def test_non_existent_course(self):
usage_key = self.store.make_course_usage_key(CourseLocator('non', 'existent', 'course')) usage_key = self.store.make_course_usage_key(CourseLocator('non', 'existent', 'course'))
url = reverse( url = reverse(
'course_blocks', 'blocks_in_block_tree',
kwargs={'usage_key_string': unicode(usage_key)} kwargs={'usage_key_string': unicode(usage_key)}
) )
self.verify_response(403, url=url) self.verify_response(403, url=url)
...@@ -153,3 +163,28 @@ class TestCourseBlocksView(SharedModuleStoreTestCase): ...@@ -153,3 +163,28 @@ class TestCourseBlocksView(SharedModuleStoreTestCase):
set(unicode(child.location) for child in xblock.get_children()), set(unicode(child.location) for child in xblock.get_children()),
set(block_data['children']), set(block_data['children']),
) )
class TestBlocksInCourseView(TestBlocksViewMixin, SharedModuleStoreTestCase):
"""
Test class for BlocksInCourseView
"""
@classmethod
def setUpClass(cls):
super(TestBlocksInCourseView, cls).setUpClass()
cls.setup_course()
cls.url = reverse('blocks_in_course')
def setUp(self):
super(TestBlocksInCourseView, self).setUp()
self.setup_user()
def test_basic(self):
response = self.verify_response(params={'course_id': unicode(self.course_key)})
self.verify_response_block_dict(response)
def test_no_course_id(self):
self.verify_response(400)
def test_invalid_course_id(self):
self.verify_response(400, params={'course_id': 'invalid_course_id'})
...@@ -9,8 +9,8 @@ class StudentViewTransformer(BlockStructureTransformer): ...@@ -9,8 +9,8 @@ class StudentViewTransformer(BlockStructureTransformer):
STUDENT_VIEW_DATA = 'student_view_data' STUDENT_VIEW_DATA = 'student_view_data'
STUDENT_VIEW_MULTI_DEVICE = 'student_view_multi_device' STUDENT_VIEW_MULTI_DEVICE = 'student_view_multi_device'
def __init__(self, requested_student_view_data): def __init__(self, requested_student_view_data = None):
self.requested_student_view_data = requested_student_view_data self.requested_student_view_data = requested_student_view_data or []
@classmethod @classmethod
def collect(cls, block_structure): def collect(cls, block_structure):
......
...@@ -3,14 +3,22 @@ Course Block API URLs ...@@ -3,14 +3,22 @@ Course Block API URLs
""" """
from django.conf import settings from django.conf import settings
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from .views import CourseBlocks from .views import BlocksView, BlocksInCourseView
urlpatterns = patterns( urlpatterns = patterns(
'', '',
# This endpoint requires the usage_key for the starting block.
url( url(
r"^{}".format(settings.USAGE_KEY_PATTERN), r'^v1/blocks/{}'.format(settings.USAGE_KEY_PATTERN),
CourseBlocks.as_view(), BlocksView.as_view(),
name="course_blocks" name="blocks_in_block_tree"
),
# This endpoint is an alternative to the above, but requires course_id as a parameter.
url(
r'^v1/blocks/',
BlocksInCourseView.as_view(),
name="blocks_in_course"
), ),
) )
...@@ -3,21 +3,22 @@ from django.http import Http404 ...@@ -3,21 +3,22 @@ from django.http import Http404
from rest_framework.generics import ListAPIView from rest_framework.generics import ListAPIView
from rest_framework.response import Response from rest_framework.response import Response
from lms.djangoapps.course_blocks.api import get_course_blocks, LMS_COURSE_TRANSFORMERS from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx.core.lib.api.view_utils import view_auth_classes, DeveloperErrorViewMixin from openedx.core.lib.api.view_utils import view_auth_classes, DeveloperErrorViewMixin
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from transformers.blocks_api import BlocksAPITransformer from .api import get_blocks
from .forms import BlockListGetForm from .forms import BlockListGetForm
from .serializers import BlockSerializer, BlockDictSerializer
@view_auth_classes() @view_auth_classes()
class CourseBlocks(DeveloperErrorViewMixin, ListAPIView): class BlocksView(DeveloperErrorViewMixin, ListAPIView):
""" """
**Use Case** **Use Case**
Returns the blocks of the course according to the requesting user's access level. Returns the blocks within the requested block tree according to the requesting user's access level.
**Example requests**: **Example requests**:
...@@ -120,39 +121,140 @@ class CourseBlocks(DeveloperErrorViewMixin, ListAPIView): ...@@ -120,39 +121,140 @@ class CourseBlocks(DeveloperErrorViewMixin, ListAPIView):
course - course module object course - course module object
""" """
# request parameters # validate request parameters
requested_params = request.GET.copy() requested_params = request.QUERY_PARAMS.copy()
requested_params.update({'usage_key': usage_key_string}) requested_params.update({'usage_key': usage_key_string})
params = BlockListGetForm(requested_params, initial={'requesting_user': request.user}) params = BlockListGetForm(requested_params, initial={'requesting_user': request.user})
if not params.is_valid(): if not params.is_valid():
raise ValidationError(params.errors) raise ValidationError(params.errors)
# transform blocks
blocks_api_transformer = BlocksAPITransformer(
params.cleaned_data.get('block_counts', []),
params.cleaned_data.get('student_view_data', []),
params.cleaned_data.get('depth', None),
params.cleaned_data.get('nav_depth', None),
)
try: try:
blocks = get_course_blocks( return Response(
params.cleaned_data['user'], get_blocks(
params.cleaned_data['usage_key'], request,
transformers=LMS_COURSE_TRANSFORMERS + [blocks_api_transformer], params.cleaned_data['usage_key'],
params.cleaned_data['user'],
params.cleaned_data.get('depth', None),
params.cleaned_data.get('nav_depth', None),
params.cleaned_data['requested_fields'],
params.cleaned_data.get('block_counts', []),
params.cleaned_data.get('student_view_data', []),
params.cleaned_data['return_type']
)
) )
except ItemNotFoundError as exception: except ItemNotFoundError as exception:
raise Http404("Course block not found: {}".format(exception.message)) raise Http404("Block not found: {}".format(exception.message))
# serialize
serializer_context = { @view_auth_classes()
'request': request, class BlocksInCourseView(BlocksView):
'block_structure': blocks, """
'requested_fields': params.cleaned_data['requested_fields'], **Use Case**
}
if params.cleaned_data['return_type'] == 'dict': Returns the blocks in the course according to the requesting user's access level.
serializer = BlockDictSerializer(blocks, context=serializer_context, many=False)
else: **Example requests**:
serializer = BlockSerializer(blocks, context=serializer_context, many=True)
GET /api/courses/v1/blocks/?course_id=<course_id>
# response GET /api/courses/v1/blocks/?course_id=<course_id>
return Response(serializer.data) &user=anjali,
&depth=all,
&requested_fields=graded,format,student_view_multi_device,
&block_counts=video,
&student_view_data=video,
**Parameters**:
* student_view_data: (list) Indicates for which block types to return student_view_data.
Example: student_view_data=video
* block_counts: (list) Indicates for which block types to return the aggregate count of the blocks.
Example: block_counts=video,problem
* requested_fields: (list) Indicates which additional fields to return for each block.
The following fields are always returned: type, display_name
Example: requested_fields=graded,format,student_view_multi_device
* depth (integer or all) Indicates how deep to traverse into the blocks hierarchy.
A value of all means the entire hierarchy.
Default is 0
Example: depth=all
* nav_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: nav_depth=3
* return_type (string) Indicates in what data type to return the blocks.
Default is dict. Supported values are: dict, list
Example: return_type=dict
**Response Values**
The following fields are returned with a successful response.
* 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.
* 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 "children" is included in the "requested_fields" parameter.
* block_counts: (dict) For each block type specified in the block_counts 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_counts" input parameter contains this block's type.
* graded (boolean) Whether or not the block or any of its descendants is graded.
Returned only if "graded" is included in the "requested_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 "requested_fields" parameter.
* student_view_data: (dict) The JSON data for this block.
Returned only if the "student_view_data" input parameter contains this block's type.
* student_view_url: (string) The URL to retrieve the HTML rendering of this block's student view.
The HTML could include CSS and Javascript code. This field can be used in combination with the
student_view_multi_device field to decide whether to display this content to the user.
This URL can be used as a fallback if the student_view_data for this block type is not supported by
the client or the block.
* student_view_multi_device: (boolean) Whether or not the block's rendering obtained via block_url has support
for multiple devices.
Returned only if "student_view_multi_device" is included in the "requested_fields" parameter.
* lms_web_url: (string) The URL to the navigational container of the xBlock on the web LMS.
This URL can be used as a further fallback if the student_view_url and the student_view_data fields
are not supported.
"""
def list(self, request):
# convert the requested course_key to the course's root block's usage_key
course_key_string = request.QUERY_PARAMS.get('course_id', None)
if not course_key_string:
raise ValidationError('course_id is required.')
try:
course_key = CourseKey.from_string(course_key_string)
course_usage_key = modulestore().make_course_usage_key(course_key)
return super(BlocksInCourseView, self).list(request, course_usage_key)
except InvalidKeyError:
raise ValidationError("'{}' is not a valid course key.".format(unicode(course_key_string)))
...@@ -9,6 +9,6 @@ from .views import CourseView ...@@ -9,6 +9,6 @@ from .views import CourseView
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^v1/course/{}'.format(settings.COURSE_KEY_PATTERN), CourseView.as_view(), name="course_detail"), url(r'^v1/courses/{}'.format(settings.COURSE_KEY_PATTERN), CourseView.as_view(), name="course_detail"),
url(r'^v1/blocks/', include('course_api.blocks.urls')) url(r'', include('course_api.blocks.urls'))
) )
...@@ -22,7 +22,7 @@ class CourseView(APIView): ...@@ -22,7 +22,7 @@ class CourseView(APIView):
return Response({ return Response({
'blocks_url': reverse( 'blocks_url': reverse(
'course_blocks', 'blocks_in_block_tree',
kwargs={'usage_key_string': unicode(course_usage_key)}, kwargs={'usage_key_string': unicode(course_usage_key)},
request=request, request=request,
) )
......
...@@ -76,7 +76,7 @@ urlpatterns = ( ...@@ -76,7 +76,7 @@ urlpatterns = (
url(r'^api/course_structure/', include('course_structure_api.urls', namespace='course_structure_api')), url(r'^api/course_structure/', include('course_structure_api.urls', namespace='course_structure_api')),
# Course API # Course API
url(r'^api/course/', include('course_api.urls')), url(r'^api/courses/', include('course_api.urls')),
# User API endpoints # User API endpoints
url(r'^api/user/', include('openedx.core.djangoapps.user_api.urls')), url(r'^api/user/', include('openedx.core.djangoapps.user_api.urls')),
......
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