diff --git a/src/course-home/data/__factories__/progressTabData.factory.js b/src/course-home/data/__factories__/progressTabData.factory.js
index 1ff83241ce..839837e373 100644
--- a/src/course-home/data/__factories__/progressTabData.factory.js
+++ b/src/course-home/data/__factories__/progressTabData.factory.js
@@ -12,6 +12,11 @@ Factory.define('progressTabData')
incomplete_count: 1,
locked_count: 0,
},
+ optional_completion_summary: {
+ complete_count: 1,
+ incomplete_count: 1,
+ locked_count: 0,
+ },
course_grade: {
letter_grade: 'pass',
percent: 1,
diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap
index 4644d7b142..3f8b64924f 100644
--- a/src/course-home/data/__snapshots__/redux.test.js.snap
+++ b/src/course-home/data/__snapshots__/redux.test.js.snap
@@ -428,6 +428,7 @@ Object {
"complete": false,
"courseId": "course-v1:edX+DemoX+Demo_Course",
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
+ "optional": undefined,
"resumeBlock": false,
"sequenceIds": Array [
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
@@ -444,6 +445,7 @@ Object {
"effortTime": 15,
"icon": null,
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
+ "optional": undefined,
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"showLink": true,
"title": "Title of Sequence",
@@ -636,6 +638,11 @@ Object {
},
"hasScheduledContent": false,
"id": "course-v1:edX+DemoX+Demo_Course",
+ "optionalCompletionSummary": Object {
+ "completeCount": 1,
+ "incompleteCount": 1,
+ "lockedCount": 0,
+ },
"sectionScores": Array [
Object {
"displayName": "First section",
diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js
index 3265d8a1c4..76fd785d32 100644
--- a/src/course-home/data/api.js
+++ b/src/course-home/data/api.js
@@ -136,6 +136,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
title: block.display_name,
resumeBlock: block.resume_block,
sequenceIds: block.children || [],
+ optional: block.optional_content,
};
break;
@@ -152,6 +153,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
// link in the outline (even though we ignore the given url and use an internal to ourselves).
showLink: !!block.lms_web_url,
title: block.display_name,
+ optional: block.optional_content,
};
break;
diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx
index 3de888a89a..668d3b2cc6 100644
--- a/src/course-home/outline-tab/Section.jsx
+++ b/src/course-home/outline-tab/Section.jsx
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { Collapsible, IconButton } from '@edx/paragon';
+import { Badge, Collapsible, IconButton } from '@edx/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';
@@ -23,6 +23,7 @@ const Section = ({
complete,
sequenceIds,
title,
+ optional,
} = section;
const {
courseBlocks: {
@@ -64,6 +65,7 @@ const Section = ({
{title}
+ {intl.formatMessage(messages.optionalContent)}
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
diff --git a/src/course-home/outline-tab/SequenceLink.jsx b/src/course-home/outline-tab/SequenceLink.jsx
index 0530d53e66..a2052ebb1f 100644
--- a/src/course-home/outline-tab/SequenceLink.jsx
+++ b/src/course-home/outline-tab/SequenceLink.jsx
@@ -29,6 +29,7 @@ const SequenceLink = ({
due,
showLink,
title,
+ optional,
} = sequence;
const {
userTimezone,
@@ -115,6 +116,9 @@ const SequenceLink = ({
+
+ {optional ? intl.formatMessage(messages.optionalContent) : ''}
+
{due ? dueDateMessage : noDueDateMessage}
diff --git a/src/course-home/outline-tab/messages.js b/src/course-home/outline-tab/messages.js
index 8d3204594f..98a76ffe5d 100644
--- a/src/course-home/outline-tab/messages.js
+++ b/src/course-home/outline-tab/messages.js
@@ -99,6 +99,11 @@ const messages = defineMessages({
defaultMessage: 'Open',
description: 'A button to open the given section of the course outline',
},
+ optionalContent: {
+ id: 'learning.outline.optionalBlock',
+ defaultMessage: 'Optional',
+ description: 'Used as a label to indicate that a section, sequence, or unit is optional.',
+ },
proctoringInfoPanel: {
id: 'learning.proctoringPanel.header',
defaultMessage: 'This course contains proctored exams',
diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx
index 9f6dc3dbf5..37cc3c8a1c 100644
--- a/src/course-home/progress-tab/ProgressTab.test.jsx
+++ b/src/course-home/progress-tab/ProgressTab.test.jsx
@@ -262,6 +262,11 @@ describe('Progress Tab', () => {
incomplete_count: 1,
locked_count: 1,
},
+ optional_completion_summary: {
+ complete_count: 1,
+ incomplete_count: 1,
+ locked_count: 0,
+ },
verified_mode: {
access_expiration_date: '2050-01-01T12:00:00',
currency: 'USD',
@@ -304,6 +309,11 @@ describe('Progress Tab', () => {
incomplete_count: 1,
locked_count: 1,
},
+ optional_completion_summary: {
+ complete_count: 1,
+ incomplete_count: 1,
+ locked_count: 0,
+ },
verified_mode: {
access_expiration_date: '2050-01-01T12:00:00',
currency: 'USD',
@@ -364,6 +374,11 @@ describe('Progress Tab', () => {
incomplete_count: 1,
locked_count: 1,
},
+ optional_completion_summary: {
+ complete_count: 1,
+ incomplete_count: 1,
+ locked_count: 0,
+ },
section_scores: [
{
display_name: 'First section',
@@ -402,6 +417,11 @@ describe('Progress Tab', () => {
incomplete_count: 1,
locked_count: 1,
},
+ optional_completion_summary: {
+ complete_count: 1,
+ incomplete_count: 1,
+ locked_count: 0,
+ },
verified_mode: {
access_expiration_date: '2050-01-01T12:00:00',
currency: 'USD',
diff --git a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx
index 54b6caa9c6..a5e4ae08ed 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,20 @@ import IncompleteDonutSegment from './IncompleteDonutSegment';
import LockedDonutSegment from './LockedDonutSegment';
import messages from './messages';
-const CompletionDonutChart = ({ intl }) => {
+const CompletionDonutChart = ({ intl, optional }) => {
const {
courseId,
} = useSelector(state => state.courseHome);
+ const key = optional ? 'optionalCompletionSummary' : 'completionSummary';
+ const label = optional ? intl.formatMessage(messages.optionalDonutLabel) : intl.formatMessage(messages.donutLabel);
+
+ const progress = useModel('progress', courseId);
const {
- completionSummary: {
- completeCount,
- incompleteCount,
- lockedCount,
- },
- } = useModel('progress', courseId);
+ completeCount,
+ incompleteCount,
+ lockedCount,
+ } = progress[key];
const numTotalUnits = completeCount + incompleteCount + lockedCount;
const completePercentage = completeCount ? Number(((completeCount / numTotalUnits) * 100).toFixed(0)) : 0;
@@ -30,6 +33,10 @@ const CompletionDonutChart = ({ intl }) => {
const isLocaleRtl = isRtl(getLocale());
+ if (optional && numTotalUnits === 0) {
+ return <>>;
+ }
+
return (
<>
-
+
+
diff --git a/src/course-home/progress-tab/course-completion/messages.js b/src/course-home/progress-tab/course-completion/messages.js
index 08bb8f59db..81151ec8ab 100644
--- a/src/course-home/progress-tab/course-completion/messages.js
+++ b/src/course-home/progress-tab/course-completion/messages.js
@@ -6,6 +6,11 @@ const messages = defineMessages({
defaultMessage: 'completed',
description: 'Label text for progress donut chart',
},
+ optionalDonutLabel: {
+ id: 'progress.completion.optionalDonut.label',
+ defaultMessage: 'optional',
+ description: 'Label text for optional progress donut chart',
+ },
completionBody: {
id: 'progress.completion.body',
defaultMessage: 'This represents how much of the course content you have completed. Note that some content may not yet be released.',
diff --git a/src/courseware/course/sequence/Unit.jsx b/src/courseware/course/sequence/Unit.jsx
index 1926a36c46..64755242b5 100644
--- a/src/courseware/course/sequence/Unit.jsx
+++ b/src/courseware/course/sequence/Unit.jsx
@@ -1,7 +1,7 @@
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { AppContext, ErrorPage } from '@edx/frontend-platform/react';
-import { Modal } from '@edx/paragon';
+import { Modal, Badge } from '@edx/paragon';
import PropTypes from 'prop-types';
import React, {
Suspense, useCallback, useContext, useEffect, useLayoutEffect, useState,
@@ -155,6 +155,7 @@ const Unit = ({
return (
{unit.title}
+ {intl.formatMessage(messages.optionalContent)}
{intl.formatMessage(messages.headerPlaceholder)}