Skip to content

Commit 1362767

Browse files
committed
feat: implement optional completion
1 parent ad9166f commit 1362767

File tree

14 files changed

+99
-18
lines changed

14 files changed

+99
-18
lines changed

src/course-home/data/api.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
137137
resumeBlock: block.resume_block,
138138
sequenceIds: block.children || [],
139139
hideFromTOC: block.hide_from_toc,
140+
optionalCompletion: block.optional_completion,
140141
};
141142
break;
142143

@@ -155,6 +156,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
155156
title: block.display_name,
156157
hideFromTOC: block.hide_from_toc,
157158
navigationDisabled: block.navigation_disabled,
159+
optionalCompletion: block.optional_completion,
158160
};
159161
break;
160162

src/course-home/outline-tab/Section.jsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import React, { useEffect, useState } from 'react';
22
import PropTypes from 'prop-types';
33
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
4-
import { Collapsible, IconButton, Icon } from '@openedx/paragon';
4+
import {
5+
Badge, Collapsible, IconButton, Icon,
6+
} from '@openedx/paragon';
57
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
68
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
79
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -25,6 +27,7 @@ const Section = ({
2527
sequenceIds,
2628
title,
2729
hideFromTOC,
30+
optionalCompletion,
2831
} = section;
2932
const {
3033
courseBlocks: {
@@ -82,6 +85,11 @@ const Section = ({
8285
)}
8386
</div>
8487
)}
88+
{optionalCompletion && (
89+
<Badge className="align-self-center text-uppercase border" variant="light" data-testid="optional-completion-badge-outline-section">
90+
{intl.formatMessage(messages.optionalCompletion)}
91+
</Badge>
92+
)}
8593
</div>
8694
);
8795

src/course-home/outline-tab/SequenceLink.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-ico
1111
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
1212
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
1313

