Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optional xblocks #9

Open
wants to merge 3 commits into
base: opencraft-release/palm.1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions src/course-home/data/__factories__/progressTabData.factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/course-home/data/__snapshots__/redux.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"optionalCompletion": undefined,
"resumeBlock": false,
"sequenceIds": Array [
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
Expand All @@ -444,6 +445,7 @@ Object {
"effortTime": 15,
"icon": null,
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
"optionalCompletion": undefined,
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"showLink": true,
"title": "Title of Sequence",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/course-home/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
title: block.display_name,
resumeBlock: block.resume_block,
sequenceIds: block.children || [],
optionalCompletion: block.optional_completion,
};
break;

Expand All @@ -152,6 +153,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
// link in the outline (even though we ignore the given url and use an internal <Link> to ourselves).
showLink: !!block.lms_web_url,
title: block.display_name,
optionalCompletion: block.optional_completion,
};
break;

Expand Down
4 changes: 3 additions & 1 deletion src/course-home/outline-tab/Section.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useLocation } from 'react-router-dom';
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';
Expand All @@ -27,6 +27,7 @@ const Section = ({
complete,
sequenceIds,
title,
optionalCompletion,
} = section;
const {
courseBlocks: {
Expand Down Expand Up @@ -74,6 +75,7 @@ const Section = ({
</div>
<div className="col-10 ml-3 p-0 font-weight-bold text-dark-500">
<span className="align-middle">{title}</span>
<Badge className="ml-2" variant="light" hidden={!optionalCompletion}>{intl.formatMessage(messages.optionalCompletion)}</Badge>
<span className="sr-only">
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
</span>
Expand Down
7 changes: 7 additions & 0 deletions src/course-home/outline-tab/SequenceLink.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { Badge } from '@edx/paragon';
import {
FormattedMessage,
FormattedTime,
Expand Down Expand Up @@ -33,6 +34,7 @@
due,
showLink,
title,
optionalCompletion,
} = sequence;
const {
userTimezone,
Expand Down Expand Up @@ -128,6 +130,11 @@
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
</span>
<EffortEstimate className="ml-3 align-middle" block={sequence} />
{optionalCompletion && (
<Badge className="ml-2" variant="light">

Check warning on line 134 in src/course-home/outline-tab/SequenceLink.jsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L134 was not covered by tests
{intl.formatMessage(messages.optionalCompletion)}
</Badge>
)}
</div>
</div>
<div className="row w-100 m-0 ml-3 pl-3">
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 @@ -99,6 +99,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
112 changes: 112 additions & 0 deletions src/course-home/progress-tab/ProgressTab.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -1359,4 +1379,96 @@ describe('Progress Tab', () => {
expect(screen.getByText('Course progress for otherstudent')).toBeInTheDocument();
});
});

describe('Completion Donut Chart', () => {
it('Renders optional completion donut chart', async () => {
setTabData({
completion_summary: {
complete_count: 1,
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',
currency_symbol: '$',
price: 149,
sku: 'ABCD1234',
upgrade_url: 'edx.org/upgrade',
},
section_scores: [
{
display_name: 'First section',
subsections: [
{
assignment_type: 'Homework',
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
display_name: 'First subsection',
learner_has_access: false,
has_graded_assignment: true,
num_points_earned: 8,
num_points_possible: 10,
percent_graded: 1.0,
show_correctness: 'always',
show_grades: true,
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
},
],
},
],
});
await fetchAndRender();
expect(screen.getByText('optional')).toBeInTheDocument();
});

it('Hides optional completion donut chart', async () => {
setTabData({
completion_summary: {
complete_count: 1,
incomplete_count: 1,
locked_count: 1,
},
optional_completion_summary: {
complete_count: 0,
incomplete_count: 0,
locked_count: 0,
},
verified_mode: {
access_expiration_date: '2050-01-01T12:00:00',
currency: 'USD',
currency_symbol: '$',
price: 149,
sku: 'ABCD1234',
upgrade_url: 'edx.org/upgrade',
},
section_scores: [
{
display_name: 'First section',
subsections: [
{
assignment_type: 'Homework',
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
display_name: 'First subsection',
learner_has_access: false,
has_graded_assignment: true,
num_points_earned: 8,
num_points_possible: 10,
percent_graded: 1.0,
show_correctness: 'always',
show_grades: true,
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
},
],
},
],
});
await fetchAndRender();
expect(screen.queryByText('optional')).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,28 @@ 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 }) => {
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;
Expand All @@ -30,6 +33,10 @@ const CompletionDonutChart = ({ intl }) => {

const isLocaleRtl = isRtl(getLocale());

if (optional && numTotalUnits === 0) {
return <></>;
Agrendalath marked this conversation as resolved.
Show resolved Hide resolved
}

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 +49,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 +69,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 @@ -14,7 +14,8 @@ const CourseCompletion = ({ intl }) => (
</p>
</div>
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
<CompletionDonutChart />
<CompletionDonutChart optional={false} />
<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
3 changes: 2 additions & 1 deletion src/courseware/course/sequence/Unit.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -143,6 +143,7 @@ const Unit = ({
return (
<div className="unit">
<h1 className="mb-0 h3">{unit.title}</h1>
<Badge className="ml-2" variant="light" hidden={!unit.optionalCompletion}>{intl.formatMessage(messages.optionalCompletion)}</Badge>
<h2 className="sr-only">{intl.formatMessage(messages.headerPlaceholder)}</h2>
<BookmarkButton
unitId={unit.id}
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.',
},
optionalCompletion: {
id: 'learn.sequence.optionalBlock',
defaultMessage: 'Optional',
description: 'Used as a label to indicate that a unit is optional.',
},
});

export default messages;
1 change: 1 addition & 0 deletions src/courseware/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ function normalizeSequenceMetadata(sequence) {
contentType: unit.type,
graded: unit.graded,
containsContentTypeGatedContent: unit.contains_content_type_gated_content,
optionalCompletion: unit.optional_completion,
})),
};
}
Expand Down
Loading