diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index 504c866e..e5aa549a 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -461,6 +461,7 @@ Object { "canEnroll": true, "extraText": "Contact the administrator.", }, + "enrollmentMode": undefined, "handoutsHtml": "", "hasEnded": undefined, "hasScheduledContent": null, @@ -471,6 +472,7 @@ Object { "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde", }, "timeOffsetMillis": 0, + "userHasPassingGrade": undefined, "verifiedMode": Object { "accessExpirationDate": "2050-01-01T12:00:00", "currency": "USD", diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 9791171a..ef008ea4 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -343,12 +343,14 @@ export async function getOutlineTabData(courseId) { const datesBannerInfo = camelCaseObject(data.dates_banner_info); const datesWidget = camelCaseObject(data.dates_widget); const enrollAlert = camelCaseObject(data.enroll_alert); + const enrollmentMode = data.enrollment_mode; const handoutsHtml = data.handouts_html; const hasScheduledContent = data.has_scheduled_content; const hasEnded = data.has_ended; const offer = camelCaseObject(data.offer); const resumeCourse = camelCaseObject(data.resume_course); const timeOffsetMillis = getTimeOffsetMillis(headers && headers.date, requestTime, responseTime); + const userHasPassingGrade = data.user_has_passing_grade; const verifiedMode = camelCaseObject(data.verified_mode); const welcomeMessageHtml = data.welcome_message_html; @@ -362,12 +364,14 @@ export async function getOutlineTabData(courseId) { datesBannerInfo, datesWidget, enrollAlert, + enrollmentMode, handoutsHtml, hasScheduledContent, hasEnded, offer, resumeCourse, timeOffsetMillis, // This should move to a global time correction reference + userHasPassingGrade, verifiedMode, welcomeMessageHtml, }; diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx index 040f3ce0..400e55d1 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -693,6 +693,40 @@ describe('Outline Tab', () => { await fetchAndRender(); expect(screen.queryByText('Verify your identity to earn a certificate!')).toBeInTheDocument(); }); + it('renders non passing grade', async () => { + const now = new Date(); + const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1); + const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); + setMetadata({ is_enrolled: true }); + setTabData({ + cert_data: {}, + user_has_passing_grade: false, + has_ended: true, + enrollment_mode: 'verified', + }, { + date_blocks: [ + { + date_type: 'course-end-date', + date: yesterday.toISOString(), + title: 'End', + }, + { + date_type: 'certificate-available-date', + date: tomorrow.toISOString(), + title: 'Cert Available', + }, + { + date_type: 'verification-deadline-date', + date: tomorrow.toISOString(), + link_text: 'Verify', + title: 'Verification Upgrade Deadline', + }, + ], + }); + await fetchAndRender(); + screen.getAllByText('You are not eligible for a certificate'); + expect(screen.queryByText('You are not eligible for a certificate')).toBeInTheDocument(); + }); it('tracks request cert button', async () => { sendTrackEvent.mockClear(); const now = new Date(); diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx index c92497b1..becf78ad 100644 --- a/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx +++ b/src/course-home/outline-tab/alerts/certificate-status-alert/CertificateStatusAlert.jsx @@ -36,6 +36,8 @@ function CertificateStatusAlert({ intl, payload }) { isWebCert, userTimezone, org, + notPassingCourseEnded, + tabs, } = payload; // eslint-disable-next-line react/prop-types @@ -118,6 +120,24 @@ function CertificateStatusAlert({ intl, payload }) { return alertProps; }; + const renderNotPassingCourseEnded = () => { + const progressTab = tabs.find(tab => tab.slug === 'progress'); + const progressLink = progressTab && progressTab.url; + + const alertProps = { + header: intl.formatMessage(certMessages.certStatusNotPassingHeader), + buttonMessage: intl.formatMessage(certMessages.certStatusNotPassingButton), + body: intl.formatMessage(certStatusMessages.notPassingBody), + buttonVisible: true, + buttonLink: progressLink, + buttonAction: () => { + sendAlertClickTracking('edx.ui.lms.course_outline.certificate_alert_view_grades_button.clicked'); + }, + }; + + return alertProps; + }; + let alertProps = {}; switch (certStatus) { case CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE: @@ -129,6 +149,9 @@ function CertificateStatusAlert({ intl, payload }) { alertProps = renderNotIDVerifiedStatus(); break; default: + if (notPassingCourseEnded) { + alertProps = renderNotPassingCourseEnded(); + } break; } @@ -184,6 +207,12 @@ CertificateStatusAlert.propTypes = { isWebCert: PropTypes.bool, userTimezone: PropTypes.string, org: PropTypes.string, + notPassingCourseEnded: PropTypes.bool, + tabs: PropTypes.arrayOf(PropTypes.shape({ + tab_id: PropTypes.string, + title: PropTypes.string, + url: PropTypes.string, + })), }).isRequired, }; diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js b/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js index 8a8e62d7..4564ff3a 100644 --- a/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js +++ b/src/course-home/outline-tab/alerts/certificate-status-alert/hooks.js @@ -21,9 +21,19 @@ function verifyCertStatusType(status) { } function useCertificateStatusAlert(courseId) { + const VERIFIED_MODES = { + PROFESSIONAL: 'professional', + VERIFIED: 'verified', + NO_ID_PROFESSIONAL_MODE: 'no-id-professional', + CREDIT_MODE: 'credit', + MASTERS: 'masters', + EXECUTIVE_EDUCATION: 'executive-education', + }; + const { isEnrolled, org, + tabs, } = useModel('courseHomeMeta', courseId); const { @@ -32,6 +42,9 @@ function useCertificateStatusAlert(courseId) { userTimezone, }, certData, + hasEnded, + userHasPassingGrade, + enrollmentMode, } = useModel('outline', courseId); const { @@ -42,7 +55,11 @@ function useCertificateStatusAlert(courseId) { } = certData || {}; const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date'); const isWebCert = downloadUrl === null; - + const isVerifiedEnrollmentMode = ( + enrollmentMode !== null + && enrollmentMode !== undefined + && !!Object.values(VERIFIED_MODES).find(mode => mode === enrollmentMode) + ); let certURL = ''; if (certWebViewUrl) { certURL = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`; @@ -52,8 +69,17 @@ function useCertificateStatusAlert(courseId) { } const hasAlertingCertStatus = verifyCertStatusType(certStatus); - // Only show if there is a known cert status that we want provide status on. + // Only show if: + // - there is a known cert status that we want provide status on. + // - Or the course has ended and the learner does not have a passing grade. const isVisible = isEnrolled && hasAlertingCertStatus; + const notPassingCourseEnded = ( + isEnrolled + && isVerifiedEnrollmentMode + && !hasAlertingCertStatus + && hasEnded + && !userHasPassingGrade + ); const payload = { certificateAvailableDate, certURL, @@ -63,9 +89,11 @@ function useCertificateStatusAlert(courseId) { userTimezone, isWebCert, org, + notPassingCourseEnded, + tabs, }; - useAlert(isVisible, { + useAlert(isVisible || notPassingCourseEnded, { code: 'clientCertificateStatusAlert', payload: useMemo(() => payload, Object.values(payload).sort()), topic: 'outline-course-alerts', diff --git a/src/course-home/outline-tab/alerts/certificate-status-alert/messages.js b/src/course-home/outline-tab/alerts/certificate-status-alert/messages.js index 8dfce5f6..a9756a8e 100644 --- a/src/course-home/outline-tab/alerts/certificate-status-alert/messages.js +++ b/src/course-home/outline-tab/alerts/certificate-status-alert/messages.js @@ -11,6 +11,14 @@ const messages = defineMessages({ defaultMessage: 'Congratulations! Your certificate is ready.', description: 'Header alerting the user that their certificate is ready.', }, + certStatusNotPassingHeader: { + id: 'cert.alert.notPassing.header', + defaultMessage: 'You are not eligible for a certificate', + }, + certStatusNotPassingButton: { + id: 'cert.alert.notPassing.button', + defaultMessage: 'View grades', + }, }); export default messages;