Commit 3f355241 by Mushtaq Ali

Get concise course outline data for move dialog box

Get ancestor info for the given xblock
- TNL-6061
parent 84ad88b7
...@@ -336,11 +336,16 @@ def _course_outline_json(request, course_module): ...@@ -336,11 +336,16 @@ def _course_outline_json(request, course_module):
""" """
Returns a JSON representation of the course module and recursively all of its children. 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( return create_xblock_info(
course_module, course_module,
include_child_info=True, include_child_info=True,
course_outline=True, course_outline=False if is_concise else True,
include_children_predicate=lambda xblock: not xblock.category == 'vertical', include_children_predicate=include_children_predicate,
is_concise=is_concise,
user=request.user user=request.user
) )
......
...@@ -98,6 +98,7 @@ def xblock_handler(request, usage_key_string): ...@@ -98,6 +98,7 @@ def xblock_handler(request, usage_key_string):
GET GET
json: returns representation of the xblock (locator id, data, and metadata). 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=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) html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view)
PUT or POST or PATCH PUT or POST or PATCH
json: if xblock locator is specified, update the xblock instance. The json payload can contain 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): ...@@ -149,6 +150,10 @@ def xblock_handler(request, usage_key_string):
if 'graderType' in fields: if 'graderType' in fields:
# right now can't combine output of this w/ output of _get_module_info, but worthy goal # 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)) 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 # TODO: pass fields to _get_module_info and only return those
with modulestore().bulk_operations(usage_key.course_key): with modulestore().bulk_operations(usage_key.course_key):
response = _get_module_info(_get_xblock(usage_key, request.user)) response = _get_module_info(_get_xblock(usage_key, request.user))
...@@ -887,7 +892,7 @@ def _get_gating_info(course, xblock): ...@@ -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, 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, 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. 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 ...@@ -897,6 +902,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
There are three optional boolean parameters: There are three optional boolean parameters:
include_ancestor_info - if true, ancestor info is added to the response 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 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. 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. 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 ...@@ -933,20 +939,22 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
graders, graders,
include_children_predicate=include_children_predicate, include_children_predicate=include_children_predicate,
user=user, user=user,
course=course course=course,
is_concise=is_concise
) )
else: else:
child_info = None child_info = None
release_date = _get_release_date(xblock, user) release_date = _get_release_date(xblock, user)
if xblock.category != 'course': if xblock.category != 'course' and not is_concise:
visibility_state = _compute_visibility_state( visibility_state = _compute_visibility_state(
xblock, child_info, is_xblock_unit and has_changes, is_self_paced(course) xblock, child_info, is_xblock_unit and has_changes, is_self_paced(course)
) )
else: else:
visibility_state = None visibility_state = None
published = modulestore().has_published_version(xblock) if not is_library_block else 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 # defining the default value 'True' for delete, duplicate, drag and add new child actions
# in xblock_actions for each xblock. # in xblock_actions for each xblock.
...@@ -970,83 +978,89 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -970,83 +978,89 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
pct_sign=_('%')) pct_sign=_('%'))
xblock_info = { xblock_info = {
"id": unicode(xblock.location), 'id': unicode(xblock.location),
"display_name": xblock.display_name_with_default, 'display_name': xblock.display_name_with_default,
"category": xblock.category, '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),
"self_paced": is_self_paced(course),
"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),
} }
if is_concise:
if xblock.category == 'sequential': if child_info and len(child_info.get('children', [])) > 0:
xblock_info['child_info'] = child_info
else:
xblock_info.update({ 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 xblock.category == 'sequential':
if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
if xblock.category == 'course':
xblock_info.update({ xblock_info.update({
"enable_proctored_exams": xblock.enable_proctored_exams, 'hide_after_due': xblock.hide_after_due,
"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,
}) })
# Update with gating info # update xblock_info with special exam information if the feature flag is enabled
xblock_info.update(_get_gating_info(course, xblock)) if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
if xblock.category == 'course':
if xblock.category == 'sequential': xblock_info.update({
# Entrance exam subsection should be hidden. in_entrance_exam is 'enable_proctored_exams': xblock.enable_proctored_exams,
# inherited metadata, all children will have it. 'create_zendesk_tickets': xblock.create_zendesk_tickets,
if getattr(xblock, "in_entrance_exam", False): 'enable_timed_exams': xblock.enable_timed_exams
xblock_info["is_header_visible"] = False })
elif xblock.category == 'sequential':
if data is not None: xblock_info.update({
xblock_info["data"] = data 'is_proctored_exam': xblock.is_proctored_exam,
if metadata is not None: 'is_practice_exam': xblock.is_practice_exam,
xblock_info["metadata"] = metadata 'is_time_limited': xblock.is_time_limited,
if include_ancestor_info: 'exam_review_rules': xblock.exam_review_rules,
xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock, course_outline) 'default_time_limit_minutes': xblock.default_time_limit_minutes,
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
if course_outline: # Update with gating info
if xblock_info["has_explicit_staff_lock"]: xblock_info.update(_get_gating_info(course, xblock))
xblock_info["staff_only_message"] = True
elif child_info and child_info["children"]: if xblock.category == 'sequential':
xblock_info["staff_only_message"] = all([child["staff_only_message"] for child in child_info["children"]]) # 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: 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 return xblock_info
...@@ -1156,14 +1170,14 @@ def _compute_visibility_state(xblock, child_info, is_unit_with_changes, is_cours ...@@ -1156,14 +1170,14 @@ def _compute_visibility_state(xblock, child_info, is_unit_with_changes, is_cours
return VisibilityState.ready 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 Returns information about the ancestors of an xblock. Note that the direct parent will also return
information about all of its children. information about all of its children.
""" """
ancestors = [] 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. Collect xblock info regarding the specified xblock and its ancestors.
""" """
...@@ -1173,16 +1187,18 @@ def _create_xblock_ancestor_info(xblock, course_outline): ...@@ -1173,16 +1187,18 @@ def _create_xblock_ancestor_info(xblock, course_outline):
ancestor, ancestor,
include_child_info=include_child_info, include_child_info=include_child_info,
course_outline=course_outline, 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(ancestor), is_concise=is_concise)
collect_ancestor_info(get_parent_xblock(xblock), include_child_info=True) collect_ancestor_info(get_parent_xblock(xblock), include_child_info=include_child_info, is_concise=is_concise)
return { return {
'ancestors': ancestors '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 Returns information about the children of an xblock, as well as about the primary category
of xblock expected as children. of xblock expected as children.
...@@ -1203,6 +1219,7 @@ def _create_xblock_child_info(xblock, course_outline, graders, include_children_ ...@@ -1203,6 +1219,7 @@ def _create_xblock_child_info(xblock, course_outline, graders, include_children_
graders=graders, graders=graders,
user=user, user=user,
course=course, course=course,
is_concise=is_concise
) for child in xblock.get_children() ) for child in xblock.get_children()
] ]
return child_info return child_info
......
...@@ -352,11 +352,16 @@ class TestCourseOutline(CourseTestCase): ...@@ -352,11 +352,16 @@ class TestCourseOutline(CourseTestCase):
parent_location=self.vertical.location, category="video", display_name="My Video" 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. 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 = 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') resp = self.client.get(outline_url, HTTP_ACCEPT='application/json')
json_response = json.loads(resp.content) json_response = json.loads(resp.content)
...@@ -364,8 +369,9 @@ class TestCourseOutline(CourseTestCase): ...@@ -364,8 +369,9 @@ class TestCourseOutline(CourseTestCase):
self.assertEqual(json_response['category'], 'course') self.assertEqual(json_response['category'], 'course')
self.assertEqual(json_response['id'], unicode(self.course.location)) self.assertEqual(json_response['id'], unicode(self.course.location))
self.assertEqual(json_response['display_name'], self.course.display_name) self.assertEqual(json_response['display_name'], self.course.display_name)
self.assertTrue(json_response['published']) if not is_concise:
self.assertIsNone(json_response['visibility_state']) self.assertTrue(json_response['published'])
self.assertIsNone(json_response['visibility_state'])
# Now verify the first child # Now verify the first child
children = json_response['child_info']['children'] children = json_response['child_info']['children']
...@@ -374,24 +380,26 @@ class TestCourseOutline(CourseTestCase): ...@@ -374,24 +380,26 @@ class TestCourseOutline(CourseTestCase):
self.assertEqual(first_child_response['category'], 'chapter') self.assertEqual(first_child_response['category'], 'chapter')
self.assertEqual(first_child_response['id'], unicode(self.chapter.location)) self.assertEqual(first_child_response['id'], unicode(self.chapter.location))
self.assertEqual(first_child_response['display_name'], 'Week 1') self.assertEqual(first_child_response['display_name'], 'Week 1')
self.assertTrue(json_response['published']) if not is_concise:
self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled) self.assertTrue(json_response['published'])
self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled)
self.assertGreater(len(first_child_response['child_info']['children']), 0) self.assertGreater(len(first_child_response['child_info']['children']), 0)
# Finally, validate the entire response for consistency # 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 Asserts that the JSON response is syntactically consistent
""" """
self.assertIsNotNone(json_response['display_name']) self.assertIsNotNone(json_response['display_name'])
self.assertIsNotNone(json_response['id']) self.assertIsNotNone(json_response['id'])
self.assertIsNotNone(json_response['category']) 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): if json_response.get('child_info', None):
for child_response in json_response['child_info']['children']: 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): def test_course_outline_initial_state(self):
course_module = modulestore().get_item(self.course.location) course_module = modulestore().get_item(self.course.location)
......
...@@ -20,7 +20,8 @@ from contentstore.views.component import ( ...@@ -20,7 +20,8 @@ from contentstore.views.component import (
) )
from contentstore.views.item import ( 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 contentstore.tests.utils import CourseTestCase
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
...@@ -384,6 +385,59 @@ class GetItemTest(ItemTest): ...@@ -384,6 +385,59 @@ class GetItemTest(ItemTest):
]) ])
self.assertEqual(result["group_access"], {}) 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 @ddt.ddt
class DeleteItem(ItemTest): class DeleteItem(ItemTest):
......
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