Commit f0b60717 by Jonathan Piacenti

Courseware Navigation API update for jump_to_children

parent 7ed5672e
...@@ -6,6 +6,7 @@ Run these tests @ Devstack: ...@@ -6,6 +6,7 @@ Run these tests @ Devstack:
from datetime import datetime from datetime import datetime
import json import json
import uuid import uuid
from django.utils import timezone
import mock import mock
from random import randint from random import randint
from urllib import urlencode from urllib import urlencode
...@@ -23,6 +24,7 @@ from courseware import module_render ...@@ -23,6 +24,7 @@ from courseware import module_render
from courseware.tests.factories import StudentModuleFactory from courseware.tests.factories import StudentModuleFactory
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from django_comment_common.models import Role, FORUM_ROLE_MODERATOR from django_comment_common.models import Role, FORUM_ROLE_MODERATOR
from gradebook.models import StudentGradebook
from instructor.access import allow_access from instructor.access import allow_access
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -92,7 +94,6 @@ class CoursesApiTests(ModuleStoreTestCase): ...@@ -92,7 +94,6 @@ class CoursesApiTests(ModuleStoreTestCase):
) )
return module return module
def setUp(self): def setUp(self):
self.test_server_prefix = 'https://testserver' self.test_server_prefix = 'https://testserver'
self.base_courses_uri = '/api/server/courses' self.base_courses_uri = '/api/server/courses'
...@@ -141,11 +142,32 @@ class CoursesApiTests(ModuleStoreTestCase): ...@@ -141,11 +142,32 @@ class CoursesApiTests(ModuleStoreTestCase):
display_name="Video_Sequence" display_name="Video_Sequence"
) )
self.course_content2 = ItemFactory.create(
category="sequential",
parent_location=self.chapter.location,
data=self.test_data,
display_name="Sequential",
)
self.content_child = ItemFactory.create( self.content_child = ItemFactory.create(
category="video", category="video",
parent_location=self.course_content.location, parent_location=self.course_content.location,
data=self.test_data, data=self.test_data,
display_name="Video_Resources" display_name="Video"
)
self.content_child2 = ItemFactory.create(
category="vertical",
parent_location=self.course_content2.location,
data=self.test_data,
display_name="Vertical Sequence"
)
self.content_subchild = ItemFactory.create(
category="video",
parent_location=self.content_child2.location,
data=self.test_data,
display_name="Child Video",
) )
self.overview = ItemFactory.create( self.overview = ItemFactory.create(
...@@ -330,7 +352,6 @@ class CoursesApiTests(ModuleStoreTestCase): ...@@ -330,7 +352,6 @@ class CoursesApiTests(ModuleStoreTestCase):
self.assertIsNotNone(course['course_image_url']) self.assertIsNotNone(course['course_image_url'])
self.assertItemsEqual(courses, courses_in_result) self.assertItemsEqual(courses, courses_in_result)
def test_course_detail_without_date_values(self): def test_course_detail_without_date_values(self):
create_course_with_out_date_values = CourseFactory.create() # pylint: disable=C0103 create_course_with_out_date_values = CourseFactory.create() # pylint: disable=C0103
test_uri = self.base_courses_uri + '/' + unicode(create_course_with_out_date_values.id) test_uri = self.base_courses_uri + '/' + unicode(create_course_with_out_date_values.id)
...@@ -387,7 +408,7 @@ class CoursesApiTests(ModuleStoreTestCase): ...@@ -387,7 +408,7 @@ class CoursesApiTests(ModuleStoreTestCase):
chapter = response.data['content'][0] chapter = response.data['content'][0]
self.assertEqual(chapter['category'], 'chapter') self.assertEqual(chapter['category'], 'chapter')
self.assertEqual(chapter['name'], 'Overview') self.assertEqual(chapter['name'], 'Overview')
self.assertEqual(len(chapter['children']), 5) self.assertEqual(len(chapter['children']), 6)
sequence = chapter['children'][0] sequence = chapter['children'][0]
self.assertEqual(sequence['category'], 'videosequence') self.assertEqual(sequence['category'], 'videosequence')
...@@ -2456,3 +2477,21 @@ class CoursesApiTests(ModuleStoreTestCase): ...@@ -2456,3 +2477,21 @@ class CoursesApiTests(ModuleStoreTestCase):
delete_uri = '{}invalid_role/users/{}'.format(test_uri, self.users[0].id) delete_uri = '{}invalid_role/users/{}'.format(test_uri, self.users[0].id)
response = self.do_delete(delete_uri) response = self.do_delete(delete_uri)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_course_navigation(self):
test_uri = '{}/{}/navigation/{}'.format(
self.base_courses_uri, unicode(self.course.id), self.content_subchild.location.block_id
)
response = self.do_get(test_uri)
self.maxDiff = None
self.assertEqual(
{
'chapter': unicode(self.chapter.location),
'vertical': unicode(self.content_child2.location),
'section': unicode(self.course_content2.location),
'course_key': unicode(self.course.id),
'final_target_id': unicode(self.content_subchild.location),
'position': '1',
},
response.data
)
...@@ -40,6 +40,7 @@ urlpatterns = patterns( ...@@ -40,6 +40,7 @@ urlpatterns = patterns(
url(r'^{0}/users/(?P<user_id>[0-9]+)$'.format(COURSE_ID_PATTERN), courses_views.CoursesUsersDetail.as_view()), url(r'^{0}/users/(?P<user_id>[0-9]+)$'.format(COURSE_ID_PATTERN), courses_views.CoursesUsersDetail.as_view()),
url(r'^{0}/users/*$'.format(COURSE_ID_PATTERN), courses_views.CoursesUsersList.as_view()), url(r'^{0}/users/*$'.format(COURSE_ID_PATTERN), courses_views.CoursesUsersList.as_view()),
url(r'^{0}/workgroups/*$'.format(COURSE_ID_PATTERN), courses_views.CoursesWorkgroupsList.as_view()), url(r'^{0}/workgroups/*$'.format(COURSE_ID_PATTERN), courses_views.CoursesWorkgroupsList.as_view()),
url(r'^{0}/navigation/{1}$'.format(COURSE_ID_PATTERN, settings.USAGE_KEY_PATTERN), courses_views.CourseNavView.as_view()),
url(r'^{0}$'.format(COURSE_ID_PATTERN), courses_views.CoursesDetail.as_view()), url(r'^{0}$'.format(COURSE_ID_PATTERN), courses_views.CoursesDetail.as_view()),
url(r'/*$^', courses_views.CoursesList.as_view()), url(r'/*$^', courses_views.CoursesList.as_view()),
) )
......
...@@ -21,7 +21,7 @@ from rest_framework.response import Response ...@@ -21,7 +21,7 @@ from rest_framework.response import Response
from courseware.courses import get_course_about_section, get_course_info_section, course_image_url from courseware.courses import get_course_about_section, get_course_info_section, course_image_url
from courseware.models import StudentModule from courseware.models import StudentModule
from courseware.views import get_static_tab_contents from courseware.views import get_static_tab_contents, item_finder
from django_comment_common.models import FORUM_ROLE_MODERATOR from django_comment_common.models import FORUM_ROLE_MODERATOR
from gradebook.models import StudentGradebook from gradebook.models import StudentGradebook
from instructor.access import revoke_access, update_forum_role from instructor.access import revoke_access, update_forum_role
...@@ -29,13 +29,14 @@ from lms.lib.comment_client.user import get_course_social_stats ...@@ -29,13 +29,14 @@ from lms.lib.comment_client.user import get_course_social_stats
from lms.lib.comment_client.thread import get_course_thread_stats from lms.lib.comment_client.thread import get_course_thread_stats
from lms.lib.comment_client.utils import CommentClientRequestError from lms.lib.comment_client.utils import CommentClientRequestError
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey, UsageKey
from progress.models import StudentProgress from progress.models import StudentProgress
from projects.models import Project, Workgroup from projects.models import Project, Workgroup
from projects.serializers import ProjectSerializer, BasicWorkgroupSerializer from projects.serializers import ProjectSerializer, BasicWorkgroupSerializer
from student.models import CourseEnrollment, CourseEnrollmentAllowed from student.models import CourseEnrollment, CourseEnrollmentAllowed
from student.roles import CourseRole, CourseAccessRole, CourseInstructorRole, CourseStaffRole, CourseObserverRole, CourseAssistantRole, UserBasedRole, get_aggregate_exclusion_user_ids from student.roles import CourseRole, CourseAccessRole, CourseInstructorRole, CourseStaffRole, CourseObserverRole, CourseAssistantRole, UserBasedRole, get_aggregate_exclusion_user_ids
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.search import path_to_location
from api_manager.courseware_access import get_course, get_course_child, get_course_leaf_nodes, get_course_key, \ from api_manager.courseware_access import get_course, get_course_child, get_course_leaf_nodes, get_course_key, \
course_exists, get_modulestore, get_course_descriptor course_exists, get_modulestore, get_course_descriptor
...@@ -1961,3 +1962,47 @@ class CoursesRolesUsersDetail(SecureAPIView): ...@@ -1961,3 +1962,47 @@ class CoursesRolesUsersDetail(SecureAPIView):
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
return Response({}, status=status.HTTP_204_NO_CONTENT) return Response({}, status=status.HTTP_204_NO_CONTENT)
class CourseNavView(SecureAPIView):
"""
### The CourseNavView view exposes navigation information for particular usage id: course, chapter, section and
vertical keys, position in innermost container and last addressable block/module on the path (usually the same
usage id that was passed as an argument)
- URI: ```/api/courses/{course_id}/navigation/{module_id}```
- GET: Gets navigation information
"""
def _get_full_location_key_by_module_id(self, request, course_key, module_id):
"""
Gets full location id by module id
"""
items = item_finder(request, course_key, module_id)
return items[0].location
def get(self, request, course_id, usage_key_string): # pylint: disable=W0613
"""
GET /api/courses/{course_id}/navigation/{module_id}
"""
try:
_, course_key, __ = get_course(request, request.user, course_id)
usage_key = self._get_full_location_key_by_module_id(request, course_key, usage_key_string)
except InvalidKeyError:
raise Http404(u"Invalid course_key or usage_key")
(course_key, chapter, section, vertical, position, final_target_id) = path_to_location(modulestore(), usage_key)
chapter_key = course_key.make_usage_key('chapter', chapter)
section_key = course_key.make_usage_key('sequential', section)
vertical_key = course_key.make_usage_key('vertical', vertical)
result = {
'course_key': unicode(course_key),
'chapter': unicode(chapter_key),
'section': unicode(section_key),
'vertical': unicode(vertical_key),
'position': unicode(position),
'final_target_id': unicode(final_target_id)
}
return Response(result, status=status.HTTP_200_OK)
...@@ -623,28 +623,34 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -623,28 +623,34 @@ def _index_bulk_op(request, course_key, chapter, section, position):
return result return result
@ensure_csrf_cookie def item_finder(request, course_key, module_id):
@ensure_valid_course_key
def jump_to_id(request, course_id, module_id):
"""
This entry point allows for a shorter version of a jump to where just the id of the element is
passed in. This assumes that id is unique within the course_id namespace
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
items = modulestore().get_items(course_key, qualifiers={'name': module_id}) items = modulestore().get_items(course_key, qualifiers={'name': module_id})
if len(items) == 0: if len(items) == 0:
raise Http404( raise Http404(
u"Could not find id: {0} in course_id: {1}. Referer: {2}".format( u"Could not find id: {0} in course_id: {1}. Referer: {2}".format(
module_id, course_id, request.META.get("HTTP_REFERER", "") module_id, course_key, request.META.get("HTTP_REFERER", "")
)) ))
if len(items) > 1: if len(items) > 1:
log.warning( log.warning(
u"Multiple items found with id: {0} in course_id: {1}. Referer: {2}. Using first: {3}".format( u"Multiple items found with id: {0} in course_id: {1}. Referer: {2}. Using first: {3}".format(
module_id, course_id, request.META.get("HTTP_REFERER", ""), items[0].location.to_deprecated_string() module_id, course_key, request.META.get("HTTP_REFERER", ""), items[0].location.to_deprecated_string()
)) ))
return jump_to(request, course_id, items[0].location.to_deprecated_string()) return items
@ensure_csrf_cookie
@ensure_valid_course_key
def jump_to_id(request, course_id, module_id):
"""
This entry point allows for a shorter version of a jump to where just the id of the element is
passed in. This assumes that id is unique within the course_id namespace
"""
course_key = CourseKey.from_string(course_id)
items = item_finder(request, course_key, module_id)
return jump_to(request, course_id, unicode(items[0].location))
@ensure_csrf_cookie @ensure_csrf_cookie
......
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