diff --git a/src/core/OuterExamTimer.jsx b/src/core/OuterExamTimer.jsx index 77ecc992..425bc689 100644 --- a/src/core/OuterExamTimer.jsx +++ b/src/core/OuterExamTimer.jsx @@ -8,10 +8,9 @@ import { getLatestAttemptData } from '../data'; import { IS_STARTED_STATUS } from '../constants'; const ExamTimer = ({ courseId }) => { - const { activeAttempt } = useSelector(state => state.specialExams); + const { activeAttempt, apiErrorMsg } = useSelector(state => state.specialExams); const { authenticatedUser } = useContext(AppContext); const showTimer = !!(activeAttempt && IS_STARTED_STATUS(activeAttempt.attempt_status)); - const { apiErrorMsg } = useSelector(state => state.specialExams); const dispatch = useDispatch(); useEffect(() => { diff --git a/src/data/api.js b/src/data/api.js index 5cd62c42..a01247de 100644 --- a/src/data/api.js +++ b/src/data/api.js @@ -1,6 +1,5 @@ import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { logError } from '@edx/frontend-platform/logging'; import { ExamAction } from '../constants'; import { generateHumanizedTime } from '../helpers'; @@ -13,14 +12,14 @@ async function fetchActiveAttempt() { return activeAttemptResponse.data; } -async function fetchLatestExamAttempt(sequenceId) { +async function fetchAttemptForExamSequnceId(sequenceId) { + const attemptUrl = new URL(`${getConfig().EXAMS_BASE_URL}/api/v1/exams/attempt/latest`); // the calls the same endpoint as fetchActiveAttempt but it behaves slightly different // with an exam's section specified. The attempt for that requested exam is always returned // even if it is not 'active' (timer is not running) - const attemptUrl = new URL(`${getConfig().EXAMS_BASE_URL}/api/v1/exams/attempt/latest`); attemptUrl.searchParams.append('content_id', sequenceId); - const response = await getAuthenticatedHttpClient().get(attemptUrl.href); - return response.data; + const attemptResponse = await getAuthenticatedHttpClient().get(attemptUrl.href); + return attemptResponse.data; } export async function fetchExamAttemptsData(courseId, sequenceId) { @@ -70,25 +69,31 @@ export async function fetchLatestAttempt(courseId) { export async function pollExamAttempt(pollUrl, sequenceId) { let data; + + // sites configured with only edx-proctoring must have pollUrl set if (pollUrl) { const edxProctoringURL = new URL( `${getConfig().LMS_BASE_URL}${pollUrl}`, ); const urlResponse = await getAuthenticatedHttpClient().get(edxProctoringURL.href); data = urlResponse.data; - } else if (sequenceId && getConfig().EXAMS_BASE_URL) { - data = await fetchLatestExamAttempt(sequenceId); - - // Update dictionaries returned by edx-exams to have correct status key for legacy compatibility - if (data.attempt_status) { - data.status = data.attempt_status; - delete data.attempt_status; - } + + return data; + + // exams configured with edx-exams expect sequenceId if pollUrl is not set when viewing the exam sequence + } if (sequenceId) { + data = await fetchAttemptForExamSequnceId(sequenceId); + // outside the exam sequence, we can't get the sequenceId easily, so here we just call the last active attempt } else { - // sites configured with only edx-proctoring must have pollUrl set - // sites configured with edx-exams expect sequenceId if pollUrl is not set - logError(`pollExamAttempt recieved unexpected parameters pollUrl=${pollUrl} sequenceId=${sequenceId}`); + data = await fetchActiveAttempt(); + } + + // Update dictionaries returned by edx-exams to have correct status key for legacy compatibility + if (data.attempt_status) { + data.status = data.attempt_status; + delete data.attempt_status; } + return data; } diff --git a/src/data/redux.test.jsx b/src/data/redux.test.jsx index bffa0cde..6a036a03 100644 --- a/src/data/redux.test.jsx +++ b/src/data/redux.test.jsx @@ -990,10 +990,14 @@ describe('Data layer integration tests', () => { await api.pollExamAttempt(null, sequenceId); expect(axiosMock.history.get[0].url).toEqual(expectedUrl); }); - test('pollUrl is required if edx-exams in not enabled, an error should be logged', async () => { - mergeConfig({ EXAMS_BASE_URL: null }); - api.pollExamAttempt(null, null); - expect(loggingService.logError).toHaveBeenCalled(); + test('should call the latest attempt w/o a sequence if if neither a pollUrl or sequence id is provided', async () => { + const expectedUrl = `${getConfig().EXAMS_BASE_URL}/api/v1/exams/attempt/latest`; + axiosMock.onGet(expectedUrl).reply(200, { + time_remaining_seconds: 1739.9, + status: ExamStatus.STARTED, + }); + await api.pollExamAttempt(null); + expect(axiosMock.history.get[0].url).toEqual(expectedUrl); }); }); }); diff --git a/src/data/thunks.js b/src/data/thunks.js index c0628b7f..31759531 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -271,8 +271,6 @@ export function pollAttempt(url) { } try { - // TODO: make sure sequenceId pulled here is correct both in-exam-sequence and in outline - // test w/ timed exam const { exam } = getState().specialExams; const data = await pollExamAttempt(url, exam.content_id); if (!data) { diff --git a/src/timer/TimerProvider.jsx b/src/timer/TimerProvider.jsx index 74fbc87e..2671fe2b 100644 --- a/src/timer/TimerProvider.jsx +++ b/src/timer/TimerProvider.jsx @@ -95,15 +95,20 @@ const TimerProvider = ({ timeLimitMins, ]); + // Set deadline as a reference to timerEnds that updates when it changes + const deadline = useRef(new Date(timerEnds)); + useEffect(() => { + deadline.current = new Date(timerEnds); + }, [timerEnds]); + useEffect(() => { const timerRef = { current: true }; let timerTick = -1; - const deadline = new Date(timerEnds); const ticker = () => { timerTick++; const now = Date.now(); - const remainingTime = (deadline.getTime() - now) / 1000; + const remainingTime = (deadline.current.getTime() - now) / 1000; const secondsLeft = Math.floor(remainingTime); setTimeState(getFormattedRemainingTime(secondsLeft)); @@ -143,7 +148,6 @@ const TimerProvider = ({ timerRef.current = null; }; }, [ - timerEnds, pingInterval, workerUrl, processTimeLeft,