diff --git a/lms/djangoapps/course_api/blocks/tests/test_views.py b/lms/djangoapps/course_api/blocks/tests/test_views.py index ccdf3a0c9c..f3667a20e2 100644 --- a/lms/djangoapps/course_api/blocks/tests/test_views.py +++ b/lms/djangoapps/course_api/blocks/tests/test_views.py @@ -11,6 +11,7 @@ from urllib.parse import urlencode, urlunparse from django.conf import settings from django.urls import reverse from opaque_keys.edx.locator import CourseLocator +from completion.test_utils import CompletionWaffleTestMixin, submit_completions_for_testing from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory @@ -395,7 +396,7 @@ class TestBlocksView(SharedModuleStoreTestCase): self.verify_response_with_requested_fields(response) -class TestBlocksInCourseView(TestBlocksView): # pylint: disable=test-inherits-tests +class TestBlocksInCourseView(TestBlocksView, CompletionWaffleTestMixin): # pylint: disable=test-inherits-tests """ Test class for BlocksInCourseView """ @@ -404,6 +405,13 @@ class TestBlocksInCourseView(TestBlocksView): # pylint: disable=test-inherits-t super(TestBlocksInCourseView, self).setUp() self.url = reverse('blocks_in_course') self.query_params['course_id'] = str(self.course_key) + self.override_waffle_switch(True) + self.non_orphaned_raw_block_usage_keys = set( + item.location + for item in self.store.get_items(self.course_key) + # remove all orphaned items in the course, except for the root 'course' block + if self.store.get_parent_location(item.location) or item.category == 'course' + ) def test_no_course_id(self): self.query_params.pop('course_id') @@ -419,3 +427,42 @@ class TestBlocksInCourseView(TestBlocksView): # pylint: disable=test-inherits-t self.client.logout() self.query_params['username'] = '' self.verify_response(403, params={'course_id': str(CourseLocator('non', 'existent', 'course'))}) + + def test_completion_one_unit(self): + for item in self.store.get_items(self.course_key): + if item.category == 'html': + block_usage_key = item.location + break + + submit_completions_for_testing(self.user, [block_usage_key]) + response = self.verify_response(params={ + 'depth': 'all', + 'requested_fields': ['completion', 'children'], + }) + + completion = response.data['blocks'][str(block_usage_key)].get('completion') + self.assertTrue(completion) + + def test_completion_all_course(self): + for block in self.non_orphaned_raw_block_usage_keys: + submit_completions_for_testing(self.user, [block]) + + response = self.verify_response(params={ + 'depth': 'all', + 'requested_fields': ['completion', 'children'], + }) + for block_id in self.non_orphaned_block_usage_keys: + self.assertTrue(response.data['blocks'][block_id].get('completion')) + + def test_completion_all_course_with_list_return_type(self): + for block in self.non_orphaned_raw_block_usage_keys: + submit_completions_for_testing(self.user, [block]) + + response = self.verify_response(params={ + 'depth': 'all', + 'return_type': 'list', + 'requested_fields': ['completion', 'children'], + }) + for block in response.data: + if block['block_id'] in self.non_orphaned_block_usage_keys: + self.assertTrue(block.get('completion')) diff --git a/lms/djangoapps/course_api/blocks/views.py b/lms/djangoapps/course_api/blocks/views.py index 6f2421fcde..34e054a2e9 100644 --- a/lms/djangoapps/course_api/blocks/views.py +++ b/lms/djangoapps/course_api/blocks/views.py @@ -302,4 +302,54 @@ class BlocksInCourseView(BlocksView): course_usage_key = modulestore().make_course_usage_key(course_key) except InvalidKeyError: raise ValidationError(u"'{}' is not a valid course key.".format(six.text_type(course_key_string))) - return super(BlocksInCourseView, self).list(request, course_usage_key, hide_access_denials=hide_access_denials) + response = super().list(request, course_usage_key, + hide_access_denials=hide_access_denials) + + if 'completion' not in request.query_params.getlist('requested_fields', ''): + return response + + course_blocks = {} + root = None + if request.query_params.get('return_type') == 'list': + for course_block in response.data: + course_blocks[course_block['id']] = course_block + + if course_block.get('type') == 'course': + root = course_block['id'] + else: + root = response.data['root'] + course_blocks = response.data['blocks'] + + if not root: + raise ValueError("Unable to find course block in {}".format(course_key_string)) + + recurse_mark_complete(root, course_blocks) + return response + + +def recurse_mark_complete(block_id, blocks): + """ + Helper function to walk course tree dict, + marking completion as 1 or 0 + + If all blocks are complete, mark parent block complete + + :param blocks: dict of all blocks + :param block_id: root or child block id + + :return: + block: course_outline_root_block block object or child block + """ + block = blocks.get(block_id, {}) + if block.get('completion') == 1: + return + + child_blocks = block.get('children', block.get('descendents')) + # Unit blocks(blocks with no children) completion is being marked by patch call to completion service. + if child_blocks: + for child_block in child_blocks: + recurse_mark_complete(child_block, blocks) + + completable_blocks = [blocks[child_block_id] for child_block_id in child_blocks + if blocks[child_block_id].get('type') != 'discussion'] + block['completion'] = int(all(child.get('completion') == 1 for child in completable_blocks))