diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 79184b5f..031bb273 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -130,6 +130,20 @@ 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=${courseId}`; + try { + const { data } = await getAuthenticatedHttpClient().get(url); + return data; + } catch (error) { + const { httpErrorStatus } = error && error.customAttributes; + if (httpErrorStatus === 404) { + return {}; + } + throw error; + } +} + export async function getOutlineTabData(courseId) { const url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/${courseId}`; let { tabData } = {}; diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index a1b9d066..b349e45e 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -25,6 +25,7 @@ import useOfferAlert from '../../alerts/offer-alert'; import usePrivateCourseAlert from '../../alerts/private-course-alert'; import { useModel } from '../../generic/model-store'; import WelcomeMessage from './widgets/WelcomeMessage'; +import ProctoringInfoPanel from './widgets/ProctoringInfoPanel'; function OutlineTab({ intl }) { const { @@ -162,6 +163,9 @@ function OutlineTab({ intl }) { {rootCourseId && (
+ {courseGoalToDisplay && goalOptions.length > 0 && ( { const enrollmentUrl = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`; const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/save_course_goal`; const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/${courseId}`; + const proctoringInfoUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?course_id=${courseId}`; const store = initializeStore(); const defaultMetadata = Factory.build('courseHomeMetadata', { courseId }); @@ -42,7 +43,7 @@ describe('Outline Tab', () => { async function fetchAndRender() { await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch); - render(, { store }); + await act(async () => render(, { store })); } beforeEach(async () => { @@ -53,6 +54,7 @@ describe('Outline Tab', () => { axiosMock.onPost(enrollmentUrl).reply(200, {}); axiosMock.onPost(goalUrl).reply(200, { header: 'Success' }); axiosMock.onGet(outlineUrl).reply(200, defaultTabData); + axiosMock.onGet(proctoringInfoUrl).reply(200, { onboarding_status: 'created', onboarding_link: 'test' }); logUnhandledRequests(axiosMock); }); @@ -473,4 +475,33 @@ describe('Outline Tab', () => { }); }); }); + + describe('Proctoring Info Panel', () => { + it('appears', async () => { + await fetchAndRender(); + await screen.findByText('This course contains proctored exams'); + expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).toBeInTheDocument(); + }); + + it('appears for verified', async () => { + axiosMock.onGet(proctoringInfoUrl).reply(200, { onboarding_status: 'verified', onboarding_link: 'test' }); + await fetchAndRender(); + await screen.findByText('This course contains proctored exams'); + expect(screen.queryByRole('link', { name: 'Complete Onboarding' })).not.toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).toBeInTheDocument(); + }); + + it('appears for rejected', async () => { + axiosMock.onGet(proctoringInfoUrl).reply(200, { onboarding_status: 'rejected', onboarding_link: 'test' }); + await fetchAndRender(); + await screen.findByText('This course contains proctored exams'); + expect(screen.queryByRole('link', { name: 'Complete Onboarding' })).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).toBeInTheDocument(); + }); + + it('does not appear for 404', async () => { + axiosMock.onGet(proctoringInfoUrl).reply(404); + expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/course-home/outline-tab/messages.js b/src/course-home/outline-tab/messages.js index f0c839aa..b7ddb66f 100644 --- a/src/course-home/outline-tab/messages.js +++ b/src/course-home/outline-tab/messages.js @@ -112,6 +112,78 @@ const messages = defineMessages({ defaultMessage: 'Welcome to', description: 'This precedes the title of the course', }, + proctoringInfoPanel: { + id: 'learning.proctoringPanel.header', + defaultMessage: 'This course contains proctored exams', + }, + notStartedProctoringStatus: { + id: 'learning.proctoringPanel.status.notStarted', + defaultMessage: 'Not Started', + }, + startedProctoringStatus: { + id: 'learning.proctoringPanel.status.started', + defaultMessage: 'Started', + }, + submittedProctoringStatus: { + id: 'learning.proctoringPanel.status.submitted', + defaultMessage: 'Submitted', + }, + verifiedProctoringStatus: { + id: 'learning.proctoringPanel.status.verified', + defaultMessage: 'Verified', + }, + rejectedProctoringStatus: { + id: 'learning.proctoringPanel.status.rejected', + defaultMessage: 'Rejected', + }, + errorProctoringStatus: { + id: 'learning.proctoringPanel.status.error', + defaultMessage: 'Error', + }, + proctoringCurrentStatus: { + id: 'learning.proctoringPanel.status', + defaultMessage: 'Current Onboarding Status:', + }, + notStartedProctoringMessage: { + id: 'learning.proctoringPanel.message.notStarted', + defaultMessage: 'You have not started your onboarding exam.', + }, + startedProctoringMessage: { + id: 'learning.proctoringPanel.message.started', + defaultMessage: 'You have started your onboarding exam.', + }, + submittedProctoringMessage: { + id: 'learning.proctoringPanel.message.submitted', + defaultMessage: 'You have submitted your onboarding exam.', + }, + verifiedProctoringMessage: { + id: 'learning.proctoringPanel.message.verified', + defaultMessage: 'You can now take proctored exams in this course.', + }, + rejectedProctoringMessage: { + id: 'learning.proctoringPanel.message.rejected', + defaultMessage: 'Your onboarding exam has been rejected. Please retry onboarding.', + }, + errorProctoringMessage: { + id: 'learning.proctoringPanel.message.error', + defaultMessage: 'An error has occurred during your onboarding exam. Please retry onboarding.', + }, + proctoringPanelGeneralInfo: { + id: 'learning.proctoringPanel.generalInfo', + defaultMessage: 'You must complete the onboarding process prior to taking any proctored exam. ', + }, + proctoringPanelGeneralTime: { + id: 'learning.proctoringPanel.generalTime', + defaultMessage: 'Onboarding profile review, including identity verification, can take 2+ business days.', + }, + proctoringOnboardingButton: { + id: 'learning.proctoringPanel.onboardingButton', + defaultMessage: 'Complete Onboarding', + }, + proctoringReviewRequirementsButton: { + id: 'learning.proctoringPanel.reviewRequirementsButton', + defaultMessage: 'Review instructions and system requirements', + }, }); export default messages; diff --git a/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx b/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx new file mode 100644 index 00000000..915bf100 --- /dev/null +++ b/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx @@ -0,0 +1,99 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; + +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Button } from '@edx/paragon'; + +import messages from '../messages'; +import { getProctoringInfoData } from '../../data/api'; + +function ProctoringInfoPanel({ courseId, intl }) { + const [status, setStatus] = useState(''); + const [link, setLink] = useState(''); + const [readableStatus, setReadableStatus] = useState(''); + + function getReadableStatusClass(examStatus) { + let readableClass = ''; + if (['created', 'download_software_clicked', 'ready_to_start'].includes(examStatus) || !examStatus) { + readableClass = 'notStarted'; + } else if (['started', 'ready_to_submit'].includes(examStatus)) { + readableClass = 'started'; + } else if (['second_review_required', 'submitted'].includes(examStatus)) { + readableClass = 'submitted'; + } else if (['verified', 'rejected', 'error'].includes(examStatus)) { + readableClass = examStatus; + } + return readableClass; + } + + function showExamLink(examStatus) { + const NO_SHOW_STATES = ['submitted', 'second_review_required', 'verified']; + return !NO_SHOW_STATES.includes(examStatus); + } + + function getBorderClass(examStatus) { + let borderClass = ''; + if (['submitted', 'second_review_required'].includes(examStatus)) { + borderClass = 'proctoring-onboarding-submitted'; + } else if (examStatus === 'verified') { + borderClass = 'proctoring-onboarding-success'; + } + return borderClass; + } + + useEffect(() => { + getProctoringInfoData(courseId) + .then( + response => { + if (response) { + setStatus(response.onboarding_status); + setLink(response.onboarding_link); + setReadableStatus(getReadableStatusClass(response.onboarding_status)); + } + }, + ); + }, []); + + return ( + <> + { status && ( +
+

{intl.formatMessage(messages.proctoringInfoPanel)}

+
+ {readableStatus && ( + <> +

+ {intl.formatMessage(messages.proctoringCurrentStatus)} {intl.formatMessage(messages[`${readableStatus}ProctoringStatus`])} +

+

+ {intl.formatMessage(messages[`${readableStatus}ProctoringMessage`])} +

+ + )} + {(readableStatus !== 'verified') && ( + <> +

{intl.formatMessage(messages.proctoringPanelGeneralInfo)}

+

{intl.formatMessage(messages.proctoringPanelGeneralTime)}

+ + )} + {showExamLink(status) && ( + + )} + +
+
+ )} + + ); +} + +ProctoringInfoPanel.propTypes = { + courseId: PropTypes.string.isRequired, + intl: intlShape.isRequired, +}; + +export default injectIntl(ProctoringInfoPanel); diff --git a/src/course-home/outline-tab/widgets/ProctoringInfoPanel.scss b/src/course-home/outline-tab/widgets/ProctoringInfoPanel.scss new file mode 100644 index 00000000..f7bdeedf --- /dev/null +++ b/src/course-home/outline-tab/widgets/ProctoringInfoPanel.scss @@ -0,0 +1,10 @@ +.outline-sidebar-proctoring-panel { + border: 1px solid $dark-500; + border-top: 5px solid $brand-600; +} +.proctoring-onboarding-success { + border-top: 5px solid $primary-500; +} +.proctoring-onboarding-submitted { + border-top: 5px solid $dark-500; +} diff --git a/src/index.scss b/src/index.scss index 9a51cc5b..1b5902a4 100755 --- a/src/index.scss +++ b/src/index.scss @@ -366,3 +366,4 @@ @import 'course-home/dates-tab/Badge.scss'; @import 'course-home/dates-tab/Day.scss'; @import 'course-home/outline-tab/widgets/UpgradeCard.scss'; +@import 'course-home/outline-tab/widgets/ProctoringInfoPanel.scss';