diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 8254d4ef1e..b2ec2d3a9f 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -379,3 +379,24 @@ export async function searchCourseContentFromAPI(courseId, searchKeyword, option return camelCaseObject(response); } + +export async function getExamsData(courseId, sequenceId) { + let url; + + if (!getConfig().EXAMS_BASE_URL) { + url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`; + } else { + url = `${getConfig().EXAMS_BASE_URL}/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}`; + } + + try { + const { data } = await getAuthenticatedHttpClient().get(url); + return camelCaseObject(data); + } catch (error) { + const { httpErrorStatus } = error && error.customAttributes; + if (httpErrorStatus === 404) { + return {}; + } + throw error; + } +} diff --git a/src/course-home/data/slice.js b/src/course-home/data/slice.js index 21c804d3f3..86179f8aa7 100644 --- a/src/course-home/data/slice.js +++ b/src/course-home/data/slice.js @@ -18,6 +18,7 @@ const slice = createSlice({ toastBodyLink: null, toastHeader: '', showSearch: false, + examsData: null, }, reducers: { fetchProctoringInfoResolved: (state) => { @@ -53,6 +54,9 @@ const slice = createSlice({ setShowSearch: (state, { payload }) => { state.showSearch = payload; }, + setExamsData: (state, { payload }) => { + state.examsData = payload; + }, }, }); @@ -64,6 +68,7 @@ export const { fetchTabSuccess, setCallToActionToast, setShowSearch, + setExamsData, } = slice.actions; export const { diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 4dd3658e53..44b60fdc26 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -4,6 +4,7 @@ import { executePostFromPostEvent, getCourseHomeCourseMetadata, getDatesTabData, + getExamsData, getOutlineTabData, getProgressTabData, postCourseDeadlines, @@ -26,6 +27,7 @@ import { fetchTabRequest, fetchTabSuccess, setCallToActionToast, + setExamsData, } from './slice'; import mapSearchResponse from '../courseware-search/map-search-response'; @@ -223,3 +225,19 @@ export function searchCourseContent(courseId, searchKeyword) { }); }; } + +export function fetchExamAttemptsData(courseId, sequenceIds) { + return async (dispatch) => { + const results = await Promise.all(sequenceIds.map(async (sequenceId) => { + try { + const response = await getExamsData(courseId, sequenceId); + return response.exam || {}; + } catch (e) { + logError(e); + return [sequenceId, {}]; + } + })); + + dispatch(setExamsData(results)); + }; +} diff --git a/src/course-home/progress-tab/ProgressTab.jsx b/src/course-home/progress-tab/ProgressTab.jsx index a0d86a288b..32506930bf 100644 --- a/src/course-home/progress-tab/ProgressTab.jsx +++ b/src/course-home/progress-tab/ProgressTab.jsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { useWindowSize } from '@openedx/paragon'; import { useContextId } from '../../data/hooks'; +import { useModel } from '../../generic/model-store'; import ProgressTabCertificateStatusSidePanelSlot from '../../plugin-slots/ProgressTabCertificateStatusSidePanelSlot'; import CourseCompletion from './course-completion/CourseCompletion'; @@ -10,11 +11,17 @@ import ProgressTabCertificateStatusMainBodySlot from '../../plugin-slots/Progres import ProgressTabCourseGradeSlot from '../../plugin-slots/ProgressTabCourseGradeSlot'; import ProgressTabGradeBreakdownSlot from '../../plugin-slots/ProgressTabGradeBreakdownSlot'; import ProgressTabRelatedLinksSlot from '../../plugin-slots/ProgressTabRelatedLinksSlot'; -import { useModel } from '../../generic/model-store'; +import { useGetExamsData } from './hooks'; const ProgressTab = () => { const courseId = useContextId(); - const { disableProgressGraph } = useModel('progress', courseId); + const { disableProgressGraph, sectionScores } = useModel('progress', courseId); + + const sequenceIds = useMemo(() => ( + sectionScores.flatMap((section) => (section.subsections)).map((subsection) => subsection.blockKey) + ), [sectionScores]); + + useGetExamsData(courseId, sequenceIds); const windowWidth = useWindowSize().width; if (windowWidth === undefined) { diff --git a/src/course-home/progress-tab/hooks.jsx b/src/course-home/progress-tab/hooks.jsx new file mode 100644 index 0000000000..d1b707bc80 --- /dev/null +++ b/src/course-home/progress-tab/hooks.jsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +import { fetchExamAttemptsData } from '../data/thunks'; + +export function useGetExamsData(courseId, sequenceIds) { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchExamAttemptsData(courseId, sequenceIds)); + }, [dispatch, courseId, sequenceIds]); +}