From 6a9a736ce6b59643fb533cb92112830e51f91aab Mon Sep 17 00:00:00 2001 From: Alie Langston Date: Mon, 9 Dec 2024 13:12:24 -0500 Subject: [PATCH 1/3] feat: update gating for chat component --- src/constants.ts | 7 +++++++ src/courseware/course/chat/Chat.jsx | 26 +++++++++++++++++++----- src/courseware/course/chat/Chat.test.jsx | 26 ++++++++++++++++++++++++ src/index.jsx | 1 + 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 484d84530d..20f7d35d15 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -48,6 +48,13 @@ export const VERIFIED_MODES = [ 'paid-bootcamp', ] as const satisfies readonly string[]; +export const AUDIT_MODES = [ + 'audit', + 'honor', + 'unpaid-executive-education', + 'unpaid-bootcamp', +] as const satisfies readonly string[]; + export const WIDGETS = { DISCUSSIONS: 'DISCUSSIONS', NOTIFICATIONS: 'NOTIFICATIONS', diff --git a/src/courseware/course/chat/Chat.jsx b/src/courseware/course/chat/Chat.jsx index 4dbe81398c..f51819bd4f 100644 --- a/src/courseware/course/chat/Chat.jsx +++ b/src/courseware/course/chat/Chat.jsx @@ -3,9 +3,10 @@ import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { Xpert } from '@edx/frontend-lib-learning-assistant'; +import { getConfig } from '@edx/frontend-platform'; import { injectIntl } from '@edx/frontend-platform/i18n'; -import { VERIFIED_MODES } from '@src/constants'; +import { AUDIT_MODES, VERIFIED_MODES } from '@src/constants'; import { useModel } from '../../../generic/model-store'; const Chat = ({ @@ -21,10 +22,14 @@ const Chat = ({ } = useSelector(state => state.specialExams); const course = useModel('coursewareMeta', courseId); - const hasVerifiedEnrollment = ( + const hasValidEnrollment = ( enrollmentMode !== null && enrollmentMode !== undefined - && VERIFIED_MODES.includes(enrollmentMode) + && ( + VERIFIED_MODES.includes(enrollmentMode) + // audit learners are only eligible if Xpert has been explicitly enabled for audit + || (AUDIT_MODES.includes(enrollmentMode) && getConfig().ENABLE_XPERT_AUDIT) + ) ); const validDates = () => { @@ -42,7 +47,7 @@ const Chat = ({ const shouldDisplayChat = ( enabled - && (hasVerifiedEnrollment || isStaff) // display only to verified learners or staff + && (hasValidEnrollment || isStaff) && validDates() // it is necessary to check both whether the user is in an exam, and whether or not they are viewing an exam // this will prevent the learner from interacting with the tool at any point of the exam flow, even at the @@ -50,11 +55,22 @@ const Chat = ({ && !(activeAttempt?.attempt_id || exam?.id) ); + const isUpgradeEligible = ( + enrollmentMode !== null + && enrollmentMode !== undefined + && AUDIT_MODES.includes(enrollmentMode) + ); + return ( <> {/* Use a portal to ensure that component overlay does not compete with learning MFE styles. */} {shouldDisplayChat && (createPortal( - , + , document.body, ))} diff --git a/src/courseware/course/chat/Chat.test.jsx b/src/courseware/course/chat/Chat.test.jsx index 067565f34b..3abf64f26f 100644 --- a/src/courseware/course/chat/Chat.test.jsx +++ b/src/courseware/course/chat/Chat.test.jsx @@ -2,6 +2,8 @@ import { BrowserRouter } from 'react-router-dom'; import React from 'react'; import { Factory } from 'rosie'; +import { getConfig } from '@edx/frontend-platform'; + import { initializeMockApp, initializeTestStore, @@ -28,6 +30,10 @@ jest.mock('@edx/frontend-lib-learning-assistant', () => { }; }); +jest.mock('@edx/frontend-platform', () => ({ + getConfig: jest.fn().mockReturnValue({ ENABLE_XPERT_AUDIT: false }), +})); + initializeMockApp(); const courseId = 'course-v1:edX+DemoX+Demo_Course'; @@ -225,4 +231,24 @@ describe('Chat', () => { const chat = screen.queryByTestId(mockXpertTestId); expect(chat).toBeInTheDocument(); }); + + it('displays component for audit learner if explicitly enabled', async () => { + getConfig.mockImplementation(() => ({ ENABLE_XPERT_AUDIT: true })); + + render( + + + , + { store }, + ); + + const chat = screen.queryByTestId(mockXpertTestId); + expect(chat).toBeInTheDocument(); + }); }); diff --git a/src/index.jsx b/src/index.jsx index af7a153096..6da653de6c 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -175,6 +175,7 @@ initialize({ CHAT_RESPONSE_URL: process.env.CHAT_RESPONSE_URL || null, PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL || null, SHOW_UNGRADED_ASSIGNMENT_PROGRESS: process.env.SHOW_UNGRADED_ASSIGNMENT_PROGRESS || false, + ENABLE_XPERT_AUDIT: process.env.ENABLE_XPERT_AUDIT || false, }, 'LearnerAppConfig'); }, }, From ad73e3d8e885303c93d662aa6a43cad503337f4b Mon Sep 17 00:00:00 2001 From: Alie Langston Date: Mon, 9 Dec 2024 15:54:13 -0500 Subject: [PATCH 2/3] fix: add gating for access expiration --- src/courseware/course/chat/Chat.jsx | 39 +++++++++++++++--------- src/courseware/course/chat/Chat.test.jsx | 32 +++++++++++++++++++ 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/courseware/course/chat/Chat.jsx b/src/courseware/course/chat/Chat.jsx index f51819bd4f..702d76e784 100644 --- a/src/courseware/course/chat/Chat.jsx +++ b/src/courseware/course/chat/Chat.jsx @@ -22,32 +22,45 @@ const Chat = ({ } = useSelector(state => state.specialExams); const course = useModel('coursewareMeta', courseId); - const hasValidEnrollment = ( + const { + accessExpiration, + start, + end, + } = course; + + const hasVerifiedEnrollment = ( enrollmentMode !== null && enrollmentMode !== undefined - && ( - VERIFIED_MODES.includes(enrollmentMode) - // audit learners are only eligible if Xpert has been explicitly enabled for audit - || (AUDIT_MODES.includes(enrollmentMode) && getConfig().ENABLE_XPERT_AUDIT) - ) + && VERIFIED_MODES.includes(enrollmentMode) + ); + + // audit learners should only have access if the ENABLE_XPERT_AUDIT setting is true + const hasAuditEnrollmentAndAccess = ( + enrollmentMode !== null + && enrollmentMode !== undefined + && AUDIT_MODES.includes(enrollmentMode) + && getConfig().ENABLE_XPERT_AUDIT ); const validDates = () => { const date = new Date(); const utcDate = date.toISOString(); - const startDate = course.start || utcDate; - const endDate = course.end || utcDate; + const startDate = start || utcDate; + const endDate = end || utcDate; + const accessExpirationDate = accessExpiration && accessExpiration.expirationDate + ? accessExpiration.expirationDate : utcDate; return ( startDate <= utcDate && utcDate <= endDate + && (hasAuditEnrollmentAndAccess ? utcDate <= accessExpirationDate : true) ); }; const shouldDisplayChat = ( enabled - && (hasValidEnrollment || isStaff) + && (hasVerifiedEnrollment || isStaff || hasAuditEnrollmentAndAccess) && validDates() // it is necessary to check both whether the user is in an exam, and whether or not they are viewing an exam // this will prevent the learner from interacting with the tool at any point of the exam flow, even at the @@ -55,11 +68,7 @@ const Chat = ({ && !(activeAttempt?.attempt_id || exam?.id) ); - const isUpgradeEligible = ( - enrollmentMode !== null - && enrollmentMode !== undefined - && AUDIT_MODES.includes(enrollmentMode) - ); + const isUpgradeEligible = !hasVerifiedEnrollment && !isStaff; return ( <> @@ -69,7 +78,7 @@ const Chat = ({ courseId={courseId} contentToolsEnabled={contentToolsEnabled} unitId={unitId} - isUpgradEligible={isUpgradeEligible} + isUpgradeEligible={isUpgradeEligible} />, document.body, ))} diff --git a/src/courseware/course/chat/Chat.test.jsx b/src/courseware/course/chat/Chat.test.jsx index 3abf64f26f..d50ead1adf 100644 --- a/src/courseware/course/chat/Chat.test.jsx +++ b/src/courseware/course/chat/Chat.test.jsx @@ -235,6 +235,12 @@ describe('Chat', () => { it('displays component for audit learner if explicitly enabled', async () => { getConfig.mockImplementation(() => ({ ENABLE_XPERT_AUDIT: true })); + store = await initializeTestStore({ + courseMetadata: Factory.build('courseMetadata', { + access_expiration: { expiration_date: '' }, + }), + }); + render( { const chat = screen.queryByTestId(mockXpertTestId); expect(chat).toBeInTheDocument(); }); + + it('does not display component for audit learner if access deadline has passed', async () => { + getConfig.mockImplementation(() => ({ ENABLE_XPERT_AUDIT: true })); + + store = await initializeTestStore({ + courseMetadata: Factory.build('courseMetadata', { + access_expiration: { expiration_date: '2014-02-03T05:00:00Z' }, + }), + }); + + render( + + + , + { store }, + ); + + const chat = screen.queryByTestId(mockXpertTestId); + expect(chat).not.toBeInTheDocument(); + }); }); From bfa4b4ee1e1231925d8fa154620e1d26bb010079 Mon Sep 17 00:00:00 2001 From: Alie Langston Date: Mon, 9 Dec 2024 16:13:27 -0500 Subject: [PATCH 3/3] chore: upgrade learning assistant version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4878aac286..8ca0a3ff0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/browserslist-config": "1.2.0", "@edx/frontend-component-header": "^5.8.0", - "@edx/frontend-lib-learning-assistant": "^2.6.0", + "@edx/frontend-lib-learning-assistant": "^2.9.0", "@edx/frontend-lib-special-exams": "^3.1.3", "@edx/frontend-platform": "^8.0.0", "@edx/openedx-atlas": "^0.6.0", @@ -2431,9 +2431,9 @@ } }, "node_modules/@edx/frontend-lib-learning-assistant": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-2.6.0.tgz", - "integrity": "sha512-73lggfdACTwpz6HA0VbjIetRxpWUMRyz4f79GIrmvHo2DvhICo84jVzbOSrwsvb43H0+52XjbHj6EeaBEDKHPA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-2.9.0.tgz", + "integrity": "sha512-Z85aRiMDv0N/QB8WTywsryJZ7GCtqwimxaW3knQU7dICn4UWRgqccHitoUtwZI6r6R6A57vn0T9Jpg5MZXJy+g==", "dependencies": { "@edx/brand": "npm:@edx/brand-openedx@1.2.0", "@optimizely/react-sdk": "^2.9.2", diff --git a/package.json b/package.json index 3310dbdf7f..68eb695986 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/browserslist-config": "1.2.0", "@edx/frontend-component-header": "^5.8.0", - "@edx/frontend-lib-learning-assistant": "^2.6.0", + "@edx/frontend-lib-learning-assistant": "^2.9.0", "@edx/frontend-lib-special-exams": "^3.1.3", "@edx/frontend-platform": "^8.0.0", "@edx/openedx-atlas": "^0.6.0",