diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js
index 22b6738df4..c94308ac3b 100644
--- a/src/course-home/data/api.js
+++ b/src/course-home/data/api.js
@@ -137,6 +137,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
resumeBlock: block.resume_block,
sequenceIds: block.children || [],
hideFromTOC: block.hide_from_toc,
+ optionalCompletion: block.optional_completion,
};
break;
@@ -155,6 +156,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
title: block.display_name,
hideFromTOC: block.hide_from_toc,
navigationDisabled: block.navigation_disabled,
+ optionalCompletion: block.optional_completion,
};
break;
diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx
index 4e013c73cb..b36c158657 100644
--- a/src/course-home/outline-tab/Section.jsx
+++ b/src/course-home/outline-tab/Section.jsx
@@ -1,7 +1,9 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Collapsible, IconButton, Icon } from '@openedx/paragon';
+import {
+ Badge, Collapsible, IconButton, Icon,
+} from '@openedx/paragon';
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -25,6 +27,7 @@ const Section = ({
sequenceIds,
title,
hideFromTOC,
+ optionalCompletion,
} = section;
const {
courseBlocks: {
@@ -82,6 +85,11 @@ const Section = ({
)}
)}
+ {optionalCompletion && (
+
+ {intl.formatMessage(messages.optionalCompletion)}
+
+ )}
);
diff --git a/src/course-home/outline-tab/SequenceLink.jsx b/src/course-home/outline-tab/SequenceLink.jsx
index 41af780c19..9b5ff6cf97 100644
--- a/src/course-home/outline-tab/SequenceLink.jsx
+++ b/src/course-home/outline-tab/SequenceLink.jsx
@@ -11,7 +11,7 @@ import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-ico
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Icon } from '@openedx/paragon';
+import { Badge, Icon } from '@openedx/paragon';
import { Block } from '@openedx/paragon/icons';
import EffortEstimate from '../../shared/effort-estimate';
import { useModel } from '../../generic/model-store';
@@ -31,6 +31,7 @@ const SequenceLink = ({
showLink,
title,
hideFromTOC,
+ optionalCompletion,
} = sequence;
const {
userTimezone,
@@ -108,12 +109,17 @@ const SequenceLink = ({
/>
)}
-
+
{displayTitle}
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
+ {optionalCompletion && (
+
+ {intl.formatMessage(messages.optionalCompletion)}
+
+ )}
{hideFromTOC && (
diff --git a/src/course-home/outline-tab/messages.js b/src/course-home/outline-tab/messages.js
index 46ac3bd2f0..faf5d0dda4 100644
--- a/src/course-home/outline-tab/messages.js
+++ b/src/course-home/outline-tab/messages.js
@@ -109,6 +109,11 @@ const messages = defineMessages({
defaultMessage: 'Open',
description: 'A button to open the given section of the course outline',
},
+ optionalCompletion: {
+ id: 'learning.outline.optionalBlock',
+ defaultMessage: 'Optional',
+ description: 'Used as a label to indicate that a section or sequence is optional.',
+ },
proctoringInfoPanel: {
id: 'learning.proctoringPanel.header',
defaultMessage: 'This course contains proctored exams',
diff --git a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx
index 54b6caa9c6..de8ce01f62 100644
--- a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx
+++ b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
import {
getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
import { useModel } from '../../../generic/model-store';
import CompleteDonutSegment from './CompleteDonutSegment';
@@ -10,18 +11,16 @@ import IncompleteDonutSegment from './IncompleteDonutSegment';
import LockedDonutSegment from './LockedDonutSegment';
import messages from './messages';
-const CompletionDonutChart = ({ intl }) => {
+const CompletionDonutChart = ({ intl, optional = false }) => {
const {
courseId,
} = useSelector(state => state.courseHome);
- const {
- completionSummary: {
- completeCount,
- incompleteCount,
- lockedCount,
- },
- } = useModel('progress', courseId);
+ const progress = useModel('progress', courseId);
+ const completionSummary = progress?.completionSummary || {};
+ const completeCount = optional ? completionSummary.optionalCompleteCount : completionSummary.completeCount;
+ const incompleteCount = optional ? completionSummary.optionalIncompleteCount : completionSummary.incompleteCount;
+ const lockedCount = optional ? completionSummary.optionalLockedCount : completionSummary.lockedCount;
const numTotalUnits = completeCount + incompleteCount + lockedCount;
const completePercentage = completeCount ? Number(((completeCount / numTotalUnits) * 100).toFixed(0)) : 0;
@@ -30,6 +29,25 @@ const CompletionDonutChart = ({ intl }) => {
const isLocaleRtl = isRtl(getLocale());
+ if (optional && numTotalUnits === 0) {
+ return <>>;
+ }
+
+ const optionalTotalUnits = (
+ completionSummary.optionalCompleteCount
+ + completionSummary.optionalIncompleteCount
+ + completionSummary.optionalLockedCount
+ );
+
+ let label;
+ if (optional) {
+ label = intl.formatMessage(messages.optionalDonutLabel);
+ } else if (optionalTotalUnits > 0) {
+ label = intl.formatMessage(messages.requiredDonutLabel);
+ } else {
+ label = intl.formatMessage(messages.donutLabel);
+ }
+
return (
<>