From f21c8550c0e07f7b7872b18ab74a7d54bb926a0a Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Thu, 22 Feb 2024 00:48:12 -0300 Subject: [PATCH 1/3] feat: optional xblocks --- .../__factories__/progressTabData.factory.js | 5 ++++ .../data/__snapshots__/redux.test.js.snap | 7 +++++ src/course-home/data/api.js | 2 ++ src/course-home/outline-tab/Section.jsx | 4 ++- src/course-home/outline-tab/SequenceLink.jsx | 4 +++ src/course-home/outline-tab/messages.js | 5 ++++ .../progress-tab/ProgressTab.test.jsx | 20 +++++++++++++ .../CompletionDonutChart.jsx | 28 +++++++++++++------ .../course-completion/CourseCompletion.jsx | 3 +- .../course-completion/messages.js | 5 ++++ src/courseware/course/sequence/Unit.jsx | 3 +- src/courseware/course/sequence/messages.js | 5 ++++ src/courseware/data/api.js | 1 + 13 files changed, 81 insertions(+), 11 deletions(-) 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 60d83269fd..d93265122d 100644 --- a/src/course-home/outline-tab/Section.jsx +++ b/src/course-home/outline-tab/Section.jsx @@ -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'; @@ -27,6 +27,7 @@ const Section = ({ complete, sequenceIds, title, + optional, } = section; const { courseBlocks: { @@ -74,6 +75,7 @@ const Section = ({
{title} + , {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 2e0ef3725c..3cfc8356b6 100644 --- a/src/course-home/outline-tab/SequenceLink.jsx +++ b/src/course-home/outline-tab/SequenceLink.jsx @@ -33,6 +33,7 @@ const SequenceLink = ({ due, showLink, title, + optional, } = sequence; const { userTimezone, @@ -131,6 +132,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 274bc68143..1be69e0621 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, @@ -143,6 +143,7 @@ const Unit = ({ return (

{unit.title}

+

{intl.formatMessage(messages.headerPlaceholder)}

Date: Thu, 29 Feb 2024 03:27:46 -0300 Subject: [PATCH 2/3] refactor: rename to optional completion --- src/course-home/data/api.js | 4 ++-- src/course-home/outline-tab/Section.jsx | 2 +- src/course-home/outline-tab/SequenceLink.jsx | 2 +- src/course-home/outline-tab/messages.js | 2 +- src/courseware/course/sequence/Unit.jsx | 2 +- src/courseware/course/sequence/messages.js | 2 +- src/courseware/data/api.js | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 76fd785d32..633b11c20c 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -136,7 +136,7 @@ export function normalizeOutlineBlocks(courseId, blocks) { title: block.display_name, resumeBlock: block.resume_block, sequenceIds: block.children || [], - optional: block.optional_content, + optional: block.optional_completion, }; break; @@ -153,7 +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, + optional: block.optional_completion, }; break; diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx index d93265122d..80a45d63c5 100644 --- a/src/course-home/outline-tab/Section.jsx +++ b/src/course-home/outline-tab/Section.jsx @@ -75,7 +75,7 @@ const Section = ({
{title} - + , {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 3cfc8356b6..59c28299f2 100644 --- a/src/course-home/outline-tab/SequenceLink.jsx +++ b/src/course-home/outline-tab/SequenceLink.jsx @@ -133,7 +133,7 @@ const SequenceLink = ({
- {optional ? intl.formatMessage(messages.optionalContent) : ''} + {optional ? intl.formatMessage(messages.optionalCompletion) : ''} {due ? dueDateMessage : noDueDateMessage} diff --git a/src/course-home/outline-tab/messages.js b/src/course-home/outline-tab/messages.js index 98a76ffe5d..d3b0ac5e5c 100644 --- a/src/course-home/outline-tab/messages.js +++ b/src/course-home/outline-tab/messages.js @@ -99,7 +99,7 @@ const messages = defineMessages({ defaultMessage: 'Open', description: 'A button to open the given section of the course outline', }, - optionalContent: { + optionalCompletion: { id: 'learning.outline.optionalBlock', defaultMessage: 'Optional', description: 'Used as a label to indicate that a section, sequence, or unit is optional.', diff --git a/src/courseware/course/sequence/Unit.jsx b/src/courseware/course/sequence/Unit.jsx index 1be69e0621..415f4037a3 100644 --- a/src/courseware/course/sequence/Unit.jsx +++ b/src/courseware/course/sequence/Unit.jsx @@ -143,7 +143,7 @@ const Unit = ({ return (

{unit.title}

- +

{intl.formatMessage(messages.headerPlaceholder)}

Date: Sat, 9 Mar 2024 21:48:56 -0300 Subject: [PATCH 3/3] refactor: opt. donut test, rename field to optionalCompletion --- .../data/__snapshots__/redux.test.js.snap | 4 +- src/course-home/data/api.js | 4 +- src/course-home/outline-tab/Section.jsx | 4 +- src/course-home/outline-tab/SequenceLink.jsx | 11 ++- src/course-home/outline-tab/messages.js | 2 +- .../progress-tab/ProgressTab.test.jsx | 92 +++++++++++++++++++ src/courseware/course/sequence/Unit.jsx | 2 +- src/courseware/course/sequence/messages.js | 2 +- src/courseware/data/api.js | 2 +- 9 files changed, 109 insertions(+), 14 deletions(-) diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 3f8b64924f..a9e6c1c535 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -428,7 +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, + "optionalCompletion": undefined, "resumeBlock": false, "sequenceIds": Array [ "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", @@ -445,7 +445,7 @@ Object { "effortTime": 15, "icon": null, "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", - "optional": undefined, + "optionalCompletion": undefined, "sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", "showLink": true, "title": "Title of Sequence", diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 633b11c20c..a35e984887 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -136,7 +136,7 @@ export function normalizeOutlineBlocks(courseId, blocks) { title: block.display_name, resumeBlock: block.resume_block, sequenceIds: block.children || [], - optional: block.optional_completion, + optionalCompletion: block.optional_completion, }; break; @@ -153,7 +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_completion, + optionalCompletion: block.optional_completion, }; break; diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx index 80a45d63c5..a330903a34 100644 --- a/src/course-home/outline-tab/Section.jsx +++ b/src/course-home/outline-tab/Section.jsx @@ -27,7 +27,7 @@ const Section = ({ complete, sequenceIds, title, - optional, + optionalCompletion, } = section; const { courseBlocks: { @@ -75,7 +75,7 @@ const Section = ({
{title} - + , {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 59c28299f2..904305a275 100644 --- a/src/course-home/outline-tab/SequenceLink.jsx +++ b/src/course-home/outline-tab/SequenceLink.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; +import { Badge } from '@edx/paragon'; import { FormattedMessage, FormattedTime, @@ -33,7 +34,7 @@ const SequenceLink = ({ due, showLink, title, - optional, + optionalCompletion, } = sequence; const { userTimezone, @@ -129,12 +130,14 @@ const SequenceLink = ({ , {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)} + {optionalCompletion && ( + + {intl.formatMessage(messages.optionalCompletion)} + + )}
- - {optional ? intl.formatMessage(messages.optionalCompletion) : ''} - {due ? dueDateMessage : noDueDateMessage} diff --git a/src/course-home/outline-tab/messages.js b/src/course-home/outline-tab/messages.js index d3b0ac5e5c..49dd2f97aa 100644 --- a/src/course-home/outline-tab/messages.js +++ b/src/course-home/outline-tab/messages.js @@ -102,7 +102,7 @@ const messages = defineMessages({ optionalCompletion: { id: 'learning.outline.optionalBlock', defaultMessage: 'Optional', - description: 'Used as a label to indicate that a section, sequence, or unit is optional.', + description: 'Used as a label to indicate that a section or sequence is optional.', }, proctoringInfoPanel: { id: 'learning.proctoringPanel.header', diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx index 37cc3c8a1c..b75ada6a50 100644 --- a/src/course-home/progress-tab/ProgressTab.test.jsx +++ b/src/course-home/progress-tab/ProgressTab.test.jsx @@ -1379,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(); + }); + }); }); diff --git a/src/courseware/course/sequence/Unit.jsx b/src/courseware/course/sequence/Unit.jsx index 415f4037a3..cd6dad2659 100644 --- a/src/courseware/course/sequence/Unit.jsx +++ b/src/courseware/course/sequence/Unit.jsx @@ -143,7 +143,7 @@ const Unit = ({ return (

{unit.title}

- +

{intl.formatMessage(messages.headerPlaceholder)}