Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/course-home/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand Down
10 changes: 9 additions & 1 deletion src/course-home/outline-tab/Section.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -25,6 +27,7 @@ const Section = ({
sequenceIds,
title,
hideFromTOC,
optionalCompletion,
} = section;
const {
courseBlocks: {
Expand Down Expand Up @@ -82,6 +85,11 @@ const Section = ({
)}
</div>
)}
{optionalCompletion && (
<Badge className="align-self-center text-uppercase border" variant="light" data-testid="optional-completion-badge-outline-section">
{intl.formatMessage(messages.optionalCompletion)}
</Badge>
)}
</div>
);

Expand Down
10 changes: 8 additions & 2 deletions src/course-home/outline-tab/SequenceLink.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,6 +31,7 @@ const SequenceLink = ({
showLink,
title,
hideFromTOC,
optionalCompletion,
} = sequence;
const {
userTimezone,
Expand Down Expand Up @@ -108,12 +109,17 @@ const SequenceLink = ({
/>
)}
</div>
<div className="col-10 p-0 ml-3 text-break">
<div className="d-flex justify-content-between col-11 p-0 ml-3 text-break">
<span className="align-middle">{displayTitle}</span>
<span className="sr-only">
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
</span>
<EffortEstimate className="ml-3 align-middle" block={sequence} />
{optionalCompletion && (
<Badge className="align-self-center text-uppercase mr-5 border" variant="light" data-testid="optional-completion-badge-outline-subsection">
{intl.formatMessage(messages.optionalCompletion)}
</Badge>
)}
</div>
</div>
{hideFromTOC && (
Expand Down
5 changes: 5 additions & 0 deletions src/course-home/outline-tab/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@ 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';
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 label = optional ? intl.formatMessage(messages.optionalDonutLabel) : intl.formatMessage(messages.donutLabel);

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;
Expand All @@ -30,6 +31,10 @@ const CompletionDonutChart = ({ intl }) => {

const isLocaleRtl = isRtl(getLocale());

if (optional && numTotalUnits === 0) {
return <></>;
}

return (
<>
<svg role="img" width="50%" height="100%" viewBox="0 0 42 42" className="donut" style={{ maxWidth: '178px' }} aria-hidden="true">
Expand All @@ -42,7 +47,7 @@ const CompletionDonutChart = ({ intl }) => {
{completePercentage}{isLocaleRtl && '\u200f'}%
</text>
<text x="50%" y="50%" className="donut-chart-label">
{intl.formatMessage(messages.donutLabel)}
{label}
</text>
</g>
<IncompleteDonutSegment incompletePercentage={incompletePercentage} />
Expand All @@ -62,8 +67,13 @@ const CompletionDonutChart = ({ intl }) => {
);
};

CompletionDonutChart.defaultProps = {
optional: false,
};

CompletionDonutChart.propTypes = {
intl: intlShape.isRequired,
optional: PropTypes.bool,
};

export default injectIntl(CompletionDonutChart);
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const CourseCompletion = ({ intl }) => (
</div>
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
<CompletionDonutChart />
<CompletionDonutChart optional />
</div>
</div>
</section>
Expand Down
5 changes: 5 additions & 0 deletions src/course-home/progress-tab/course-completion/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
6 changes: 6 additions & 0 deletions src/courseware/course/sequence/Unit/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const Unit = ({
const unit = useModel(modelKeys.units, id);
const isProcessing = unit.bookmarkedUpdateState === 'loading';
const view = authenticatedUser ? views.student : views.public;
const { optionalCompletion } = unit;

const getUrl = usePluginsCallback('getIFrameUrl', () => getIFrameUrl({
id,
Expand All @@ -51,6 +52,11 @@ const Unit = ({
isBookmarked={unit.bookmarked}
isProcessing={isProcessing}
/>
{optionalCompletion && (
<div className="alert alert-info small my-3" role="alert" data-testid="optional-completion-unit-alert">
{formatMessage(messages.optionalCompletionUnitAlert)}
</div>
)}
<UnitSuspense {...{ courseId, id }} />
<ContentIFrame
elementId="unit-iframe"
Expand Down
5 changes: 5 additions & 0 deletions src/courseware/course/sequence/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const messages = defineMessages({
defaultMessage: 'There is no content here.',
description: 'Message shown when there is no content to show a user inside a learning sequence.',
},
optionalCompletionUnitAlert: {
id: 'learn.optionalCompletionUnitAlert',
defaultMessage: 'This is optional content and will not affect your course score or completion.',
description: 'Alert message shown in a unit when the unit has optional completion.',
},
});

export default messages;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Icon } from '@openedx/paragon';
import { Badge, Button, Icon } from '@openedx/paragon';
import { ChevronRight as ChevronRightIcon } from '@openedx/paragon/icons';

import courseOutlineMessages from '@src/course-home/outline-tab/messages';
Expand All @@ -16,6 +16,7 @@ const SidebarSection = ({ intl, section, handleSelectSection }) => {
title,
sequenceIds,
completionStat,
optionalCompletion,
} = section;

const activeSequenceId = useSelector(getSequenceId);
Expand All @@ -26,13 +27,18 @@ const SidebarSection = ({ intl, section, handleSelectSection }) => {
<div className="col-auto p-0">
<CompletionIcon completionStat={completionStat} />
</div>
<div className="col-10 ml-3 p-0 flex-grow-1 text-dark-500 text-left text-break">
<div className="d-flex justify-content-between col-10 ml-3 p-0 flex-grow-1 text-dark-500 text-left text-break">
{title}
<span className="sr-only">
, {intl.formatMessage(complete
? courseOutlineMessages.completedSection
: courseOutlineMessages.incompleteSection)}
</span>
{optionalCompletion && (
<Badge className="align-self-center text-uppercase border" variant="light" data-testid="optional-completion-badge-sidebar-section">
{intl.formatMessage(courseOutlineMessages.optionalCompletion)}
</Badge>
)}
</div>
</>
);
Expand Down Expand Up @@ -65,6 +71,7 @@ SidebarSection.propTypes = {
completed: PropTypes.number,
total: PropTypes.number,
}),
optionalCompletion: PropTypes.bool,
}).isRequired,
handleSelectSection: PropTypes.func.isRequired,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible } from '@openedx/paragon';
import { Badge, Collapsible } from '@openedx/paragon';

import courseOutlineMessages from '@src/course-home/outline-tab/messages';
import { getCourseOutline, getSequenceId } from '@src/courseware/data/selectors';
Expand All @@ -26,6 +26,7 @@ const SidebarSequence = ({
unitIds,
type,
completionStat,
optionalCompletion,
} = sequence;

const [open, setOpen] = useState(defaultOpen);
Expand All @@ -38,8 +39,15 @@ const SidebarSequence = ({
<div className="col-auto p-0" style={{ fontSize: '1.1rem' }}>
<CompletionIcon completionStat={completionStat} />
</div>
<div className="col-9 d-flex flex-column flex-grow-1 ml-3 mr-auto p-0 text-left">
<span className="align-middle text-dark-500">{title}</span>
<div className="col-10 d-flex flex-column flex-grow-1 ml-3 mr-auto p-0 text-left">
<span className="d-flex justify-content-between align-middle text-dark-500">
{title}
{optionalCompletion && (
<Badge className="align-self-center text-uppercase border" variant="light" data-testid="optional-completion-badge-sidebar-sequence">
{intl.formatMessage(courseOutlineMessages.optionalCompletion)}
</Badge>
)}
</span>
{specialExamInfo && <span className="align-middle small text-muted">{specialExamInfo}</span>}
<span className="sr-only">
, {intl.formatMessage(complete
Expand Down Expand Up @@ -94,6 +102,7 @@ SidebarSequence.propTypes = {
completed: PropTypes.number,
total: PropTypes.number,
}),
optionalCompletion: PropTypes.bool,
}).isRequired,
activeUnitId: PropTypes.string.isRequired,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { Badge } from '@openedx/paragon';

import { checkBlockCompletion } from '@src/courseware/data';
import { getCourseOutline } from '@src/courseware/data/selectors';
Expand All @@ -24,6 +25,7 @@ const SidebarUnit = ({
}) => {
const {
complete,
optionalCompletion,
title,
icon = UNIT_ICON_TYPES.other,
} = unit;
Expand Down Expand Up @@ -74,10 +76,15 @@ const SidebarUnit = ({
<div className="col-auto p-0">
<UnitIcon type={iconType} isCompleted={complete} />
</div>
<div className="col-10 p-0 ml-3 text-break">
<div className="d-flex justify-content-between col-10 p-0 ml-3 text-break">
<span className="align-middle">
{title}
</span>
{optionalCompletion && (
<Badge className="align-self-center text-uppercase mr-1 border" variant="light" data-testid="optional-completion-badge-sidebar-unit">
{intl.formatMessage(messages.optionalCompletion)}
</Badge>
)}
<span className="sr-only">
, {intl.formatMessage(complete ? messages.completedUnit : messages.incompleteUnit)}
</span>
Expand All @@ -97,6 +104,7 @@ SidebarUnit.propTypes = {
id: PropTypes.string,
title: PropTypes.string,
type: PropTypes.string,
optionalCompletion: PropTypes.bool,
}).isRequired,
isActive: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const messages = defineMessages({
defaultMessage: 'Incomplete unit',
description: 'Text used to describe the gray checkmark icon in front of a unit title',
},
optionalCompletion: {
id: 'learn.sequence.optionalBlock',
defaultMessage: 'Optional',
description: 'Used as a label to indicate that a unit is optional.',
},
});

export default messages;
4 changes: 4 additions & 0 deletions src/courseware/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export function normalizeSequenceMetadata(sequence) {
contentType: unit.type,
graded: unit.graded,
containsContentTypeGatedContent: unit.contains_content_type_gated_content,
optionalCompletion: unit.optional_completion,
})),
};
}
Expand Down Expand Up @@ -173,6 +174,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
completed: block.completion_stat?.completion,
total: block.completion_stat?.completable_children,
},
optionalCompletion: block.optional_completion || false,
};
break;

Expand All @@ -189,6 +191,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
completed: block.completion_stat?.completion,
total: block.completion_stat?.completable_children,
},
optionalCompletion: block.optional_completion || false,
};
break;

Expand All @@ -199,6 +202,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
id: block.id,
title: block.display_name,
type: block.type,
optionalCompletion: block.optional_completion || false,
};
break;

Expand Down
Loading