Commit a1d58561 by Kelketek

Merge pull request #453 from open-craft/jump_to_children

Jump to children
parents 47dbf5c4 a1afd2b0
......@@ -81,6 +81,7 @@ def path_to_location(modulestore, usage_key):
# pull out the location names
chapter = path[1].name if n > 1 else None
section = path[2].name if n > 2 else None
vertical = path[3].name if n > 3 else None
# Figure out the position
position = None
......@@ -104,4 +105,4 @@ def path_to_location(modulestore, usage_key):
position_list.append(str(child_locs.index(path[path_index + 1]) + 1))
position = "_".join(position_list)
return (course_id, chapter, section, position)
return (course_id, chapter, section, vertical, position, path[-1])
......@@ -102,7 +102,6 @@ class CoursesApiTests(ModuleStoreTestCase):
)
return module
def setUp(self):
self.test_server_prefix = 'https://testserver'
self.base_courses_uri = '/api/server/courses'
......@@ -151,11 +150,32 @@ class CoursesApiTests(ModuleStoreTestCase):
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(
category="video",
parent_location=self.course_content.location,
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(
......@@ -340,7 +360,6 @@ class CoursesApiTests(ModuleStoreTestCase):
self.assertIsNotNone(course['course_image_url'])
self.assertItemsEqual(courses, courses_in_result)
def test_course_detail_without_date_values(self):
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)
......@@ -397,7 +416,7 @@ class CoursesApiTests(ModuleStoreTestCase):
chapter = response.data['content'][0]
self.assertEqual(chapter['category'], 'chapter')
self.assertEqual(chapter['name'], 'Overview')
self.assertEqual(len(chapter['children']), 5)
self.assertEqual(len(chapter['children']), 6)
sequence = chapter['children'][0]
self.assertEqual(sequence['category'], 'videosequence')
......@@ -2472,3 +2491,21 @@ class CoursesApiTests(ModuleStoreTestCase):
delete_uri = '{}invalid_role/users/{}'.format(test_uri, self.users[0].id)
response = self.do_delete(delete_uri)
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(
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}/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'/*$^', courses_views.CoursesList.as_view()),
)
......
......@@ -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.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 gradebook.models import StudentGradebook
from instructor.access import revoke_access, update_forum_role
......@@ -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.utils import CommentClientRequestError
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 projects.models import Project, Workgroup
from projects.serializers import ProjectSerializer, BasicWorkgroupSerializer
from student.models import CourseEnrollment, CourseEnrollmentAllowed
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.search import path_to_location
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
......@@ -1961,3 +1962,47 @@ class CoursesRolesUsersDetail(SecureAPIView):
return Response({}, status=status.HTTP_404_NOT_FOUND)
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)
......@@ -23,6 +23,7 @@ from django.utils.timezone import UTC
from django.views.decorators.http import require_GET
from django.http import Http404, HttpResponse
from django.shortcuts import redirect
from opaque_keys.edx.keys import CourseKey
from edxmako.shortcuts import render_to_response, render_to_string, marketing_link
from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control
......@@ -60,6 +61,7 @@ from util.milestones_helpers import get_prerequisite_courses_display
from microsite_configuration import microsite
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locations import BlockUsageLocator
from instructor.enrollment import uses_shib
from util.db import commit_on_success_with_read_committed
......@@ -492,7 +494,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# Save where we are in the chapter
save_child_position(chapter_module, section)
context['fragment'] = section_module.render(STUDENT_VIEW)
section_render_context = {'activate_block_id': request.GET.get('activate_block_id')}
context['fragment'] = section_module.render(STUDENT_VIEW, section_render_context)
context['section_title'] = section_descriptor.display_name_with_default
else:
# section is none, so display a message
......@@ -559,29 +562,32 @@ def _index_bulk_op(request, course_key, chapter, section, position):
return result
@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 = SlashSeparatedCourseKey.from_deprecated_string(course_id)
def item_finder(request, course_key, module_id):
items = modulestore().get_items(course_key, qualifiers={'name': module_id})
if len(items) == 0:
raise Http404(
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:
log.warning(
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", ""), unicode(items[0])
))
return items
return jump_to(request, course_id, items[0].location.to_deprecated_string())
@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
......@@ -600,7 +606,7 @@ def jump_to(request, course_id, location):
except InvalidKeyError:
raise Http404(u"Invalid course_key or usage_key")
try:
(course_key, chapter, section, position) = path_to_location(modulestore(), usage_key)
(course_key, chapter, section, vertical, position, final_target_id) = path_to_location(modulestore(), usage_key)
except ItemNotFoundError:
raise Http404(u"No data at this location: {0}".format(usage_key))
except NoPathToItem:
......@@ -609,14 +615,29 @@ def jump_to(request, course_id, location):
# choose the appropriate view (and provide the necessary args) based on the
# args provided by the redirect.
# Rely on index to do all error handling and access control.
if chapter is None:
return redirect('courseware', course_id=course_key.to_deprecated_string())
redirect_url = reverse('courseware', args=(unicode(course_key), ))
elif section is None:
return redirect('courseware_chapter', course_id=course_key.to_deprecated_string(), chapter=chapter)
redirect_url = reverse('courseware_chapter', args=(unicode(course_key), chapter))
elif position is None:
return redirect('courseware_section', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section)
redirect_url = reverse(
'courseware_section',
args=(unicode(course_key), chapter, section)
)
else:
return redirect('courseware_position', course_id=course_key.to_deprecated_string(), chapter=chapter, section=section, position=position)
# Here we use the navigation_index from the position returned from
# path_to_location - we can only navigate to the topmost vertical at the
# moment
redirect_url = reverse(
'courseware_position',
args=(unicode(course_key), chapter, section, position)
)
redirect_url += "?{}".format(urllib.urlencode({'activate_block_id': unicode(final_target_id)}))
return redirect(redirect_url)
@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