fix: sidebar completion for completable XBlocks with children

It is possible to create a completable XBlock with children.
An example is the Library Content Block with the
`MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW` feature toggle.
The sidebar should use the same mechanism as the `BlockCompletionTransformer`
and the `edx-completion` library. It means that we should treat:
1. An aggregator XBlock as completed only when all its children are completed.
2. A completable XBlock as completed when it is directly marked as completed
   (without checking the completion of its children).
This commit is contained in:
Agrendalath
2024-12-17 23:43:21 +01:00
committed by Farhaan Bukhsh
parent 98f4756d09
commit 64190d1e13
2 changed files with 54 additions and 1 deletions

View File

@@ -774,6 +774,45 @@ class SidebarBlocksTestViews(BaseCourseHomeTests):
assert sequence_data['complete'] == problem_complete
assert vertical_data['complete'] == problem_complete
@ddt.data(
# In the following tests, the library is treated as an aggregate block. The library completion does not matter.
(False, False, False, False), # Nothing is completed.
(True, False, False, True), # Only the problem is completed.
(False, True, False, False), # Only the library is completed.
(True, True, False, True), # Both the library and the problem are completed.
# In the following tests, the library is treated as a completable block. The problem completion does not matter.
(False, False, True, False), # Nothing is completed.
(True, False, True, False), # Only the problem is completed.
(False, True, True, True), # Only the library is completed.
(True, True, True, True), # Both the library and the problem are completed.
)
@ddt.unpack
def test_blocks_complete_with_library_content_block(
self, problem_complete, library_complete, library_complete_on_view, expected
):
"""
Test that the API checks the children completion only when the XBlock's completion mode is `AGGREGATOR`.
The completion of the `COMPLETABLE` XBlocks should not depend on the completion of their children.
"""
self.add_blocks_to_course()
library = BlockFactory.create(parent=self.vertical, category='library_content', graded=True, has_score=True)
problem = BlockFactory.create(parent=library, category='problem', graded=True, has_score=True)
CourseEnrollment.enroll(self.user, self.course.id)
self.create_completion(problem, int(problem_complete))
self.create_completion(library, int(library_complete))
with override_settings(
FEATURES={**settings.FEATURES, 'MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW': library_complete_on_view}
):
response = self.client.get(reverse('course-home:course-navigation', args=[self.course.id]))
sequence_data = response.data['blocks'][str(self.sequential.location)]
vertical_data = response.data['blocks'][str(self.vertical.location)]
assert sequence_data['complete'] == expected
assert vertical_data['complete'] == expected
def test_blocks_completion_stat(self):
"""
Test that the API returns the correct completion statistics for the blocks.

View File

@@ -521,7 +521,7 @@ class CourseNavigationBlocksView(RetrieveAPIView):
if not block:
return block
if 'children' in block:
if 'children' in block and block['type'] in self.aggregator_block_types:
block['children'] = [self.mark_complete_recursive(child) for child in block['children'] if child]
completable_children = self.get_completable_children(block)
block['complete'] = all(child['complete'] for child in completable_children)
@@ -613,6 +613,20 @@ class CourseNavigationBlocksView(RetrieveAPIView):
for block_key, completion in completions
}
@cached_property
def aggregator_block_types(self) -> set[str]:
"""
Return a set of block types that belong to XBlockCompletionMode.AGGREGATOR.
We use this information to determine if the block completion should depend on the completion of its children:
1. If the block is an aggregator, it should be marked as completed when all its children are completed.
2. If the block is completable, it should be directly marked as completed - regardless of its children.
"""
return {
block_type for (block_type, block_cls) in XBlock.load_classes()
if XBlockCompletionMode.get_mode(block_cls) == XBlockCompletionMode.AGGREGATOR
}
@cached_property
def completable_block_types(self):
"""