14-
import { Icon } from '@openedx/paragon';
14+
import { Badge, Icon } from '@openedx/paragon';
1515
import { Block } from '@openedx/paragon/icons';
1616
import EffortEstimate from '../../shared/effort-estimate';
1717
import { useModel } from '../../generic/model-store';
@@ -31,6 +31,7 @@ const SequenceLink = ({
3131
showLink,
3232
title,
3333
hideFromTOC,
34+
optionalCompletion,
3435
} = sequence;
3536
const {
3637
userTimezone,
@@ -108,12 +109,17 @@ const SequenceLink = ({
108109
/>
109110
)}
110111
</div>
111-
<div className="col-10 p-0 ml-3 text-break">
112+
<div className="d-flex justify-content-between col-11 p-0 ml-3 text-break">
112113
<span className="align-middle">{displayTitle}</span>
113114
<span className="sr-only">
114115
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
115116
</span>
116117
<EffortEstimate className="ml-3 align-middle" block={sequence} />
118+
{optionalCompletion && (
119+
<Badge className="align-self-center text-uppercase mr-5 border" variant="light" data-testid="optional-completion-badge-outline-subsection">
120+
{intl.formatMessage(messages.optionalCompletion)}
121+
</Badge>
122+
)}
117123
</div>
118124
</div>
119125
{hideFromTOC && (

src/course-home/outline-tab/messages.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ const messages = defineMessages({
109109
defaultMessage: 'Open',
110110
description: 'A button to open the given section of the course outline',
111111
},
112+
optionalCompletion: {
113+
id: 'learning.outline.optionalBlock',
114+
defaultMessage: 'Optional',
115+
description: 'Used as a label to indicate that a section or sequence is optional.',
116+
},
112117
proctoringInfoPanel: {
113118
id: 'learning.proctoringPanel.header',
114119
defaultMessage: 'This course contains proctored exams',

src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,26 @@ import { useSelector } from 'react-redux';
33
import {
44
getLocale, injectIntl, intlShape, isRtl,
55
} from '@edx/frontend-platform/i18n';
6+
import PropTypes from 'prop-types';
67
import { useModel } from '../../../generic/model-store';
78

89
import CompleteDonutSegment from './CompleteDonutSegment';
910
import IncompleteDonutSegment from './IncompleteDonutSegment';
1011
import LockedDonutSegment from './LockedDonutSegment';
1112
import messages from './messages';
1213

13-
const CompletionDonutChart = ({ intl }) => {
14+
const CompletionDonutChart = ({ intl, optional = false }) => {
1415
const {
1516
courseId,
1617
} = useSelector(state => state.courseHome);
1718

18-
const {
19-
completionSummary: {
20-
completeCount,
21-
incompleteCount,
22-
lockedCount,
23-
},
24-
} = useModel('progress', courseId);
19+
const label = optional ? intl.formatMessage(messages.optionalDonutLabel) : intl.formatMessage(messages.donutLabel);
20+
21+
const progress = useModel('progress', courseId);
22+
const completionSummary = progress?.completionSummary || {};
23+
const completeCount = optional ? completionSummary.optionalCompleteCount : completionSummary.completeCount;
24+
const incompleteCount = optional ? completionSummary.optionalIncompleteCount : completionSummary.incompleteCount;
25+
const lockedCount = optional ? completionSummary.optionalLockedCount : completionSummary.lockedCount;
2526

2627
const numTotalUnits = completeCount + incompleteCount + lockedCount;
2728
const completePercentage = completeCount ? Number(((completeCount / numTotalUnits) * 100).toFixed(0)) : 0;
@@ -30,6 +31,10 @@ const CompletionDonutChart = ({ intl }) => {
3031

3132
const isLocaleRtl = isRtl(getLocale());
3233

34+
if (optional && numTotalUnits === 0) {
35+
return <></>;
36+
}
37+
3338
return (
3439
<>
3540
<svg role="img" width="50%" height="100%" viewBox="0 0 42 42" className="donut" style={{ maxWidth: '178px' }} aria-hidden="true">
@@ -42,7 +47,7 @@ const CompletionDonutChart = ({ intl }) => {
4247
{completePercentage}{isLocaleRtl && '\u200f'}%
4348
</text>
4449
<text x="50%" y="50%" className="donut-chart-label">
45-
{intl.formatMessage(messages.donutLabel)}
50+
{label}
4651
</text>
4752
</g>
4853
<IncompleteDonutSegment incompletePercentage={incompletePercentage} />
@@ -62,8 +67,13 @@ const CompletionDonutChart = ({ intl }) => {
6267
);
6368
};
6469

70+
CompletionDonutChart.defaultProps = {
71+
optional: false,
72+
};
73+
6574
CompletionDonutChart.propTypes = {
6675
intl: intlShape.isRequired,
76+
optional: PropTypes.bool,
6777
};
6878

6979
export default injectIntl(CompletionDonutChart);

src/course-home/progress-tab/course-completion/CourseCompletion.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const CourseCompletion = ({ intl }) => (
1515
</div>
1616
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
1717
<CompletionDonutChart />
18+
<CompletionDonutChart optional />
1819
</div>
1920
</div>
2021
</section>

src/course-home/progress-tab/course-completion/messages.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ const messages = defineMessages({
66
defaultMessage: 'completed',
77
description: 'Label text for progress donut chart',
88
},
9+
optionalDonutLabel: {
10+
id: 'progress.completion.optionalDonut.label',
11+
defaultMessage: 'optional',
12+
description: 'Label text for optional progress donut chart',
13+
},
914
completionBody: {
1015
id: 'progress.completion.body',
1116
defaultMessage: 'This represents how much of the course content you have completed. Note that some content may not yet be released.',

src/courseware/course/sequence/Unit/index.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const Unit = ({
2929
const unit = useModel(modelKeys.units, id);
3030
const isProcessing = unit.bookmarkedUpdateState === 'loading';
3131
const view = authenticatedUser ? views.student : views.public;
32+
const { optionalCompletion } = unit;
3233

3334
const getUrl = usePluginsCallback('getIFrameUrl', () => getIFrameUrl({
3435
id,
@@ -51,6 +52,11 @@ const Unit = ({
5152
isBookmarked={unit.bookmarked}
5253
isProcessing={isProcessing}
5354
/>
55+
{optionalCompletion && (
56+
<div className="alert alert-info small my-3" role="alert" data-testid="optional-completion-unit-alert">
57+
{formatMessage(messages.optionalCompletionUnitAlert)}
58+
</div>
59+
)}
5460
<UnitSuspense {...{ courseId, id }} />
5561
<ContentIFrame
5662
elementId="unit-iframe"

src/courseware/course/sequence/messages.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ const messages = defineMessages({
3131
defaultMessage: 'There is no content here.',
3232
description: 'Message shown when there is no content to show a user inside a learning sequence.',
3333
},
34+
optionalCompletionUnitAlert: {
35+
id: 'learn.optionalCompletionUnitAlert',
36+
defaultMessage: 'This is optional content and will not affect your course score or completion.',
37+
description: 'Alert message shown in a unit when the unit has optional completion.',
38+
},
3439
});
3540

3641
export default messages;

src/courseware/course/sidebar/sidebars/course-outline/components/SidebarSection.jsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
22
import classNames from 'classnames';
33
import { useSelector } from 'react-redux';
44
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
5-
import { Button, Icon } from '@openedx/paragon';
5+
import { Badge, Button, Icon } from '@openedx/paragon';
66
import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons';
77

88
import courseOutlineMessages from '@src/course-home/outline-tab/messages';
@@ -16,6 +16,7 @@ const SidebarSection = ({ intl, section, handleSelectSection }) => {
1616
title,
1717
sequenceIds,
1818
completionStat,
19+
optionalCompletion,
1920
} = section;
2021

2122
const activeSequenceId = useSelector(getSequenceId);
@@ -26,13 +27,18 @@ const SidebarSection = ({ intl, section, handleSelectSection }) => {
2627
<div className="col-auto p-0">
2728
<CompletionIcon completionStat={completionStat} />
2829
</div>
29-
<div className="col-10 ml-3 p-0 flex-grow-1 text-dark-500 text-left text-break">
30+
<div className="d-flex justify-content-between col-10 ml-3 p-0 flex-grow-1 text-dark-500 text-left text-break">
3031
{title}
3132
<span className="sr-only">
3233
, {intl.formatMessage(complete
3334
? courseOutlineMessages.completedSection
3435
: courseOutlineMessages.incompleteSection)}
3536
</span>
37+
{optionalCompletion && (
38+
<Badge className="align-self-center text-uppercase border" variant="light" data-testid="optional-completion-badge-sidebar-section">
39+
{intl.formatMessage(courseOutlineMessages.optionalCompletion)}
40+
</Badge>
41+
)}
3642
</div>
3743
</>
3844
);
@@ -65,6 +71,7 @@ SidebarSection.propTypes = {
6571
completed: PropTypes.number,
6672
total: PropTypes.number,
6773
}),
74+
optionalCompletion: PropTypes.bool,
6875
}).isRequired,
6976
handleSelectSection: PropTypes.func.isRequired,
7077
};

0 commit comments

Comments
 (0)