Files
Agrendalath bbc0cc2baa fix: show correct icons in the sidebar for units with custom XBlocks
Currently, the sidebar relies only on the XBlock's `category` class attribute
(called `type` in the transformers). This behavior is inconsistent with the
legacy subsection navigation, which relies on the `XModuleMixin.get_icon_class`
method. This commit adds the `icon_class` to the fields collected by the
transformers and uses it to determine whether the "problem" or "video" icon
should be displayed for a unit in the sidebar.
2025-08-21 00:29:01 +05:30

230 lines
8.8 KiB
Python

"""
Serializers for Course Blocks related return objects.
"""
from django.conf import settings
from rest_framework import serializers
from rest_framework.reverse import reverse
from lms.djangoapps.course_blocks.transformers.hidden_content import HiddenContentTransformer
from lms.djangoapps.course_blocks.transformers.visibility import VisibilityTransformer
from openedx.core.djangoapps.discussions.transformers import DiscussionsTopicLinkTransformer
from .transformers.block_completion import BlockCompletionTransformer
from .transformers.block_counts import BlockCountsTransformer
from .transformers.extra_fields import ExtraFieldsTransformer
from .transformers.milestones import MilestonesAndSpecialExamsTransformer
from .transformers.navigation import BlockNavigationTransformer
from .transformers.student_view import StudentViewTransformer
class SupportedFieldType:
"""
Metadata about fields supported by different transformers
"""
def __init__(
self,
block_field_name,
transformer=None,
requested_field_name=None,
serializer_field_name=None,
default_value=None
):
self.transformer = transformer
self.block_field_name = block_field_name
self.requested_field_name = requested_field_name or block_field_name
self.serializer_field_name = serializer_field_name or self.requested_field_name
self.default_value = default_value
# A list of metadata for additional requested fields to be used by the
# BlockSerializer` class. Each entry provides information on how that field can
# be requested (`requested_field_name`), can be found (`transformer` and
# `block_field_name`), and should be serialized (`serializer_field_name` and
# `default_value`).
SUPPORTED_FIELDS = [
SupportedFieldType('category', requested_field_name='type'),
SupportedFieldType('display_name', default_value=''),
SupportedFieldType('effort_activities'),
SupportedFieldType('effort_time'),
SupportedFieldType('graded'),
SupportedFieldType('format'),
SupportedFieldType('start'),
SupportedFieldType('due'),
SupportedFieldType('contains_gated_content'),
SupportedFieldType('has_score'),
SupportedFieldType('has_scheduled_content'),
SupportedFieldType('weight'),
SupportedFieldType('show_correctness'),
SupportedFieldType('hide_from_toc'),
SupportedFieldType('icon_class'),
# 'student_view_data'
SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_DATA, StudentViewTransformer),
# 'student_view_multi_device'
SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_MULTI_DEVICE, StudentViewTransformer),
SupportedFieldType('special_exam_info', MilestonesAndSpecialExamsTransformer),
# set the block_field_name to None so the entire data for the transformer is serialized
SupportedFieldType(None, BlockCountsTransformer, BlockCountsTransformer.BLOCK_COUNTS),
SupportedFieldType(
BlockNavigationTransformer.BLOCK_NAVIGATION,
BlockNavigationTransformer,
requested_field_name='nav_depth',
serializer_field_name='descendants',
),
# Provide the staff visibility info stored when VisibilityTransformer ran previously
SupportedFieldType(
'merged_visible_to_staff_only',
VisibilityTransformer,
requested_field_name='visible_to_staff_only',
),
SupportedFieldType(
'merged_hide_after_due',
HiddenContentTransformer,
requested_field_name='hide_after_due'
),
SupportedFieldType(BlockCompletionTransformer.COMPLETION, BlockCompletionTransformer),
SupportedFieldType(BlockCompletionTransformer.COMPLETE),
SupportedFieldType(BlockCompletionTransformer.RESUME_BLOCK),
SupportedFieldType(DiscussionsTopicLinkTransformer.EXTERNAL_ID),
SupportedFieldType(DiscussionsTopicLinkTransformer.EMBED_URL),
*[SupportedFieldType(field_name) for field_name in ExtraFieldsTransformer.get_requested_extra_fields()],
]
# This lists the names of all fields that are allowed
# to be show to users who do not have access to a particular piece
# of content
FIELDS_ALLOWED_IN_AUTH_DENIED_CONTENT = [
"display_name",
"block_id",
"student_view_url",
"student_view_multi_device",
"lms_web_url",
"legacy_web_url",
"type",
"id",
"block_counts",
"graded",
"descendants",
"authorization_denial_reason",
"authorization_denial_message",
'contains_gated_content',
]
class BlockSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for single course block
"""
def _get_field(self, block_key, transformer, field_name, default):
"""
Get the field value requested. The field may be an XBlock field, a
transformer block field, or an entire tranformer block data dict.
"""
value = None
if transformer is None:
value = self.context['block_structure'].get_xblock_field(block_key, field_name)
elif field_name is None:
try:
value = self.context['block_structure'].get_transformer_block_data(block_key, transformer).fields
except KeyError:
pass
else:
value = self.context['block_structure'].get_transformer_block_field(block_key, transformer, field_name)
return value if (value is not None) else default
def to_representation(self, block_key): # lint-amnesty, pylint: disable=arguments-differ
"""
Return a serializable representation of the requested block
"""
# create response data dict for basic fields
block_structure = self.context['block_structure']
authorization_denial_reason = block_structure.get_xblock_field(block_key, 'authorization_denial_reason')
authorization_denial_message = block_structure.get_xblock_field(block_key, 'authorization_denial_message')
jump_to_courseware_url = reverse(
'jump_to',
kwargs={
'course_id': str(block_key.course_key),
'location': str(block_key),
},
request=self.context['request'],
)
data = {
'id': str(block_key),
'block_id': str(block_key.block_id),
'lms_web_url': jump_to_courseware_url,
'legacy_web_url': jump_to_courseware_url + '?experience=legacy',
'student_view_url': reverse(
'render_xblock',
kwargs={'usage_key_string': str(block_key)},
request=self.context['request'],
),
}
if settings.FEATURES.get("ENABLE_LTI_PROVIDER") and 'lti_url' in self.context['requested_fields']:
data['lti_url'] = reverse(
'lti_provider_launch',
kwargs={'course_id': str(block_key.course_key), 'usage_id': str(block_key)},
request=self.context['request'],
)
# add additional requested fields that are supported by the various transformers
for supported_field in SUPPORTED_FIELDS:
if supported_field.requested_field_name in self.context['requested_fields']:
field_value = self._get_field(
block_key,
supported_field.transformer,
supported_field.block_field_name,
supported_field.default_value,
)
if field_value is not None:
# only return fields that have data
data[supported_field.serializer_field_name] = field_value
if 'children' in self.context['requested_fields']:
children = block_structure.get_children(block_key)
if children:
data['children'] = [str(child) for child in children]
if authorization_denial_reason and authorization_denial_message:
data['authorization_denial_reason'] = authorization_denial_reason
data['authorization_denial_message'] = authorization_denial_message
cleaned_data = data.copy()
for field in data.keys(): # pylint: disable=consider-iterating-dictionary
if field not in FIELDS_ALLOWED_IN_AUTH_DENIED_CONTENT:
del cleaned_data[field]
data = cleaned_data
return data
class BlockDictSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer that formats a BlockStructure object to a dictionary, rather
than a list, of blocks
"""
root = serializers.CharField(source='root_block_usage_key')
blocks = serializers.SerializerMethodField()
def get_blocks(self, structure):
"""
Serialize to a dictionary of blocks keyed by the block's usage_key.
"""
return {
str(block_key): BlockSerializer(block_key, context=self.context).data
for block_key in structure
}