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):
"""
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
)
......
......@@ -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,83 +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),
"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),
'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
......@@ -1156,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.
"""
......@@ -1173,16 +1187,18 @@ def _create_xblock_ancestor_info(xblock, course_outline):
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.
......@@ -1203,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
......
......@@ -352,11 +352,16 @@ class TestCourseOutline(CourseTestCase):
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)
......@@ -364,8 +369,9 @@ class TestCourseOutline(CourseTestCase):
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']
......@@ -374,24 +380,26 @@ class TestCourseOutline(CourseTestCase):
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)
......
......@@ -20,7 +20,8 @@ from contentstore.views.component 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 student.tests.factories import UserFactory
......@@ -384,6 +385,59 @@ class GetItemTest(ItemTest):
])
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):
......
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