From bde0a80cf0284a59deab975b33a64ec03ce57340 Mon Sep 17 00:00:00 2001 From: Dillon Dumesnil Date: Mon, 26 Apr 2021 07:54:12 -0700 Subject: [PATCH] fix: A couple of fixes for lilac (#422) * fix: pass username into proctoring info panel (#406) Pass a username into the proctoring info panel, allowing staff to view a specific learner's onboarding status while masquerading. * fix(i18n): update translations * AA-720: Progress Tab Course Completion chart (#407) * chore(deps): update dependency codecov to v3.8.1 * [REV-2127] feat: update gated content lock screen to Value Prop designs (#394) * fix(i18n): update translations * fix: allow media access through unit iframe (#412) Set the `allow` attribute of the unit iframe to allow access to camera, MIDI, location, and encrpyted media. Access to these features was implicitly allowed in older browser versions. However, in the current versions of at least Chromium and Firefox, iframed content must be explicitly granted the ability to request media access. This fixes a bug where content requiring microphone access did not work in the Learning MFE. TNL-7675 * fix: AA-738: Switch our use of FormattedTime to use hourCycle (#418) We had a bug reported where learners were seeing a due date like March 24, 24:59 instead of March 25, 00:59. This is a bug that only shows up in Chrome. The hour12 flag overrides the hourCycle flag so we are just going to swap the two. h23 means a 24 hour format ranging from 0-23 (there also exists a h24 option which goes from 1-24). See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat for any additional details on the options. * feat: Switch to default values for 12 vs 24 hour time. (#420) Our current version on react-intl doesn't support hourCycle anyway and after speaking to product, we feel comfortable with letting it default based on locale. * fix: AA-663: Update header text for CourseCompletion If the marketing url is not set, we shouldn't have a message about sharing. Co-authored-by: Bianca Severino Co-authored-by: edX Transifex Bot Co-authored-by: Carla Duarte Co-authored-by: Renovate Bot Co-authored-by: stvn Co-authored-by: Diane Kaplan Co-authored-by: Kyle McCormick --- package-lock.json | 14 +- package.json | 2 +- src/course-home/data/__factories__/index.js | 1 + .../__factories__/progressTabData.factory.js | 71 +++++++++ src/course-home/data/api.js | 7 +- src/course-home/outline-tab/OutlineTab.jsx | 2 + src/course-home/outline-tab/SequenceLink.jsx | 1 - .../course-end-alert/CourseEndAlert.jsx | 1 - .../course-start-alert/CourseStartAlert.jsx | 1 - .../widgets/ProctoringInfoPanel.jsx | 9 +- .../progress-tab/ProgressTab.test.jsx | 85 ++++++++++ .../CompleteDonutSegment.jsx | 77 +++++++++ .../CompletionDonutChart.jsx | 65 ++++++++ .../CompletionDonutChart.scss | 74 +++++++++ .../course-completion/CourseCompletion.jsx | 46 +++--- .../IncompleteDonutSegment.jsx | 55 +++++++ .../course-completion/LockedDonutSegment.jsx | 72 +++++++++ .../course-completion/messages.js | 42 +++++ .../grades/detailed-grades/DetailedGrades.jsx | 6 +- .../grade-summary/AssignmentTypeCell.jsx | 2 +- .../grade-summary/GradeSummaryHeader.jsx | 2 +- .../progress-tab/grades/messages.js | 4 + .../course/course-exit/CourseCelebration.jsx | 4 +- src/courseware/course/course-exit/messages.js | 10 +- src/courseware/course/sequence/Unit.jsx | 17 +- .../sequence/lock-paywall/LockPaywall.jsx | 149 +++++++++++++++--- .../sequence/lock-paywall/LockPaywall.scss | 12 ++ src/i18n/messages/ar.json | 16 +- src/i18n/messages/es_419.json | 32 ++-- src/i18n/messages/fr.json | 16 +- src/i18n/messages/zh_CN.json | 16 +- src/index.scss | 1 + 32 files changed, 824 insertions(+), 88 deletions(-) create mode 100644 src/course-home/data/__factories__/progressTabData.factory.js create mode 100644 src/course-home/progress-tab/ProgressTab.test.jsx create mode 100644 src/course-home/progress-tab/course-completion/CompleteDonutSegment.jsx create mode 100644 src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx create mode 100644 src/course-home/progress-tab/course-completion/CompletionDonutChart.scss create mode 100644 src/course-home/progress-tab/course-completion/IncompleteDonutSegment.jsx create mode 100644 src/course-home/progress-tab/course-completion/LockedDonutSegment.jsx create mode 100644 src/course-home/progress-tab/course-completion/messages.js create mode 100644 src/courseware/course/sequence/lock-paywall/LockPaywall.scss diff --git a/package-lock.json b/package-lock.json index d389fa63..20b78564 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5771,22 +5771,22 @@ "dev": true }, "codecov": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.7.2.tgz", - "integrity": "sha512-fmCjAkTese29DUX3GMIi4EaKGflHa4K51EoMc29g8fBHawdk/+KEq5CWOeXLdd9+AT7o1wO4DIpp/Z1KCqCz1g==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.8.1.tgz", + "integrity": "sha512-Qm7ltx1pzLPsliZY81jyaQ80dcNR4/JpcX0IHCIWrHBXgseySqbdbYfkdiXd7o/xmzQpGRVCKGYeTrHUpn6Dcw==", "dev": true, "requires": { "argv": "0.0.2", "ignore-walk": "3.0.3", - "js-yaml": "3.13.1", + "js-yaml": "3.14.0", "teeny-request": "6.0.1", "urlgrey": "0.4.4" }, "dependencies": { "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", diff --git a/package.json b/package.json index 17710f1a..7099a250 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@testing-library/react": "10.3.0", "@testing-library/user-event": "12.0.17", "axios-mock-adapter": "1.18.2", - "codecov": "3.7.2", + "codecov": "3.8.1", "es-check": "5.1.4", "glob": "7.1.6", "husky": "3.1.0", diff --git a/src/course-home/data/__factories__/index.js b/src/course-home/data/__factories__/index.js index a2680575..a620ad5d 100644 --- a/src/course-home/data/__factories__/index.js +++ b/src/course-home/data/__factories__/index.js @@ -1,3 +1,4 @@ import './courseHomeMetadata.factory'; import './datesTabData.factory'; import './outlineTabData.factory'; +import './progressTabData.factory'; diff --git a/src/course-home/data/__factories__/progressTabData.factory.js b/src/course-home/data/__factories__/progressTabData.factory.js new file mode 100644 index 00000000..5a508d78 --- /dev/null +++ b/src/course-home/data/__factories__/progressTabData.factory.js @@ -0,0 +1,71 @@ +import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies + +// Sample data helpful when developing & testing, to see a variety of configurations. +// This set of data may not be realistic, but it is intended to demonstrate many UI results. +Factory.define('progressTabData') + .attrs({ + certificate_data: null, + completion_summary: { + complete_count: 1, + incomplete_count: 1, + locked_count: 0, + }, + course_grade: { + percent: 0, + is_passing: false, + }, + section_scores: [ + { + display_name: 'First section', + subsections: [ + { + assignment_type: 'Homework', + display_name: 'First subsection', + has_graded_assignment: true, + num_points_earned: 0, + num_points_possible: 1, + percent_graded: 0.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection', + }, + ], + }, + { + display_name: 'Second section', + subsections: [ + { + assignment_type: 'Homework', + display_name: 'Second subsection', + has_graded_assignment: true, + num_points_earned: 1, + num_points_possible: 1, + percent_graded: 1.0, + show_correctness: 'always', + show_grades: true, + url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection', + }, + ], + }, + ], + enrollment_mode: 'audit', + grading_policy: { + assignment_policies: [ + { + num_droppable: 1, + short_label: 'HW', + type: 'Homework', + weight: 1, + }, + ], + grade_range: { + pass: 0.75, + }, + }, + studio_url: 'http://studio.edx.org/settings/grading/course-v1:edX+Test+run', + verification_data: { + link: null, + status: 'none', + status_date: null, + }, + }); diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 1e7104d8..4ac3268d 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -142,8 +142,11 @@ export async function getProgressTabData(courseId) { } } -export async function getProctoringInfoData(courseId) { - const url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?course_id=${encodeURIComponent(courseId)}`; +export async function getProctoringInfoData(courseId, username) { + let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?course_id=${encodeURIComponent(courseId)}`; + if (username) { + url += `&username=${encodeURIComponent(username)}`; + } try { const { data } = await getAuthenticatedHttpClient().get(url); return data; diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 1b82f854..538c21df 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -39,6 +39,7 @@ function OutlineTab({ intl }) { const { org, title, + username, } = useModel('courseHomeMeta', courseId); const { @@ -204,6 +205,7 @@ function OutlineTab({ intl }) {
{courseGoalToDisplay && goalOptions && goalOptions.length > 0 && ( { - getProctoringInfoData(courseId) + getProctoringInfoData(courseId, username) .then( response => { if (response) { @@ -172,7 +172,12 @@ function ProctoringInfoPanel({ courseId, intl }) { ProctoringInfoPanel.propTypes = { courseId: PropTypes.string.isRequired, + username: PropTypes.string, intl: intlShape.isRequired, }; +ProctoringInfoPanel.defaultProps = { + username: null, +}; + export default injectIntl(ProctoringInfoPanel); diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx new file mode 100644 index 00000000..416423cb --- /dev/null +++ b/src/course-home/progress-tab/ProgressTab.test.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Factory } from 'rosie'; +import { getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import MockAdapter from 'axios-mock-adapter'; + +import { + initializeMockApp, logUnhandledRequests, render, screen, act, +} from '../../setupTest'; +import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils'; +import * as thunks from '../data/thunks'; +import initializeStore from '../../store'; +import ProgressTab from './ProgressTab'; + +initializeMockApp(); +jest.mock('@edx/frontend-platform/analytics'); + +describe('Progress Tab', () => { + let axiosMock; + + const courseId = 'course-v1:edX+Test+run'; + let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`; + courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl); + const progressUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/${courseId}`; + + const store = initializeStore(); + const defaultMetadata = Factory.build('courseHomeMetadata', { id: courseId }); + const defaultTabData = Factory.build('progressTabData'); + + function setTabData(attributes, options) { + const progressTabData = Factory.build('progressTabData', attributes, options); + axiosMock.onGet(progressUrl).reply(200, progressTabData); + } + + async function fetchAndRender() { + await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch); + await act(async () => render(, { store })); + } + + beforeEach(async () => { + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + + // Set defaults for network requests + axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata); + axiosMock.onGet(progressUrl).reply(200, defaultTabData); + + logUnhandledRequests(axiosMock); + }); + + describe('Grade Summary', () => { + it('renders Grade Summary table when assignment policies are populated', async () => { + await fetchAndRender(); + expect(screen.getByText('Grade summary')).toBeInTheDocument(); + }); + + it('does not render Grade Summary when assignment policies are not populated', async () => { + setTabData({ + grading_policy: { + assignment_policies: [], + }, + }); + await fetchAndRender(); + expect(screen.queryByText('Grade summary')).not.toBeInTheDocument(); + }); + }); + + describe('Detailed Grades', () => { + it('renders Detailed Grades table when section scores are populated', async () => { + await fetchAndRender(); + expect(screen.getByText('Detailed grades')).toBeInTheDocument(); + + expect(screen.getByRole('link', { name: 'First subsection' })); + expect(screen.getByRole('link', { name: 'Second subsection' })); + }); + + it('render message when section scores are not populated', async () => { + setTabData({ + section_scores: [], + }); + await fetchAndRender(); + expect(screen.getByText('Detailed grades')).toBeInTheDocument(); + expect(screen.getByText('You currently have no graded problem scores.')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/course-home/progress-tab/course-completion/CompleteDonutSegment.jsx b/src/course-home/progress-tab/course-completion/CompleteDonutSegment.jsx new file mode 100644 index 00000000..03d17e47 --- /dev/null +++ b/src/course-home/progress-tab/course-completion/CompleteDonutSegment.jsx @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { OverlayTrigger, Popover } from '@edx/paragon'; + +import messages from './messages'; + +function CompleteDonutSegment({ completePercentage, intl, lockedPercentage }) { + const [showCompletePopover, setShowCompletePopover] = useState(false); + + const completeSegmentOffset = (3.6 * completePercentage) / 8; + let completeTooltipDegree = completePercentage < 100 ? -completeSegmentOffset : 0; + + const lockedSegmentOffset = lockedPercentage - 75; + if (lockedPercentage > 0) { + completeTooltipDegree = (lockedSegmentOffset + completePercentage) * -3.6 + 90 + completeSegmentOffset; + } + + return ( + setShowCompletePopover(false)} + onFocus={() => setShowCompletePopover(true)} + tabIndex="-1" + > + + + {/* Tooltip */} + + + {/* Segment dividers */} + {lockedPercentage > 0 && lockedPercentage < 100 && ( + + )} + {completePercentage < 100 && lockedPercentage < 100 && lockedPercentage + completePercentage === 100 && ( + + )} + + ); +} + +CompleteDonutSegment.propTypes = { + completePercentage: PropTypes.number.isRequired, + intl: intlShape.isRequired, + lockedPercentage: PropTypes.number.isRequired, +}; + +export default injectIntl(CompleteDonutSegment); diff --git a/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx new file mode 100644 index 00000000..ddeb67e9 --- /dev/null +++ b/src/course-home/progress-tab/course-completion/CompletionDonutChart.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useModel } from '../../../generic/model-store'; + +import CompleteDonutSegment from './CompleteDonutSegment'; +import IncompleteDonutSegment from './IncompleteDonutSegment'; +import LockedDonutSegment from './LockedDonutSegment'; +import messages from './messages'; + +function CompletionDonutChart({ intl }) { + const { + courseId, + } = useSelector(state => state.courseHome); + + const { + completionSummary: { + completeCount, + incompleteCount, + lockedCount, + }, + } = useModel('progress', courseId); + + const numTotalUnits = completeCount + incompleteCount + lockedCount; + const completePercentage = Number(((completeCount / numTotalUnits) * 100).toFixed(0)); + const lockedPercentage = Number(((lockedCount / numTotalUnits) * 100).toFixed(0)); + const incompletePercentage = 100 - completePercentage - lockedPercentage; + + return ( + <> + +
+ {intl.formatMessage(messages.percentComplete, { percent: completePercentage })} + {intl.formatMessage(messages.percentIncomplete, { percent: incompletePercentage })} + {lockedPercentage > 0 && ( + <> + {intl.formatMessage(messages.percentLocked, { percent: lockedPercentage })} + + )} +
+ + ); +} + +CompletionDonutChart.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(CompletionDonutChart); diff --git a/src/course-home/progress-tab/course-completion/CompletionDonutChart.scss b/src/course-home/progress-tab/course-completion/CompletionDonutChart.scss new file mode 100644 index 00000000..e8abfc09 --- /dev/null +++ b/src/course-home/progress-tab/course-completion/CompletionDonutChart.scss @@ -0,0 +1,74 @@ +.donut rect { + fill: transparent; + width: 4px; + height: 4px; + transform-origin: center; +} + +.donut-chart-label { + font: { + family: $font-family-sans-serif; + size: .2rem; + weight: $font-weight-normal; + } + text-anchor: middle; +} + +.donut-chart-number { + font: { + family: $font-family-monospace; + size: .5rem; + weight: $font-weight-bold; + } + line-height: 1rem; + text-anchor: middle; + -moz-transform: translateY(-0.6em); + -ms-transform: translateY(-0.6em); + -webkit-transform: translateY(-0.6em); + transform: translateY(-0.6em); +} + +.donut-chart-text { + fill: $primary-500; + -moz-transform: translateY(0.25em); + -ms-transform: translateY(0.25em); + -webkit-transform: translateY(0.25em); + transform: translateY(0.25em); +} + +.donut-ring, .donut-segment { + stroke-width: 6px; + fill: transparent; +} + +.donut-segment-group { + cursor: pointer; + pointer-events: visibleStroke; + + &:focus { + outline: none; + + circle { + stroke-width: 7px; + } + } +} + +.donut-ring, .donut-segment, .donut-hole { + &.complete-stroke { + stroke: $info-500; + } + + &.divider-stroke { + stroke-width: 7px; + stroke: white; + } + + &.incomplete-stroke { + stroke: $light-300; + } + + &.locked-stroke { + stroke: $primary-500; + } +} diff --git a/src/course-home/progress-tab/course-completion/CourseCompletion.jsx b/src/course-home/progress-tab/course-completion/CourseCompletion.jsx index 2e19a78b..c4814350 100644 --- a/src/course-home/progress-tab/course-completion/CourseCompletion.jsx +++ b/src/course-home/progress-tab/course-completion/CourseCompletion.jsx @@ -1,35 +1,29 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import { useModel } from '../../../generic/model-store'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -function CourseCompletion() { - // TODO: AA-720 - const { - courseId, - } = useSelector(state => state.courseHome); - - const { - completionSummary: { - completeCount, - incompleteCount, - lockedCount, - }, - } = useModel('progress', courseId); - - const total = completeCount + incompleteCount + lockedCount; - const completePercentage = ((completeCount / total) * 100).toFixed(0); - const incompletePercentage = ((incompleteCount / total) * 100).toFixed(0); - const lockedPercentage = ((lockedCount / total) * 100).toFixed(0); +import CompletionDonutChart from './CompletionDonutChart'; +import messages from './messages'; +function CourseCompletion({ intl }) { return (
-

Course completion

-

This represents how much course content you have completed.

- Complete: {completePercentage}% - Incomplete: {incompletePercentage}% - Locked: {lockedPercentage}% +
+
+

{intl.formatMessage(messages.courseCompletion)}

+

+ {intl.formatMessage(messages.completionBody)} +

+
+
+ +
+
); } -export default CourseCompletion; +CourseCompletion.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(CourseCompletion); diff --git a/src/course-home/progress-tab/course-completion/IncompleteDonutSegment.jsx b/src/course-home/progress-tab/course-completion/IncompleteDonutSegment.jsx new file mode 100644 index 00000000..33ebec90 --- /dev/null +++ b/src/course-home/progress-tab/course-completion/IncompleteDonutSegment.jsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { OverlayTrigger, Popover } from '@edx/paragon'; + +import messages from './messages'; + +function IncompleteDonutSegment({ incompletePercentage, intl }) { + const [showIncompletePopover, setShowIncompletePopover] = useState(false); + + const incompleteSegmentOffset = (3.6 * incompletePercentage) / 16; + const incompleteTooltipDegree = incompletePercentage < 100 ? incompleteSegmentOffset : 0; + + return ( + setShowIncompletePopover(false)} + onFocus={() => setShowIncompletePopover(true)} + tabIndex="-1" + > + + + {/* Tooltip */} + + + ); +} + +IncompleteDonutSegment.propTypes = { + incompletePercentage: PropTypes.number.isRequired, + intl: intlShape.isRequired, +}; + +export default injectIntl(IncompleteDonutSegment); diff --git a/src/course-home/progress-tab/course-completion/LockedDonutSegment.jsx b/src/course-home/progress-tab/course-completion/LockedDonutSegment.jsx new file mode 100644 index 00000000..80f03db4 --- /dev/null +++ b/src/course-home/progress-tab/course-completion/LockedDonutSegment.jsx @@ -0,0 +1,72 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +import { OverlayTrigger, Popover } from '@edx/paragon'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; + +import messages from './messages'; + +function LockedDonutSegment({ intl, lockedPercentage }) { + const [showLockedPopover, setShowLockedPopover] = useState(false); + + if (!lockedPercentage > 0) { + return null; + } + + const iconDegree = lockedPercentage > 8 ? (3.6 * lockedPercentage) / 8 : ((3.6 * lockedPercentage) / 5) * 2; + + return ( + setShowLockedPopover(false)} + onFocus={() => setShowLockedPopover(true)} + tabIndex="-1" + > + + + {/* Tooltip */} + + + ); +} + +LockedDonutSegment.propTypes = { + intl: intlShape.isRequired, + lockedPercentage: PropTypes.number.isRequired, +}; + +export default injectIntl(LockedDonutSegment); diff --git a/src/course-home/progress-tab/course-completion/messages.js b/src/course-home/progress-tab/course-completion/messages.js new file mode 100644 index 00000000..8d66f4f7 --- /dev/null +++ b/src/course-home/progress-tab/course-completion/messages.js @@ -0,0 +1,42 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + donutLabel: { + id: 'progress.completion.donut.label', + defaultMessage: 'completed', + }, + 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.', + }, + completeContentTooltip: { + id: 'progress.completion.tooltip.locked', + defaultMessage: 'Content that you have completed.', + }, + courseCompletion: { + id: 'progress.completion.header', + defaultMessage: 'Course completion', + }, + incompleteContentTooltip: { + id: 'progress.completion.tooltip', + defaultMessage: 'Content that you have access to and have not completed.', + }, + lockedContentTooltip: { + id: 'progress.completion.tooltip.complete', + defaultMessage: 'Content that is locked and available only to those who upgrade.', + }, + percentComplete: { + id: 'progress.completion.donut.percentComplete', + defaultMessage: 'You have completed {percent}% of content in this course.', + }, + percentIncomplete: { + id: 'progress.completion.donut.percentIncomplete', + defaultMessage: 'You have not completed {percent}% of content in this course that you have access to.', + }, + percentLocked: { + id: 'progress.completion.donut.percentLocked', + defaultMessage: '{percent}% of content in this course is locked and available only for those who upgrade.', + }, +}); + +export default messages; diff --git a/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx b/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx index 8ed819bc..621ac48d 100644 --- a/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx +++ b/src/course-home/progress-tab/grades/detailed-grades/DetailedGrades.jsx @@ -31,15 +31,15 @@ function DetailedGrades({ intl }) { ); return ( -
+

{intl.formatMessage(messages.detailedGrades)}

{hasSectionScores && ( )} {!hasSectionScores && ( -

You currently have no graded problem scores.

+

{intl.formatMessage(messages.detailedGradesEmpty)}

)} -

+

{intl.formatMessage(messages.gradeSummary)}

diff --git a/src/course-home/progress-tab/grades/messages.js b/src/course-home/progress-tab/grades/messages.js index 9881ccaf..ddaca03c 100644 --- a/src/course-home/progress-tab/grades/messages.js +++ b/src/course-home/progress-tab/grades/messages.js @@ -17,6 +17,10 @@ const messages = defineMessages({ id: 'progress.detailedGrades', defaultMessage: 'Detailed grades', }, + detailedGradesEmpty: { + id: 'progress.detailedGrades.emptyTable', + defaultMessage: 'You currently have no graded problem scores.', + }, footnotesTitle: { id: 'progress.footnotes.title', defaultMessage: 'Grade summary footnotes', diff --git a/src/courseware/course/course-exit/CourseCelebration.jsx b/src/courseware/course/course-exit/CourseCelebration.jsx index 66491b2f..f812bcca 100644 --- a/src/courseware/course/course-exit/CourseCelebration.jsx +++ b/src/courseware/course/course-exit/CourseCelebration.jsx @@ -45,6 +45,7 @@ function CourseCelebration({ intl }) { certificateData, end, linkedinAddToProfileUrl, + marketingUrl, offer, org, relatedPrograms, @@ -287,7 +288,8 @@ function CourseCelebration({ intl }) { {intl.formatMessage(messages.congratulationsHeader)}
- {intl.formatMessage(messages.shareHeader)} + {intl.formatMessage(messages.completedCourseHeader)} + {marketingUrl && ` ${intl.formatMessage(messages.shareMessage)}`} import('./lock-paywall')); +/** + * Feature policy for iframe, allowing access to certain courseware-related media. + * + * We must use the wildcard (*) origin for each feature, as courseware content + * may be embedded in external iframes. Notably, xblock-lti-consumer is a popular + * block that iframes external course content. + + * This policy was selected in conference with the edX Security Working Group. + * Changes to it should be vetted by them (security@edx.org). + */ +const IFRAME_FEATURE_POLICY = ( + 'microphone *; camera *; midi *; geolocation *; encrypted-media *' +); + /** * We discovered an error in Firefox where - upon iframe load - React would cease to call any * useEffect hooks until the user interacts with the page again. This is particularly confusing @@ -155,7 +169,7 @@ function Unit({ : (