Get concise course outline data for move dialog box
Get ancestor info for the given xblock - TNL-6061
This commit is contained in:
@@ -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))
|
||||
# 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 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
|
||||
# Update with gating info
|
||||
xblock_info.update(_get_gating_info(course, xblock))
|
||||
|
||||
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
|
||||
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 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"]])
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user