Commit 691fed7a by Nimisha Asthagiri

fixup! courses api - new endpoint that takes course_id

parent 6b2e00e9
"""
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
......@@ -12,16 +12,13 @@ from xmodule.modulestore.tests.factories import ToyCourseFactory
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
def setUpClass(cls):
super(TestCourseBlocksView, cls).setUpClass()
def setup_course(cls):
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(
unicode(item.location)
......@@ -29,14 +26,8 @@ class TestCourseBlocksView(SharedModuleStoreTestCase):
# 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'
)
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.client.login(username=self.user.username, password='test')
......@@ -74,6 +65,25 @@ class TestCourseBlocksView(SharedModuleStoreTestCase):
else:
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):
self.client.logout()
self.verify_response(401)
......@@ -85,7 +95,7 @@ class TestCourseBlocksView(SharedModuleStoreTestCase):
def test_non_existent_course(self):
usage_key = self.store.make_course_usage_key(CourseLocator('non', 'existent', 'course'))
url = reverse(
'course_blocks',
'blocks_in_block_tree',
kwargs={'usage_key_string': unicode(usage_key)}
)
self.verify_response(403, url=url)
......@@ -153,3 +163,28 @@ class TestCourseBlocksView(SharedModuleStoreTestCase):
set(unicode(child.location) for child in xblock.get_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):
STUDENT_VIEW_DATA = 'student_view_data'
STUDENT_VIEW_MULTI_DEVICE = 'student_view_multi_device'
def __init__(self, requested_student_view_data):
self.requested_student_view_data = requested_student_view_data
def __init__(self, requested_student_view_data = None):
self.requested_student_view_data = requested_student_view_data or []
@classmethod
def collect(cls, block_structure):
......
......@@ -3,14 +3,22 @@ Course Block API URLs
"""
from django.conf import settings
from django.conf.urls import patterns, url
from .views import CourseBlocks
from .views import BlocksView, BlocksInCourseView
urlpatterns = patterns(
'',
# This endpoint requires the usage_key for the starting block.
url(
r"^{}".format(settings.USAGE_KEY_PATTERN),
CourseBlocks.as_view(),
name="course_blocks"
r'^v1/blocks/{}'.format(settings.USAGE_KEY_PATTERN),
BlocksView.as_view(),
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
from rest_framework.generics import ListAPIView
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 xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from transformers.blocks_api import BlocksAPITransformer
from .api import get_blocks
from .forms import BlockListGetForm
from .serializers import BlockSerializer, BlockDictSerializer
@view_auth_classes()
class CourseBlocks(DeveloperErrorViewMixin, ListAPIView):
class BlocksView(DeveloperErrorViewMixin, ListAPIView):
"""
**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**:
......@@ -120,39 +121,140 @@ class CourseBlocks(DeveloperErrorViewMixin, ListAPIView):
course - course module object
"""
# request parameters
requested_params = request.GET.copy()
# validate request parameters
requested_params = request.QUERY_PARAMS.copy()
requested_params.update({'usage_key': usage_key_string})
params = BlockListGetForm(requested_params, initial={'requesting_user': request.user})
if not params.is_valid():
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:
blocks = get_course_blocks(
params.cleaned_data['user'],
params.cleaned_data['usage_key'],
transformers=LMS_COURSE_TRANSFORMERS + [blocks_api_transformer],
return Response(
get_blocks(
request,
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:
raise Http404("Course block not found: {}".format(exception.message))
# serialize
serializer_context = {
'request': request,
'block_structure': blocks,
'requested_fields': params.cleaned_data['requested_fields'],
}
if params.cleaned_data['return_type'] == 'dict':
serializer = BlockDictSerializer(blocks, context=serializer_context, many=False)
else:
serializer = BlockSerializer(blocks, context=serializer_context, many=True)
# response
return Response(serializer.data)
raise Http404("Block not found: {}".format(exception.message))
@view_auth_classes()
class BlocksInCourseView(BlocksView):
"""
**Use Case**
Returns the blocks in the course according to the requesting user's access level.
**Example requests**:
GET /api/courses/v1/blocks/?course_id=<course_id>
GET /api/courses/v1/blocks/?course_id=<course_id>
&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
urlpatterns = patterns(
'',
url(r'^v1/course/{}'.format(settings.COURSE_KEY_PATTERN), CourseView.as_view(), name="course_detail"),
url(r'^v1/blocks/', include('course_api.blocks.urls'))
url(r'^v1/courses/{}'.format(settings.COURSE_KEY_PATTERN), CourseView.as_view(), name="course_detail"),
url(r'', include('course_api.blocks.urls'))
)
......@@ -22,7 +22,7 @@ class CourseView(APIView):
return Response({
'blocks_url': reverse(
'course_blocks',
'blocks_in_block_tree',
kwargs={'usage_key_string': unicode(course_usage_key)},
request=request,
)
......
......@@ -76,7 +76,7 @@ urlpatterns = (
url(r'^api/course_structure/', include('course_structure_api.urls', namespace='course_structure_api')),
# Course API
url(r'^api/course/', include('course_api.urls')),
url(r'^api/courses/', include('course_api.urls')),
# User API endpoints
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