We discovered a subsection that contained a unit without any content inside, but because of our logic requiring children, it would never be marked complete (meaning the subsection, section, and course could thus never be marked complete). This fixes that by removing the children check from setting completion, but first gating that code path on the xblock being an aggregator (to prevent leaves from marking as true simply because there are no children). Test fixes include adding a test for the empty aggregator case as well as some changes to not have an entire course marked complete because they are all empty aggregators.
151 lines
6.5 KiB
Python
151 lines
6.5 KiB
Python
"""
|
|
API function for retrieving course blocks data
|
|
"""
|
|
|
|
|
|
import lms.djangoapps.course_blocks.api as course_blocks_api
|
|
from lms.djangoapps.course_blocks.transformers.access_denied_filter import AccessDeniedMessageFilterTransformer
|
|
from lms.djangoapps.course_blocks.transformers.hidden_content import HiddenContentTransformer
|
|
from lms.djangoapps.course_blocks.transformers.hide_empty import HideEmptyTransformer
|
|
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
|
|
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
|
|
from openedx.features.effort_estimation.api import EffortEstimationTransformer
|
|
|
|
from .serializers import BlockDictSerializer, BlockSerializer
|
|
from .toggles import HIDE_ACCESS_DENIALS_FLAG
|
|
from .transformers.blocks_api import BlocksAPITransformer
|
|
from .transformers.milestones import MilestonesAndSpecialExamsTransformer
|
|
|
|
|
|
def get_blocks(
|
|
request,
|
|
usage_key,
|
|
user=None,
|
|
depth=None,
|
|
nav_depth=None,
|
|
requested_fields=None,
|
|
block_counts=None,
|
|
student_view_data=None,
|
|
return_type='dict',
|
|
block_types_filter=None,
|
|
hide_access_denials=False,
|
|
allow_start_dates_in_future=False,
|
|
):
|
|
"""
|
|
Return a serialized representation of the course blocks.
|
|
|
|
Arguments:
|
|
request (HTTPRequest): Used for calling django reverse.
|
|
usage_key (UsageKey): Identifies the starting block of interest.
|
|
user (User): Optional user object for whom the blocks are being
|
|
retrieved. If None, blocks are returned regardless of access checks.
|
|
depth (integer or None): Identifies the depth of the tree to return
|
|
starting at the root block. If None, the entire tree starting at
|
|
the root is returned.
|
|
nav_depth (integer): Optional parameter that indicates how far deep to
|
|
traverse into the block hierarchy before bundling all the
|
|
descendants for navigation.
|
|
requested_fields (list): Optional list of names of additional fields
|
|
to return for each block. Supported fields are listed in
|
|
transformers.SUPPORTED_FIELDS.
|
|
block_counts (list): Optional list of names of block types for which to
|
|
return an aggregate count of blocks.
|
|
student_view_data (list): Optional list of names of block types for
|
|
which blocks to return their student_view_data.
|
|
return_type (string): Possible values are 'dict' or 'list'. Indicates
|
|
the format for returning the blocks.
|
|
block_types_filter (list): Optional list of block type names used to filter
|
|
the final result of returned blocks.
|
|
hide_access_denials (bool): When True, filter out any blocks that were
|
|
denied access to the user, even if they have access denial messages
|
|
attached.
|
|
allow_start_dates_in_future (bool): When True, will allow blocks to be
|
|
returned that can bypass the StartDateTransformer's filter to show
|
|
blocks with start dates in the future.
|
|
"""
|
|
|
|
if HIDE_ACCESS_DENIALS_FLAG.is_enabled():
|
|
hide_access_denials = True
|
|
|
|
# create ordered list of transformers, adding BlocksAPITransformer at end.
|
|
transformers = BlockStructureTransformers()
|
|
if requested_fields is None:
|
|
requested_fields = []
|
|
include_completion = 'completion' in requested_fields
|
|
include_effort_estimation = (EffortEstimationTransformer.EFFORT_TIME in requested_fields or
|
|
EffortEstimationTransformer.EFFORT_ACTIVITIES in requested_fields)
|
|
include_gated_sections = 'show_gated_sections' in requested_fields
|
|
include_has_scheduled_content = 'has_scheduled_content' in requested_fields
|
|
include_special_exams = 'special_exam_info' in requested_fields
|
|
|
|
if user is not None:
|
|
transformers += course_blocks_api.get_course_block_access_transformers(user)
|
|
transformers += [
|
|
MilestonesAndSpecialExamsTransformer(
|
|
include_special_exams=include_special_exams,
|
|
include_gated_sections=include_gated_sections
|
|
),
|
|
HiddenContentTransformer()
|
|
]
|
|
|
|
# Note: A change to the BlockCompletionTransformer (https://github.com/edx/edx-platform/pull/27622/)
|
|
# will be introducing a bug if hide_access_denials is True. I'm accepting this risk because in
|
|
# the AccessDeniedMessageFilterTransformer, there is note about deleting it and I believe it is
|
|
# technically deprecated functionality. The only use case where hide_access_denials is True
|
|
# (outside of explicitly setting the temporary waffle flag) is in lms/djangoapps/course_api/blocks/urls.py
|
|
# for a v1 api that I also believe should have been deprecated and removed. When this code is removed,
|
|
# please also remove this comment. Thanks!
|
|
if hide_access_denials:
|
|
transformers += [AccessDeniedMessageFilterTransformer()]
|
|
|
|
# TODO: Remove this after REVE-52 lands and old-mobile-app traffic falls to < 5% of mobile traffic
|
|
if is_request_from_mobile_app(request):
|
|
transformers += [HideEmptyTransformer()]
|
|
|
|
if include_effort_estimation:
|
|
transformers += [EffortEstimationTransformer()]
|
|
|
|
transformers += [
|
|
BlocksAPITransformer(
|
|
block_counts,
|
|
student_view_data,
|
|
depth,
|
|
nav_depth
|
|
)
|
|
]
|
|
|
|
# transform
|
|
blocks = course_blocks_api.get_course_blocks(
|
|
user,
|
|
usage_key,
|
|
transformers,
|
|
allow_start_dates_in_future=allow_start_dates_in_future,
|
|
include_completion=include_completion,
|
|
include_has_scheduled_content=include_has_scheduled_content
|
|
)
|
|
|
|
# filter blocks by types
|
|
if block_types_filter:
|
|
block_keys_to_remove = []
|
|
for block_key in blocks:
|
|
block_type = blocks.get_xblock_field(block_key, 'category')
|
|
if block_type not in block_types_filter:
|
|
block_keys_to_remove.append(block_key)
|
|
for block_key in block_keys_to_remove:
|
|
blocks.remove_block(block_key, keep_descendants=True)
|
|
|
|
# serialize
|
|
serializer_context = {
|
|
'request': request,
|
|
'block_structure': blocks,
|
|
'requested_fields': requested_fields or [],
|
|
}
|
|
|
|
if return_type == 'dict':
|
|
serializer = BlockDictSerializer(blocks, context=serializer_context, many=False)
|
|
else:
|
|
serializer = BlockSerializer(blocks, context=serializer_context, many=True)
|
|
|
|
# return serialized data
|
|
return serializer.data
|