Skip to content

Commit e19ed62

Browse files
feat: fetch exams data on the progress page
This commit adds changes to fetch the exams data associated with all subsections relevant to the progress page. Exams data is relevant to the progress page because the status of a learner's exam attempt may influence the state of their grade. This allows children of the root ProgressPage or downstream plugin slots to access this data from the Redux store.
1 parent d3235af commit e19ed62

File tree

5 files changed

+66
-3
lines changed

5 files changed

+66
-3
lines changed

src/course-home/data/api.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,24 @@ export async function searchCourseContentFromAPI(courseId, searchKeyword, option
379379

380380
return camelCaseObject(response);
381381
}
382+
383+
export async function getExamsData(courseId, sequenceId) {
384+
let url;
385+
386+
if (!getConfig().EXAMS_BASE_URL) {
387+
url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
388+
} else {
389+
url = `${getConfig().EXAMS_BASE_URL}/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}`;
390+
}
391+
392+
try {
393+
const { data } = await getAuthenticatedHttpClient().get(url);
394+
return camelCaseObject(data);
395+
} catch (error) {
396+
const { httpErrorStatus } = error && error.customAttributes;
397+
if (httpErrorStatus === 404) {
398+
return {};
399+
}
400+
throw error;
401+
}
402+
}

src/course-home/data/slice.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const slice = createSlice({
1818
toastBodyLink: null,
1919
toastHeader: '',
2020
showSearch: false,
21+
examsData: null,
2122
},
2223
reducers: {
2324
fetchProctoringInfoResolved: (state) => {
@@ -53,6 +54,9 @@ const slice = createSlice({
5354
setShowSearch: (state, { payload }) => {
5455
state.showSearch = payload;
5556
},
57+
setExamsData: (state, { payload }) => {
58+
state.examsData = payload;
59+
},
5660
},
5761
});
5862

@@ -64,6 +68,7 @@ export const {
6468
fetchTabSuccess,
6569
setCallToActionToast,
6670
setShowSearch,
71+
setExamsData,
6772
} = slice.actions;
6873

6974
export const {

src/course-home/data/thunks.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
executePostFromPostEvent,
55
getCourseHomeCourseMetadata,
66
getDatesTabData,
7+
getExamsData,
78
getOutlineTabData,
89
getProgressTabData,
910
postCourseDeadlines,
@@ -26,6 +27,7 @@ import {
2627
fetchTabRequest,
2728
fetchTabSuccess,
2829
setCallToActionToast,
30+
setExamsData
2931
} from './slice';
3032

3133
import mapSearchResponse from '../courseware-search/map-search-response';
@@ -223,3 +225,19 @@ export function searchCourseContent(courseId, searchKeyword) {
223225
});
224226
};
225227
}
228+
229+
export function fetchExamAttemptsData(courseId, sequenceIds) {
230+
return async (dispatch) => {
231+
const results = await Promise.all(sequenceIds.map(async (sequenceId) => {
232+
try {
233+
const response = await getExamsData(courseId, sequenceId);
234+
return response.exam || {};
235+
} catch (e) {
236+
logError(e);
237+
return [sequenceId, {}];
238+
}
239+
}));
240+
241+
dispatch(setExamsData(results));
242+
};
243+
}

src/course-home/progress-tab/ProgressTab.jsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React from 'react';
1+
import React, { useMemo } from 'react';
22
import { useWindowSize } from '@openedx/paragon';
33
import { useContextId } from '../../data/hooks';
4+
import { useModel } from '../../generic/model-store';
45
import ProgressTabCertificateStatusSidePanelSlot from '../../plugin-slots/ProgressTabCertificateStatusSidePanelSlot';
56

67
import CourseCompletion from './course-completion/CourseCompletion';
@@ -10,11 +11,17 @@ import ProgressTabCertificateStatusMainBodySlot from '../../plugin-slots/Progres
1011
import ProgressTabCourseGradeSlot from '../../plugin-slots/ProgressTabCourseGradeSlot';
1112
import ProgressTabGradeBreakdownSlot from '../../plugin-slots/ProgressTabGradeBreakdownSlot';
1213
import ProgressTabRelatedLinksSlot from '../../plugin-slots/ProgressTabRelatedLinksSlot';
13-
import { useModel } from '../../generic/model-store';
14+
import { useGetExamsData } from './hooks';
1415

1516
const ProgressTab = () => {
1617
const courseId = useContextId();
17-
const { disableProgressGraph } = useModel('progress', courseId);
18+
const { disableProgressGraph, sectionScores } = useModel('progress', courseId);
19+
20+
const sequenceIds = useMemo(() => (
21+
sectionScores.flatMap((section) => (section.subsections)).map((subsection) => subsection.blockKey)
22+
), [sectionScores]);
23+
24+
useGetExamsData(courseId, sequenceIds);
1825

1926
const windowWidth = useWindowSize().width;
2027
if (windowWidth === undefined) {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useEffect } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
4+
import { fetchExamAttemptsData } from '../data/thunks';
5+
6+
export function useGetExamsData(courseId, sequenceIds) {
7+
const dispatch = useDispatch();
8+
9+
useEffect(() => {
10+
dispatch(fetchExamAttemptsData(courseId, sequenceIds));
11+
}, [dispatch, courseId, sequenceIds]);
12+
}

0 commit comments

Comments
 (0)