Merge pull request #16674 from open-craft/tomaszgy/completion_in_blocks_api
Add block completion value as optional field in course_blocks.api.
This commit is contained in:
@@ -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'
|
||||
)
|
||||
]
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
1
setup.py
1
setup.py
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user