diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 70686f035308..790763af1aa7 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -338,11 +338,16 @@ def _course_outline_json(request, course_module): """ Returns a JSON representation of the course module and recursively all of its children. """ + is_concise = request.GET.get('formats') == 'concise' + include_children_predicate = lambda xblock: not xblock.category == 'vertical' + if is_concise: + include_children_predicate = lambda xblock: xblock.has_children return create_xblock_info( course_module, include_child_info=True, - course_outline=True, - include_children_predicate=lambda xblock: not xblock.category == 'vertical', + course_outline=False if is_concise else True, + include_children_predicate=include_children_predicate, + is_concise=is_concise, user=request.user ) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index df448141fde1..36a2834cb2d2 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -98,6 +98,7 @@ def xblock_handler(request, usage_key_string): GET json: returns representation of the xblock (locator id, data, and metadata). if ?fields=graderType, it returns the graderType for the unit instead of the above. + if ?fields=ancestorInfo, it returns ancestor info of the xblock. html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view) PUT or POST or PATCH json: if xblock locator is specified, update the xblock instance. The json payload can contain @@ -149,6 +150,10 @@ def xblock_handler(request, usage_key_string): if 'graderType' in fields: # right now can't combine output of this w/ output of _get_module_info, but worthy goal return JsonResponse(CourseGradingModel.get_section_grader_type(usage_key)) + elif 'ancestorInfo' in fields: + xblock = _get_xblock(usage_key, request.user) + ancestor_info = _create_xblock_ancestor_info(xblock, is_concise=True) + return JsonResponse(ancestor_info) # TODO: pass fields to _get_module_info and only return those with modulestore().bulk_operations(usage_key.course_key): response = _get_module_info(_get_xblock(usage_key, request.user)) @@ -887,7 +892,7 @@ def _get_gating_info(course, xblock): def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False, course_outline=False, include_children_predicate=NEVER, parent_xblock=None, graders=None, - user=None, course=None): + user=None, course=None, is_concise=False): """ Creates the information needed for client-side XBlockInfo. @@ -897,6 +902,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F There are three optional boolean parameters: include_ancestor_info - if true, ancestor info is added to the response include_child_info - if true, direct child info is included in the response + is_concise - if true, returns the concise version of xblock info, default is false. course_outline - if true, the xblock is being rendered on behalf of the course outline. There are certain expensive computations that do not need to be included in this case. @@ -933,20 +939,22 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F graders, include_children_predicate=include_children_predicate, user=user, - course=course + course=course, + is_concise=is_concise ) else: child_info = None release_date = _get_release_date(xblock, user) - if xblock.category != 'course': + if xblock.category != 'course' and not is_concise: visibility_state = _compute_visibility_state( xblock, child_info, is_xblock_unit and has_changes, is_self_paced(course) ) else: visibility_state = None published = modulestore().has_published_version(xblock) if not is_library_block else None + published_on = get_default_time_display(xblock.published_on) if published and xblock.published_on else None # defining the default value 'True' for delete, duplicate, drag and add new child actions # in xblock_actions for each xblock. @@ -970,82 +978,89 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F pct_sign=_('%')) xblock_info = { - "id": unicode(xblock.location), - "display_name": xblock.display_name_with_default, - "category": xblock.category, - "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, - "published": published, - "published_on": get_default_time_display(xblock.published_on) if published and xblock.published_on else None, - "studio_url": xblock_studio_url(xblock, parent_xblock), - "released_to_students": datetime.now(UTC) > xblock.start, - "release_date": release_date, - "visibility_state": visibility_state, - "has_explicit_staff_lock": xblock.fields['visible_to_staff_only'].is_set_on(xblock), - "start": xblock.fields['start'].to_json(xblock.start), - "graded": xblock.graded, - "due_date": get_default_time_display(xblock.due), - "due": xblock.fields['due'].to_json(xblock.due), - "format": xblock.format, - "course_graders": [grader.get('type') for grader in graders], - "has_changes": has_changes, - "actions": xblock_actions, - "explanatory_message": explanatory_message, - "group_access": xblock.group_access, - "user_partitions": get_user_partition_info(xblock, course=course), + 'id': unicode(xblock.location), + 'display_name': xblock.display_name_with_default, + 'category': xblock.category } - - if xblock.category == 'sequential': + if is_concise: + if child_info and len(child_info.get('children', [])) > 0: + xblock_info['child_info'] = child_info + else: xblock_info.update({ - "hide_after_due": xblock.hide_after_due, + 'edited_on': get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, + 'published': published, + 'published_on': published_on, + 'studio_url': xblock_studio_url(xblock, parent_xblock), + 'released_to_students': datetime.now(UTC) > xblock.start, + 'release_date': release_date, + 'visibility_state': visibility_state, + 'has_explicit_staff_lock': xblock.fields['visible_to_staff_only'].is_set_on(xblock), + 'start': xblock.fields['start'].to_json(xblock.start), + 'graded': xblock.graded, + 'due_date': get_default_time_display(xblock.due), + 'due': xblock.fields['due'].to_json(xblock.due), + 'format': xblock.format, + 'course_graders': [grader.get('type') for grader in graders], + 'has_changes': has_changes, + 'actions': xblock_actions, + 'explanatory_message': explanatory_message, + 'group_access': xblock.group_access, + 'user_partitions': get_user_partition_info(xblock, course=course), }) - # update xblock_info with special exam information if the feature flag is enabled - if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'): - if xblock.category == 'course': + if xblock.category == 'sequential': xblock_info.update({ - "enable_proctored_exams": xblock.enable_proctored_exams, - "create_zendesk_tickets": xblock.create_zendesk_tickets, - "enable_timed_exams": xblock.enable_timed_exams - }) - elif xblock.category == 'sequential': - xblock_info.update({ - "is_proctored_exam": xblock.is_proctored_exam, - "is_practice_exam": xblock.is_practice_exam, - "is_time_limited": xblock.is_time_limited, - "exam_review_rules": xblock.exam_review_rules, - "default_time_limit_minutes": xblock.default_time_limit_minutes, + 'hide_after_due': xblock.hide_after_due, }) - # Update with gating info - xblock_info.update(_get_gating_info(course, xblock)) - - if xblock.category == 'sequential': - # Entrance exam subsection should be hidden. in_entrance_exam is - # inherited metadata, all children will have it. - if getattr(xblock, "in_entrance_exam", False): - xblock_info["is_header_visible"] = False - - if data is not None: - xblock_info["data"] = data - if metadata is not None: - xblock_info["metadata"] = metadata - if include_ancestor_info: - xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock, course_outline) - if child_info: - xblock_info['child_info'] = child_info - if visibility_state == VisibilityState.staff_only: - xblock_info["ancestor_has_staff_lock"] = ancestor_has_staff_lock(xblock, parent_xblock) - else: - xblock_info["ancestor_has_staff_lock"] = False + # update xblock_info with special exam information if the feature flag is enabled + if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'): + if xblock.category == 'course': + xblock_info.update({ + 'enable_proctored_exams': xblock.enable_proctored_exams, + 'create_zendesk_tickets': xblock.create_zendesk_tickets, + 'enable_timed_exams': xblock.enable_timed_exams + }) + elif xblock.category == 'sequential': + xblock_info.update({ + 'is_proctored_exam': xblock.is_proctored_exam, + 'is_practice_exam': xblock.is_practice_exam, + 'is_time_limited': xblock.is_time_limited, + 'exam_review_rules': xblock.exam_review_rules, + 'default_time_limit_minutes': xblock.default_time_limit_minutes, + }) - if course_outline: - if xblock_info["has_explicit_staff_lock"]: - xblock_info["staff_only_message"] = True - elif child_info and child_info["children"]: - xblock_info["staff_only_message"] = all([child["staff_only_message"] for child in child_info["children"]]) + # Update with gating info + xblock_info.update(_get_gating_info(course, xblock)) + + if xblock.category == 'sequential': + # Entrance exam subsection should be hidden. in_entrance_exam is + # inherited metadata, all children will have it. + if getattr(xblock, 'in_entrance_exam', False): + xblock_info['is_header_visible'] = False + + if data is not None: + xblock_info['data'] = data + if metadata is not None: + xblock_info['metadata'] = metadata + if include_ancestor_info: + xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock, course_outline, include_child_info=True) + if child_info: + xblock_info['child_info'] = child_info + if visibility_state == VisibilityState.staff_only: + xblock_info['ancestor_has_staff_lock'] = ancestor_has_staff_lock(xblock, parent_xblock) else: - xblock_info["staff_only_message"] = False - + xblock_info['ancestor_has_staff_lock'] = False + + if course_outline: + if xblock_info['has_explicit_staff_lock']: + xblock_info['staff_only_message'] = True + elif child_info and child_info['children']: + xblock_info['staff_only_message'] = all( + [child['staff_only_message'] for child in child_info['children']] + ) + else: + xblock_info['staff_only_message'] = False return xblock_info @@ -1155,14 +1170,14 @@ def _compute_visibility_state(xblock, child_info, is_unit_with_changes, is_cours return VisibilityState.ready -def _create_xblock_ancestor_info(xblock, course_outline): +def _create_xblock_ancestor_info(xblock, course_outline=False, include_child_info=False, is_concise=False): """ Returns information about the ancestors of an xblock. Note that the direct parent will also return information about all of its children. """ ancestors = [] - def collect_ancestor_info(ancestor, include_child_info=False): + def collect_ancestor_info(ancestor, include_child_info=False, is_concise=False): """ Collect xblock info regarding the specified xblock and its ancestors. """ @@ -1172,16 +1187,18 @@ def collect_ancestor_info(ancestor, include_child_info=False): ancestor, include_child_info=include_child_info, course_outline=course_outline, - include_children_predicate=direct_children_only + include_children_predicate=direct_children_only, + is_concise=is_concise )) - collect_ancestor_info(get_parent_xblock(ancestor)) - collect_ancestor_info(get_parent_xblock(xblock), include_child_info=True) + collect_ancestor_info(get_parent_xblock(ancestor), is_concise=is_concise) + collect_ancestor_info(get_parent_xblock(xblock), include_child_info=include_child_info, is_concise=is_concise) return { 'ancestors': ancestors } -def _create_xblock_child_info(xblock, course_outline, graders, include_children_predicate=NEVER, user=None, course=None): # pylint: disable=line-too-long +def _create_xblock_child_info(xblock, course_outline, graders, include_children_predicate=NEVER, user=None, + course=None, is_concise=False): # pylint: disable=line-too-long """ Returns information about the children of an xblock, as well as about the primary category of xblock expected as children. @@ -1202,6 +1219,7 @@ def _create_xblock_child_info(xblock, course_outline, graders, include_children_ graders=graders, user=user, course=course, + is_concise=is_concise ) for child in xblock.get_children() ] return child_info diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index 70b3e6387f1e..8da705aad968 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -334,11 +334,16 @@ def setUp(self): parent_location=self.vertical.location, category="video", display_name="My Video" ) - def test_json_responses(self): + @ddt.data(True, False) + def test_json_responses(self, is_concise): """ Verify the JSON responses returned for the course. + + Arguments: + is_concise (Boolean) : If True, fetch concise version of course outline. """ outline_url = reverse_course_url('course_handler', self.course.id) + outline_url = outline_url + '?format=concise' if is_concise else outline_url resp = self.client.get(outline_url, HTTP_ACCEPT='application/json') json_response = json.loads(resp.content) @@ -346,8 +351,9 @@ def test_json_responses(self): self.assertEqual(json_response['category'], 'course') self.assertEqual(json_response['id'], unicode(self.course.location)) self.assertEqual(json_response['display_name'], self.course.display_name) - self.assertTrue(json_response['published']) - self.assertIsNone(json_response['visibility_state']) + if not is_concise: + self.assertTrue(json_response['published']) + self.assertIsNone(json_response['visibility_state']) # Now verify the first child children = json_response['child_info']['children'] @@ -356,24 +362,26 @@ def test_json_responses(self): self.assertEqual(first_child_response['category'], 'chapter') self.assertEqual(first_child_response['id'], unicode(self.chapter.location)) self.assertEqual(first_child_response['display_name'], 'Week 1') - self.assertTrue(json_response['published']) - self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled) + if not is_concise: + self.assertTrue(json_response['published']) + self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled) self.assertGreater(len(first_child_response['child_info']['children']), 0) # Finally, validate the entire response for consistency - self.assert_correct_json_response(json_response) + self.assert_correct_json_response(json_response, is_concise) - def assert_correct_json_response(self, json_response): + def assert_correct_json_response(self, json_response, is_concise=False): """ Asserts that the JSON response is syntactically consistent """ self.assertIsNotNone(json_response['display_name']) self.assertIsNotNone(json_response['id']) self.assertIsNotNone(json_response['category']) - self.assertTrue(json_response['published']) + if not is_concise: + self.assertTrue(json_response['published']) if json_response.get('child_info', None): for child_response in json_response['child_info']['children']: - self.assert_correct_json_response(child_response) + self.assert_correct_json_response(child_response, is_concise) def test_course_outline_initial_state(self): course_module = modulestore().get_item(self.course.location) diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 93637793b985..0df8ae36e247 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -20,7 +20,8 @@ ) from contentstore.views.item import ( - create_xblock_info, ALWAYS, VisibilityState, _xblock_type_and_display_name, add_container_page_publishing_info + create_xblock_info, _get_module_info, ALWAYS, VisibilityState, _xblock_type_and_display_name, + add_container_page_publishing_info ) from contentstore.tests.utils import CourseTestCase from student.tests.factories import UserFactory @@ -384,6 +385,59 @@ def test_get_user_partitions_and_groups(self): ]) self.assertEqual(result["group_access"], {}) + @ddt.data('ancestorInfo', '') + def test_ancestor_info(self, field_type): + """ + Test that we get correct ancestor info. + + Arguments: + field_type (string): If field_type=ancestorInfo, fetch ancestor info of the XBlock otherwise not. + """ + + # Create a parent chapter + chap1 = self.create_xblock(parent_usage_key=self.course.location, display_name='chapter1', category='chapter') + chapter_usage_key = self.response_usage_key(chap1) + + # create a sequential + seq1 = self.create_xblock(parent_usage_key=chapter_usage_key, display_name='seq1', category='sequential') + seq_usage_key = self.response_usage_key(seq1) + + # create a vertical + vert1 = self.create_xblock(parent_usage_key=seq_usage_key, display_name='vertical1', category='vertical') + vert_usage_key = self.response_usage_key(vert1) + + # create problem and an html component + problem1 = self.create_xblock(parent_usage_key=vert_usage_key, display_name='problem1', category='problem') + problem_usage_key = self.response_usage_key(problem1) + + def assert_xblock_info(xblock, xblock_info): + """ + Assert we have correct xblock info. + + Arguments: + xblock (XBlock): An XBlock item. + xblock_info (dict): A dict containing xblock information. + """ + self.assertEqual(unicode(xblock.location), xblock_info['id']) + self.assertEqual(xblock.display_name, xblock_info['display_name']) + self.assertEqual(xblock.category, xblock_info['category']) + + for usage_key in (problem_usage_key, vert_usage_key, seq_usage_key, chapter_usage_key): + xblock = self.get_item_from_modulestore(usage_key) + url = reverse_usage_url('xblock_handler', usage_key) + '?fields={field_type}'.format(field_type=field_type) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + response = json.loads(response.content) + if field_type == 'ancestorInfo': + self.assertIn('ancestors', response) + for ancestor_info in response['ancestors']: + parent_xblock = xblock.get_parent() + assert_xblock_info(parent_xblock, ancestor_info) + xblock = parent_xblock + else: + self.assertNotIn('ancestors', response) + self.assertEqual(_get_module_info(xblock), response) + @ddt.ddt class DeleteItem(ItemTest):