Skip to content

Commit

Permalink
feet: [BD-26][EDUCATOR-5761, EDUCATOR-5762] Add additional pages for …
Browse files Browse the repository at this point in the history
…proctored exams (#10)

* feat: [OeX_Proctoring-178] add mock api for fetching proctoring settings and instructions

* feat: use API instead of mocked data to get proctoring settings

* feat: add proctoring software download page + refactor footer

* feat: add user verification page

* feat: add proctored exam instructions page (when exam is in 'ready_to_start' status)

* feat: [OeX_Proctoring-178] fetch proctoring settings and add proctoring exam instructions (#14)

Co-authored-by: Ihor Romaniuk <[email protected]>
Co-authored-by: Sagirov Evgeniy <[email protected]>

* [BD-26] Cover timed exam functionality with unit and functional tests (#16)

* feat: Cover timed exam functionality with unit and functional tests

* fix: don't do pulling when exam is in 'ready_to_submit' status (#19)

* [BD-26][EDUCATOR-5791, EDUCATOR-5767] Add handling of exam API errors and support for js workers from provider (#20)

* feat: add handling of exam API errors

* feat: add support for workers from proctoring providers

* feat: get provider software download url from backend instead of building on the frontend

* fix: merge errors

* fix: fix tests

* feat: add verification url to user verification page

Co-authored-by: Ihor Romaniuk <[email protected]>
Co-authored-by: Sagirov Eugeniy <[email protected]>
Co-authored-by: Ihor Romaniuk <[email protected]>
Co-authored-by: Sagirov Evgeniy <[email protected]>
  • Loading branch information
5 people authored Jun 3, 2021
1 parent c4b2b13 commit addb8f8
Show file tree
Hide file tree
Showing 23 changed files with 788 additions and 94 deletions.
10 changes: 10 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable import/prefer-default-export */
export const ExamStatus = Object.freeze({
CREATED: 'created',
DOWNLOAD_SOFTWARE_CLICKED: 'download_software_clicked',
READY_TO_START: 'ready_to_start',
STARTED: 'started',
READY_TO_SUBMIT: 'ready_to_submit',
SUBMITTED: 'submitted',
Expand All @@ -19,4 +21,12 @@ export const ExamAction = Object.freeze({
PING: 'ping',
SUBMIT: 'submit',
ERROR: 'error',
CLICK_DOWNLOAD_SOFTWARE: 'click_download_software',
});

export const VerificationStatus = Object.freeze({
PENDING: 'pending',
MUST_REVERIFY: 'must_reverify',
APPROVED: 'approved',
EXPIRED: 'expired',
});
20 changes: 20 additions & 0 deletions src/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,28 @@ export async function endExamWithFailure(attemptId, error) {
return updateAttemptStatus(attemptId, ExamAction.ERROR, error);
}

export async function softwareDownloadAttempt(attemptId) {
return updateAttemptStatus(attemptId, ExamAction.CLICK_DOWNLOAD_SOFTWARE);
}

export async function fetchExamReviewPolicy(examId) {
const url = new URL(
`${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/review_policy/exam_id/${examId}/`,
);
const { data } = await getAuthenticatedHttpClient().get(url.href);
return data;
}

export async function fetchProctoringSettings(examId) {
const url = new URL(`${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/settings/exam_id/${examId}/`);
const { data } = await getAuthenticatedHttpClient().get(url.href);
return data;
}

export async function fetchVerificationStatus() {
const url = new URL(
`${getConfig().LMS_BASE_URL}/verify_student/status/`,
);
const { data } = await getAuthenticatedHttpClient().get(url.href);
return data;
}
2 changes: 2 additions & 0 deletions src/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export {
submitExam,
expireExam,
pollAttempt,
getVerificationData,
getExamReviewPolicy,
pingAttempt,
} from './thunks';

Expand Down
10 changes: 9 additions & 1 deletion src/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const examSlice = createSlice({
activeAttempt: null,
proctoringSettings: {},
exam: {},
verification: {},
apiErrorMsg: '',
},
reducers: {
Expand All @@ -30,6 +31,12 @@ export const examSlice = createSlice({
state.timeIsOver = true;
},
getExamId: (state) => state.examId,
setVerificationData: (state, { payload }) => {
state.verification = payload.verification;
},
setReviewPolicy: (state, { payload }) => {
state.exam.reviewPolicy = payload.policy;
},
setApiError: (state, { payload }) => {
state.apiErrorMsg = payload.errorMsg;
},
Expand All @@ -38,7 +45,8 @@ export const examSlice = createSlice({

export const {
setIsLoading, setExamState, getExamId, expireExamAttempt,
setActiveAttempt, setProctoringSettings, setApiError,
setActiveAttempt, setProctoringSettings, setVerificationData,
setReviewPolicy, setApiError,
} = examSlice.actions;

export default examSlice.reducer;
46 changes: 43 additions & 3 deletions src/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ import {
submitAttempt,
pollExamAttempt,
fetchProctoringSettings,
softwareDownloadAttempt,
fetchVerificationStatus,
fetchExamReviewPolicy,
} from './api';
import { isEmpty } from '../helpers';
import {
setIsLoading,
setExamState,
expireExamAttempt,
setActiveAttempt,
setApiError,
setProctoringSettings,
setVerificationData,
setReviewPolicy,
setApiError,
} from './slice';
import { ExamStatus } from '../constants';
import { workerPromiseForEventNames, pingApplication } from './messages/handlers';
Expand Down Expand Up @@ -113,6 +118,8 @@ export function startProctoringExam() {
await updateAttemptAfter(
exam.course_id, exam.content_id, createExamAttempt(exam.id, false, true),
)(dispatch);
const proctoringSettings = await fetchProctoringSettings(exam.id);
dispatch(setProctoringSettings({ proctoringSettings }));
};
}

Expand Down Expand Up @@ -168,7 +175,7 @@ export function stopExam() {
};
}

export function continueExam() {
export function continueExam(noLoading = true) {
return async (dispatch, getState) => {
const { exam } = getState().examState;
const attemptId = exam.attempt.attempt_id;
Expand All @@ -181,7 +188,7 @@ export function continueExam() {
return;
}
await updateAttemptAfter(
exam.course_id, exam.content_id, continueAttempt(attemptId), true,
exam.course_id, exam.content_id, continueAttempt(attemptId), noLoading,
)(dispatch);
};
}
Expand Down Expand Up @@ -258,3 +265,36 @@ export function pingAttempt(timeoutInSeconds, workerUrl) {
));
};
}

export function startProctoringSoftwareDownload() {
return async (dispatch, getState) => {
const { exam } = getState().examState;
const attemptId = exam.attempt.attempt_id;
if (!attemptId) {
logError('Failed to start downloading proctoring software. No attempt id.');
return;
}
await updateAttemptAfter(
exam.course_id, exam.content_id, softwareDownloadAttempt(attemptId),
)(dispatch);
};
}

export function getVerificationData() {
return async (dispatch) => {
const data = await fetchVerificationStatus();
dispatch(setVerificationData({ verification: data }));
};
}

export function getExamReviewPolicy() {
return async (dispatch, getState) => {
const { exam } = getState().examState;
if (!exam.id) {
logError('Failed to fetch exam review policy. No exam id.');
return;
}
const data = await fetchExamReviewPolicy(exam.id);
dispatch(setReviewPolicy({ policy: data.review_policy }));
};
}
1 change: 1 addition & 0 deletions src/exam/ExamWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const ExamWrapper = ({ children, ...props }) => {
const loadInitialData = async () => {
await state.getExamAttemptsData(courseId, sequence.id);
state.getProctoringSettings();
state.getVerificationData();
};

useEffect(() => {
Expand Down
4 changes: 4 additions & 0 deletions src/exam/ExamWrapper.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ store.getState = () => ({
examState: {
isLoading: false,
activeAttempt: null,
verification: {
status: 'none',
can_verify: true,
},
exam: {
time_limit_mins: 30,
attempt: {},
Expand Down
8 changes: 8 additions & 0 deletions src/instructions/Instructions.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ describe('SequenceExamWrapper', () => {
examState: {
isLoading: false,
activeAttempt: null,
verification: {
status: 'none',
can_verify: true,
},
exam: {
time_limit_mins: 30,
attempt: {},
Expand All @@ -43,6 +47,10 @@ describe('SequenceExamWrapper', () => {
store.getState = () => ({
examState: {
isLoading: false,
verification: {
status: 'none',
can_verify: true,
},
activeAttempt: {
attempt_status: 'started',
},
Expand Down
24 changes: 21 additions & 3 deletions src/instructions/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,40 @@ import {
SubmittedProctoredExamInstructions,
VerifiedProctoredExamInstructions,
RejectedProctoredExamInstructions,
DownloadSoftwareProctoredExamInstructions,
ReadyToStartProctoredExamInstructions,
} from './proctored_exam';
import { isEmpty } from '../helpers';
import { ExamStatus } from '../constants';
import { ExamStatus, VerificationStatus } from '../constants';
import ExamStateContext from '../context';

const Instructions = ({ children }) => {
const state = useContext(ExamStateContext);
const { attempt, is_proctored: isProctored } = state.exam;
const { exam, verification } = state;
const { attempt, is_proctored: isProctored } = exam || {};
let verificationStatus = verification.status || '';
const { verification_url: verificationUrl } = attempt || {};

// The API does not explicitly return 'expired' status, so we have to check manually.
// expires attribute is returned only for approved status, so it is safe to do this
// (meaning we won't override 'must_reverify' status for example)
if (verification.expires && new Date() > new Date(verification.expires)) {
verificationStatus = VerificationStatus.EXPIRED;
}

switch (true) {
case isEmpty(attempt):
return isProctored
? <EntranceProctoredExamInstructions />
: <StartExamInstructions />;
case attempt.attempt_status === ExamStatus.CREATED:
return <VerificationProctoredExamInstructions />;
return verificationStatus === VerificationStatus.APPROVED
? <DownloadSoftwareProctoredExamInstructions />
: <VerificationProctoredExamInstructions status={verificationStatus} verificationUrl={verificationUrl} />;
case attempt.attempt_status === ExamStatus.DOWNLOAD_SOFTWARE_CLICKED:
return <DownloadSoftwareProctoredExamInstructions />;
case attempt.attempt_status === ExamStatus.READY_TO_START:
return <ReadyToStartProctoredExamInstructions />;
case attempt.attempt_status === ExamStatus.READY_TO_SUBMIT:
return isProctored
? <SubmitProctoredExamInstructions />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useContext } from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button, Container } from '@edx/paragon';
import ExamStateContext from '../../context';
import Footer from './Footer';

const EntranceProctoredExamInstructions = () => {
const state = useContext(ExamStateContext);
Expand Down Expand Up @@ -54,19 +55,7 @@ const EntranceProctoredExamInstructions = () => {
</Button>
</p>
</Container>

<div className="footer-sequence">
<Button
data-testid="request-exam-time-button"
variant="link"
onClick={() => {}}
>
<FormattedMessage
id="exam.startExamInstructions.footerButton"
defaultMessage="About Proctored Exams"
/>
</Button>
</div>
<Footer />
</div>
);
};
Expand Down
31 changes: 31 additions & 0 deletions src/instructions/proctored_exam/Footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useContext } from 'react';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import ExamStateContext from '../../context';

const Footer = () => {
const state = useContext(ExamStateContext);
const { proctoringSettings } = state;
const { link_urls: linkUrls } = proctoringSettings;
const faqUrl = linkUrls && linkUrls.faq;

return (
<div className="footer-sequence">
{faqUrl && (
<Button
data-testid="request-exam-time-button"
variant="link"
href={faqUrl}
target="_blank"
>
<FormattedMessage
id="exam.startExamInstructions.footerButton"
defaultMessage="About Proctored Exams"
/>
</Button>
)}
</div>
);
};

export default Footer;
Loading

0 comments on commit addb8f8

Please sign in to comment.