From fbe16483ac463837651d2b5978a6a0062cbf5b7a Mon Sep 17 00:00:00 2001 From: Ishan Masdekar Date: Tue, 24 Sep 2024 00:11:52 +0530 Subject: [PATCH] fix: corrects navigation if the student does not pass the entrance exam (#1429) --- .../certificate-status/CertificateStatus.jsx | 7 +++ src/courseware/course/course-exit/utils.js | 16 ++++++- .../UnitNavigation.test.jsx | 44 +++++++++++++++++++ .../sequence/sequence-navigation/hooks.js | 11 +++++ 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx index cc73ca0690..03837ee1c9 100644 --- a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx +++ b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx @@ -19,6 +19,10 @@ const CertificateStatus = ({ intl }) => { courseId, } = useSelector(state => state.courseHome); + const { + entranceExamData, + } = useModel('coursewareMeta', courseId); + const { isEnrolled, org, @@ -42,6 +46,8 @@ const CertificateStatus = ({ intl }) => { certificateAvailableDate, } = certificateData || {}; + const entranceExamPassed = entranceExamData?.entranceExamPassed ?? null; + const mode = getCourseExitMode( certificateData, hasScheduledContent, @@ -49,6 +55,7 @@ const CertificateStatus = ({ intl }) => { userHasPassingGrade, null, // CourseExitPageIsActive canViewCertificate, + entranceExamPassed, ); const eventProperties = { diff --git a/src/courseware/course/course-exit/utils.js b/src/courseware/course/course-exit/utils.js index da1d353094..c989a150dd 100644 --- a/src/courseware/course/course-exit/utils.js +++ b/src/courseware/course/course-exit/utils.js @@ -9,6 +9,7 @@ const COURSE_EXIT_MODES = { celebration: 1, nonPassing: 2, inProgress: 3, + entranceExamFail: 4, }; // These are taken from the edx-platform `get_cert_data` function found in lms/courseware/views/views.py @@ -32,9 +33,14 @@ function getCourseExitMode( userHasPassingGrade, courseExitPageIsActive = null, canImmediatelyViewCertificate = false, + entranceExamPassed = null, ) { const authenticatedUser = getAuthenticatedUser(); + if (entranceExamPassed === false) { + return COURSE_EXIT_MODES.entranceExamFail; + } + if (courseExitPageIsActive === false || !authenticatedUser || !isEnrolled) { return COURSE_EXIT_MODES.disabled; } @@ -73,6 +79,7 @@ function GetCourseExitNavigation(courseId, intl) { isEnrolled, userHasPassingGrade, courseExitPageIsActive, + entranceExamData: { entranceExamPassed }, } = useModel('coursewareMeta', courseId); const { canViewCertificate } = useModel('courseHomeMeta', courseId); const exitMode = getCourseExitMode( @@ -82,8 +89,15 @@ function GetCourseExitNavigation(courseId, intl) { userHasPassingGrade, courseExitPageIsActive, canViewCertificate, + entranceExamPassed, ); - const exitActive = exitMode !== COURSE_EXIT_MODES.disabled; + + /** exitActive is used to enable/disable the exit i.e. next buttons. + COURSE_EXIT_MODES denote the current status of the course. + Available COURSE_EXIT_MODES: disabled, celebration, nonPassing, inProgress, entranceExamFail + If the user fails the entrance exam, + access to further course sections should not be allowed i.e. disable the next buttons. */ + const exitActive = ((exitMode !== COURSE_EXIT_MODES.disabled) && (exitMode !== COURSE_EXIT_MODES.entranceExamFail)); let exitText; switch (exitMode) { diff --git a/src/courseware/course/sequence/sequence-navigation/UnitNavigation.test.jsx b/src/courseware/course/sequence/sequence-navigation/UnitNavigation.test.jsx index 89603e6534..779769d265 100644 --- a/src/courseware/course/sequence/sequence-navigation/UnitNavigation.test.jsx +++ b/src/courseware/course/sequence/sequence-navigation/UnitNavigation.test.jsx @@ -86,6 +86,50 @@ describe('Unit Navigation', () => { expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); }); + it('has the "Next" button disabled for entrance exam failed', async () => { + const testCourseMetadata = { + ...courseMetadata, + certificate_data: { cert_status: 'bogus_status' }, + enrollment: { is_active: true }, + entrance_exam_data: { + entrance_exam_current_score: 0, entrance_exam_enabled: true, entrance_exam_id: '1', entrance_exam_minimum_score_pct: 0.65, entrance_exam_passed: false, + }, + }; + const testStore = await initializeTestStore({ courseMetadata: testCourseMetadata, unitBlocks }, false); + // Have to refetch the sequenceId since the new store generates new sequences + const { courseware } = testStore.getState(); + const testData = { ...mockData, sequenceId: courseware.sequenceId }; + + render( + , + { store: testStore, wrapWithRouter: true }, + ); + + expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); + }); + + it('has the "Next" button enabled for entrance exam pass', async () => { + const testCourseMetadata = { + ...courseMetadata, + certificate_data: { cert_status: 'bogus_status' }, + enrollment: { is_active: true }, + entrance_exam_data: { + entrance_exam_current_score: 1.0, entrance_exam_enabled: true, entrance_exam_id: '1', entrance_exam_minimum_score_pct: 0.65, entrance_exam_passed: true, + }, + }; + const testStore = await initializeTestStore({ courseMetadata: testCourseMetadata, unitBlocks }, false); + // Have to refetch the sequenceId since the new store generates new sequences + const { courseware } = testStore.getState(); + const testData = { ...mockData, sequenceId: courseware.sequenceId }; + + render( + , + { store: testStore, wrapWithRouter: true }, + ); + + expect(screen.getByRole('link', { name: /next/i })).toBeEnabled(); + }); + it('displays end of course message instead of the "Next" button as needed', async () => { const testCourseMetadata = { ...courseMetadata, certificate_data: { cert_status: 'notpassing' }, enrollment: { is_active: true } }; const testStore = await initializeTestStore({ courseMetadata: testCourseMetadata, unitBlocks }, false); diff --git a/src/courseware/course/sequence/sequence-navigation/hooks.js b/src/courseware/course/sequence/sequence-navigation/hooks.js index ef0db2035b..d0195468c8 100644 --- a/src/courseware/course/sequence/sequence-navigation/hooks.js +++ b/src/courseware/course/sequence/sequence-navigation/hooks.js @@ -13,6 +13,7 @@ export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId) const sequence = useModel('sequences', currentSequenceId); const courseId = useSelector(state => state.courseware.courseId); const courseStatus = useSelector(state => state.courseware.courseStatus); + const { entranceExamData: { entranceExamPassed } } = useModel('coursewareMeta', courseId); const sequenceStatus = useSelector(state => state.courseware.sequenceStatus); // If we don't know the sequence and unit yet, then assume no. @@ -25,6 +26,16 @@ export function useSequenceNavigationMetadata(currentSequenceId, currentUnitId) }; } + // if entrance exam is not passed then we should treat this as 1st and last unit + if (entranceExamPassed === false) { + return { + isFirstUnit: true, + isLastUnit: true, + navigationDisabledNextSequence: false, + navigationDisabledPrevSequence: false, + }; + } + const sequenceIndex = sequenceIds.indexOf(currentSequenceId); const unitIndex = sequence.unitIds.indexOf(currentUnitId);