Skip to content

Commit 2d41f44

Browse files
committed
feat: implement optional completion
This adds a new XBlock field that allows marking sections, subsections, and units as optional, which means that their completion is counted separately.
1 parent 4574c48 commit 2d41f44

File tree

6 files changed

+42
-14
lines changed

6 files changed

+42
-14
lines changed

lms/djangoapps/course_api/blocks/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def __init__(
8585
SupportedFieldType(BlockCompletionTransformer.COMPLETION, BlockCompletionTransformer),
8686
SupportedFieldType(BlockCompletionTransformer.COMPLETE),
8787
SupportedFieldType(BlockCompletionTransformer.RESUME_BLOCK),
88+
SupportedFieldType(BlockCompletionTransformer.OPTIONAL_COMPLETION),
8889
SupportedFieldType(DiscussionsTopicLinkTransformer.EXTERNAL_ID),
8990
SupportedFieldType(DiscussionsTopicLinkTransformer.EMBED_URL),
9091

lms/djangoapps/course_api/blocks/transformers/block_completion.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class BlockCompletionTransformer(BlockStructureTransformer):
1818
COMPLETION = 'completion'
1919
COMPLETE = 'complete'
2020
RESUME_BLOCK = 'resume_block'
21+
OPTIONAL_COMPLETION = 'optional_completion'
2122

2223
@classmethod
2324
def name(cls):
@@ -43,7 +44,7 @@ def get_block_completion(cls, block_structure, block_key):
4344

4445
@classmethod
4546
def collect(cls, block_structure):
46-
block_structure.request_xblock_fields('completion_mode')
47+
block_structure.request_xblock_fields('completion_mode', cls.OPTIONAL_COMPLETION)
4748

4849
@staticmethod
4950
def _is_block_excluded(block_structure, block_key):

lms/djangoapps/course_home_api/outline/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def get_blocks(self, block): # pylint: disable=missing-function-docstring
5050
block_key: {
5151
'children': [child['id'] for child in children],
5252
'complete': block.get('complete', False),
53+
'optional_completion': block.get('optional_completion', False),
5354
'description': description,
5455
'display_name': display_name,
5556
'due': block.get('due'),

lms/djangoapps/courseware/courses.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -552,38 +552,51 @@ def get_course_assignment_date_blocks(course, user, request, num_return=None,
552552

553553

554554
@request_cached()
555-
def get_course_blocks_completion_summary(course_key, user):
555+
def get_course_blocks_completion_summary(course_key, user) -> dict[str, int]:
556556
"""
557-
Returns an object with the number of complete units, incomplete units, and units that contain gated content
557+
Returns a dict with the number of complete units, incomplete units, and units that contain gated content
558558
for the given course. The complete and incomplete counts only reflect units that are able to be completed by
559559
the given user. If a unit contains gated content, it is not counted towards the incomplete count.
560560
561-
The object contains fields: complete_count, incomplete_count, locked_count
561+
The dict contains fields:
562+
- complete_count
563+
- incomplete_count
564+
- locked_count
565+
- optional_complete_count
566+
- optional_incomplete_count
567+
- optional_locked_count
562568
"""
563569
if not user.id:
564-
return []
570+
return {}
571+
565572
store = modulestore()
566573
course_usage_key = store.make_course_usage_key(course_key)
567574
block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True)
568575

569-
complete_count, incomplete_count, locked_count = 0, 0, 0
576+
counts = {
577+
'complete_count': 0,
578+
'incomplete_count': 0,
579+
'locked_count': 0,
580+
'optional_complete_count': 0,
581+
'optional_incomplete_count': 0,
582+
'optional_locked_count': 0,
583+
}
570584
for section_key in block_data.get_children(course_usage_key): # pylint: disable=too-many-nested-blocks
571585
for subsection_key in block_data.get_children(section_key):
572586
for unit_key in block_data.get_children(subsection_key):
573587
complete = block_data.get_xblock_field(unit_key, 'complete', False)
574588
contains_gated_content = block_data.get_xblock_field(unit_key, 'contains_gated_content', False)
589+
optional = block_data.get_xblock_field(unit_key, 'optional_completion', False)
590+
prefix = "optional_" if optional else ""
591+
575592
if contains_gated_content:
576-
locked_count += 1
593+
counts[f"{prefix}locked_count"] += 1
577594
elif complete:
578-
complete_count += 1
595+
counts[f"{prefix}complete_count"] += 1
579596
else:
580-
incomplete_count += 1
597+
counts[f"{prefix}incomplete_count"] += 1
581598

582-
return {
583-
'complete_count': complete_count,
584-
'incomplete_count': incomplete_count,
585-
'locked_count': locked_count
586-
}
599+
return counts
587600

588601

589602
@request_cached()

openedx/features/course_experience/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def recurse_mark_auth_denial(block):
114114
'weight',
115115
'completion',
116116
'complete',
117+
'optional_completion',
117118
'resume_block',
118119
'hide_from_toc',
119120
'icon_class',

xmodule/modulestore/inheritance.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,17 @@ class InheritanceMixin(XBlockMixin):
247247
scope=Scope.settings
248248
)
249249

250+
optional_completion = Boolean(
251+
display_name=_("Optional"),
252+
help=_(
253+
"Set this to true to mark this block as optional. "
254+
"Progress in this block won't count towards course completion progress "
255+
"and will count as optional progress instead."
256+
),
257+
default=False,
258+
scope=Scope.settings,
259+
)
260+
250261
@property
251262
def close_date(self):
252263
"""

0 commit comments

Comments
 (0)