Unverified Commit c1d02529 by Nimisha Asthagiri Committed by GitHub

Merge pull request #16674 from open-craft/tomaszgy/completion_in_blocks_api

Add block completion value as optional field in course_blocks.api.
parents 931fd8a5 d4edf33f
......@@ -8,6 +8,7 @@ from openedx.core.djangoapps.content.block_structure.transformers import BlockSt
from .serializers import BlockDictSerializer, BlockSerializer
from .transformers.blocks_api import BlocksAPITransformer
from .transformers.block_completion import BlockCompletionTransformer
from .transformers.milestones import MilestonesAndSpecialExamsTransformer
......@@ -51,9 +52,11 @@ def get_blocks(
"""
# create ordered list of transformers, adding BlocksAPITransformer at end.
transformers = BlockStructureTransformers()
include_special_exams = False
if requested_fields is not None and 'special_exam_info' in requested_fields:
include_special_exams = True
if requested_fields is None:
requested_fields = []
include_completion = 'completion' in requested_fields
include_special_exams = 'special_exam_info' in requested_fields
if user is not None:
transformers += COURSE_BLOCK_ACCESS_TRANSFORMERS
transformers += [MilestonesAndSpecialExamsTransformer(include_special_exams), HiddenContentTransformer()]
......@@ -66,6 +69,9 @@ def get_blocks(
)
]
if include_completion:
transformers += [BlockCompletionTransformer()]
# transform
blocks = get_course_blocks(user, usage_key, transformers)
......
......@@ -4,6 +4,7 @@ Course API Block Transformers
from lms.djangoapps.course_blocks.transformers.visibility import VisibilityTransformer
from .student_view import StudentViewTransformer
from .block_completion import BlockCompletionTransformer
from .block_counts import BlockCountsTransformer
from .navigation import BlockNavigationTransformer
from .milestones import MilestonesAndSpecialExamsTransformer
......@@ -63,5 +64,10 @@ SUPPORTED_FIELDS = [
'merged_visible_to_staff_only',
VisibilityTransformer,
requested_field_name='visible_to_staff_only',
),
SupportedFieldType(
BlockCompletionTransformer.COMPLETION,
BlockCompletionTransformer,
'completion'
)
]
"""
Block Completion Transformer
"""
from xblock.completable import XBlockCompletionMode as CompletionMode
from lms.djangoapps.completion.models import BlockCompletion
from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer
class BlockCompletionTransformer(BlockStructureTransformer):
"""
Keep track of the completion of each block within the block structure.
"""
READ_VERSION = 0
WRITE_VERSION = 1
COMPLETION = 'completion'
def __init__(self):
super(BlockCompletionTransformer, self).__init__()
@classmethod
def name(cls):
return "blocks_api:completion"
@classmethod
def get_block_completion(cls, block_structure, block_key):
"""
Return the precalculated completion of a block within the block_structure:
Arguments:
block_structure: a BlockStructure instance
block_key: the key of the block whose completion we want to know
Returns:
block_completion: float or None
"""
return block_structure.get_transformer_block_field(
block_key,
cls,
cls.COMPLETION,
)
@classmethod
def collect(cls, block_structure):
block_structure.request_xblock_fields('completion_mode')
def transform(self, usage_info, block_structure):
"""
Mutates block_structure adding extra field which contains block's completion.
"""
def _is_block_an_aggregator_or_excluded(block_key):
"""
Checks whether block's completion method
is of `AGGREGATOR` or `EXCLUDED` type.
"""
completion_mode = block_structure.get_xblock_field(
block_key, 'completion_mode'
)
return completion_mode in (CompletionMode.AGGREGATOR, CompletionMode.EXCLUDED)
completions = BlockCompletion.objects.filter(
user=usage_info.user,
course_key=usage_info.course_key,
).values_list(
'block_key',
'completion',
)
completions_dict = {
block.map_into_course(usage_info.course_key): completion
for block, completion in completions
}
for block_key in block_structure.topological_traversal():
if _is_block_an_aggregator_or_excluded(block_key):
completion_value = None
elif block_key in completions_dict:
completion_value = completions_dict[block_key]
else:
completion_value = 0.0
block_structure.set_transformer_block_field(
block_key, self, self.COMPLETION, completion_value
)
"""
Tests for BlockCompletionTransformer.
"""
from xblock.core import XBlock
from xblock.completable import CompletableXBlockMixin, XBlockCompletionMode
from lms.djangoapps.completion.models import BlockCompletion
from lms.djangoapps.completion.test_utils import CompletionWaffleTestMixin
from lms.djangoapps.course_api.blocks.transformers.block_completion import BlockCompletionTransformer
from lms.djangoapps.course_blocks.transformers.tests.helpers import ModuleStoreTestCase, TransformerRegistryTestMixin
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ...api import get_course_blocks
class StubAggregatorXBlock(XBlock):
"""
XBlock to test behaviour of BlockCompletionTransformer
when transforming aggregator XBlock.
"""
completion_mode = XBlockCompletionMode.AGGREGATOR
class StubExcludedXBlock(XBlock):
"""
XBlock to test behaviour of BlockCompletionTransformer
when transforming excluded XBlock.
"""
completion_mode = XBlockCompletionMode.EXCLUDED
class StubCompletableXBlock(XBlock, CompletableXBlockMixin):
"""
XBlock to test behaviour of BlockCompletionTransformer
when transforming completable XBlock.
"""
pass
class BlockCompletionTransformerTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase, CompletionWaffleTestMixin):
"""
Tests behaviour of BlockCompletionTransformer
"""
TRANSFORMER_CLASS_TO_TEST = BlockCompletionTransformer
COMPLETION_TEST_VALUE = 0.4
def setUp(self):
super(BlockCompletionTransformerTestCase, self).setUp()
self.user = UserFactory.create(password='test')
self.override_waffle_switch(True)
@XBlock.register_temp_plugin(StubAggregatorXBlock, identifier='aggregator')
def test_transform_gives_none_for_aggregator(self):
course = CourseFactory.create()
block = ItemFactory.create(category='aggregator', parent=course)
block_structure = get_course_blocks(
self.user, course.location, self.transformers
)
self._assert_block_has_proper_completion_value(
block_structure, block.location, None
)
@XBlock.register_temp_plugin(StubExcludedXBlock, identifier='excluded')
def test_transform_gives_none_for_excluded(self):
course = CourseFactory.create()
block = ItemFactory.create(category='excluded', parent=course)
block_structure = get_course_blocks(
self.user, course.location, self.transformers
)
self._assert_block_has_proper_completion_value(
block_structure, block.location, None
)
@XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp')
def test_transform_gives_value_for_completable(self):
course = CourseFactory.create()
block = ItemFactory.create(category='comp', parent=course)
BlockCompletion.objects.submit_completion(
user=self.user,
course_key=block.location.course_key,
block_key=block.location,
completion=self.COMPLETION_TEST_VALUE,
)
block_structure = get_course_blocks(
self.user, course.location, self.transformers
)
self._assert_block_has_proper_completion_value(
block_structure, block.location, self.COMPLETION_TEST_VALUE
)
def test_transform_gives_zero_for_ordinary_block(self):
course = CourseFactory.create()
block = ItemFactory.create(category='html', parent=course)
block_structure = get_course_blocks(
self.user, course.location, self.transformers
)
self._assert_block_has_proper_completion_value(
block_structure, block.location, 0.0
)
def _assert_block_has_proper_completion_value(
self, block_structure, block_key, expected_value
):
"""
Checks whether block's completion has expected value.
"""
block_data = block_structure.get_transformer_block_data(
block_key, self.TRANSFORMER_CLASS_TO_TEST
)
completion_value = block_data.fields['completion']
self.assertEqual(completion_value, expected_value)
......@@ -128,6 +128,11 @@ class BlocksView(DeveloperErrorViewMixin, ListAPIView):
the child blocks. Returned only if "children" is included in the
"requested_fields" parameter.
* completion: (float or None) The level of completion of the block.
Its value can vary between 0.0 and 1.0 or be equal to None
if block is not completable. Returned only if "completion"
is included in the "requested_fields" parameter.
* block_counts: (dict) For each block type specified in the
block_counts parameter to the endpoint, the aggregate number of
blocks of that type for this block and all of its descendants.
......
......@@ -730,7 +730,7 @@ class BlockStructureBlockData(BlockStructure):
Adds the given transformer to the block structure by recording
its current version number.
"""
if transformer.READ_VERSION == 0 or transformer.WRITE_VERSION == 0:
if transformer.WRITE_VERSION == 0:
raise TransformerException('Version attributes are not set on transformer {0}.', transformer.name())
self.set_transformer_data(transformer, TRANSFORMER_VERSION_KEY, transformer.WRITE_VERSION)
......
......@@ -58,6 +58,7 @@ setup(
"course_blocks_api = lms.djangoapps.course_api.blocks.transformers.blocks_api:BlocksAPITransformer",
"milestones = lms.djangoapps.course_api.blocks.transformers.milestones:MilestonesAndSpecialExamsTransformer",
"grades = lms.djangoapps.grades.transformer:GradesTransformer",
"completion = lms.djangoapps.course_api.blocks.transformers.block_completion:BlockCompletionTransformer"
],
"openedx.ace.policy": [
"bulk_email_optout = lms.djangoapps.bulk_email.policies:CourseEmailOptout"
......
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