diff --git a/src/course-home/data/__factories__/courseHomeMetadata.factory.js b/src/course-home/data/__factories__/courseHomeMetadata.factory.js
index fb4781b3..7bdf3b50 100644
--- a/src/course-home/data/__factories__/courseHomeMetadata.factory.js
+++ b/src/course-home/data/__factories__/courseHomeMetadata.factory.js
@@ -10,6 +10,7 @@ Factory.define('courseHomeMetadata')
is_enrolled: false,
is_staff: false,
can_load_courseware: true,
+ can_view_certificate: true,
celebrations: null,
course_access: {
additional_context_user_message: null,
diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap
index 8d927da1..d3872878 100644
--- a/src/course-home/data/__snapshots__/redux.test.js.snap
+++ b/src/course-home/data/__snapshots__/redux.test.js.snap
@@ -22,6 +22,7 @@ Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
"canLoadCourseware": true,
+ "canViewCertificate": true,
"celebrations": null,
"courseAccess": Object {
"additionalContextUserMessage": null,
@@ -340,6 +341,7 @@ Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
"canLoadCourseware": true,
+ "canViewCertificate": true,
"celebrations": null,
"courseAccess": Object {
"additionalContextUserMessage": null,
@@ -538,6 +540,7 @@ Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
"canLoadCourseware": true,
+ "canViewCertificate": true,
"celebrations": null,
"courseAccess": Object {
"additionalContextUserMessage": null,
diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx
index aa9c1ffd..6097f06b 100644
--- a/src/course-home/progress-tab/ProgressTab.test.jsx
+++ b/src/course-home/progress-tab/ProgressTab.test.jsx
@@ -31,6 +31,9 @@ describe('Progress Tab', () => {
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
const progressUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/progress/*`);
const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`;
+ const now = new Date();
+ const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
+ const overmorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2);
function setMetadata(attributes, options) {
const courseMetadata = Factory.build('courseHomeMetadata', attributes, options);
@@ -1220,6 +1223,65 @@ describe('Progress Tab', () => {
await fetchAndRender();
expect(screen.queryByTestId('certificate-status-component')).not.toBeInTheDocument();
});
+
+ it('Shows not available messaging before certificates are available to nonpassing learners when theres no certificate data', async () => {
+ setMetadata({
+ can_view_certificate: false,
+ is_enrolled: true,
+ });
+ setTabData({
+ end: tomorrow.toISOString(),
+ certificate_data: undefined,
+ });
+ await fetchAndRender();
+ expect(screen.getByText(`Final grades and any earned certificates are scheduled to be available after ${tomorrow.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}.`)).toBeInTheDocument();
+ });
+
+ it('Shows not available messaging before certificates are available to passing learners when theres no certificate data', async () => {
+ setMetadata({
+ can_view_certificate: false,
+ is_enrolled: true,
+ });
+ setTabData({
+ end: tomorrow.toISOString(),
+ user_has_passing_grade: true,
+ certificate_data: undefined,
+ });
+ await fetchAndRender();
+ expect(screen.getByText(`Final grades and any earned certificates are scheduled to be available after ${tomorrow.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}.`)).toBeInTheDocument();
+ });
+
+ it('Shows certificate_available_date if learner is passing', async () => {
+ setMetadata({
+ can_view_certificate: false,
+ is_enrolled: true,
+ });
+ setTabData({
+ end: tomorrow.toISOString(),
+ user_has_passing_grade: true,
+ certificate_data: {
+ cert_status: 'earned_but_not_available',
+ certificate_available_date: overmorrow.toISOString(),
+ },
+ });
+ await fetchAndRender();
+ expect(screen.getByText('Certificate status'));
+ expect(screen.getByText(
+ overmorrow.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }),
+ )).toBeInTheDocument();
+ });
});
describe('Credit Information', () => {
diff --git a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
index e32b3532..d6c7b448 100644
--- a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
+++ b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
@@ -22,6 +22,8 @@ function CertificateStatus({ intl }) {
const {
isEnrolled,
org,
+ canViewCertificate,
+ userTimezone,
} = useModel('courseHomeMeta', courseId);
const {
@@ -45,6 +47,8 @@ function CertificateStatus({ intl }) {
hasScheduledContent,
isEnrolled,
userHasPassingGrade,
+ null, // CourseExitPageIsActive
+ canViewCertificate,
);
const eventProperties = {
@@ -58,6 +62,7 @@ function CertificateStatus({ intl }) {
let certStatus;
let certWebViewUrl;
let downloadUrl;
+ const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
if (certificateData) {
certStatus = certificateData.certStatus;
@@ -178,10 +183,22 @@ function CertificateStatus({ intl }) {
}
break;
- // This code shouldn't be hit but coding defensively since switch expects a default statement
default:
- certCase = null;
- certEventName = 'no_certificate_status';
+ // if user completes a course before certificates are available, treat it as notAvailable
+ // regardless of passing or nonpassing status
+ if (!canViewCertificate) {
+ certCase = 'notAvailable';
+ endDate = intl.formatDate(end, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ ...timezoneFormatArgs,
+ });
+ body = intl.formatMessage(messages.notAvailableEndDateBody, { endDate });
+ } else {
+ certCase = null;
+ certEventName = 'no_certificate_status';
+ }
break;
}
}
diff --git a/src/course-home/progress-tab/certificate-status/messages.js b/src/course-home/progress-tab/certificate-status/messages.js
index 332fea7b..54b21e96 100644
--- a/src/course-home/progress-tab/certificate-status/messages.js
+++ b/src/course-home/progress-tab/certificate-status/messages.js
@@ -76,6 +76,11 @@ const messages = defineMessages({
defaultMessage: 'Certificate status',
description: 'Header text when the certifcate is not available',
},
+ notAvailableEndDateBody: {
+ id: 'progress.certificateBody.notAvailable.endDate',
+ defaultMessage: 'Final grades and any earned certificates are scheduled to be available after {endDate}.',
+ description: 'Shown for learners who have finished a course before grades and certificates are available.',
+ },
upgradeHeader: {
id: 'progress.certificateStatus.upgradeHeader',
defaultMessage: 'Earn a certificate',
diff --git a/src/courseware/course/course-exit/CourseCelebration.jsx b/src/courseware/course/course-exit/CourseCelebration.jsx
index 69ee7171..ec12e82a 100644
--- a/src/courseware/course/course-exit/CourseCelebration.jsx
+++ b/src/courseware/course/course-exit/CourseCelebration.jsx
@@ -55,6 +55,8 @@ function CourseCelebration({ intl }) {
const {
org,
verifiedMode,
+ canViewCertificate,
+ userTimezone,
} = useModel('courseHomeMeta', courseId);
const {
@@ -69,6 +71,7 @@ function CourseCelebration({ intl }) {
const dashboardLink =
+ {intl.formatMessage(messages.certificateNotAvailableEndDateBody, { endDate })} +
++ {intl.formatMessage(messages.certificateNotAvailableBodyAccessCert)} +
+ > + ); + } break; } diff --git a/src/courseware/course/course-exit/CourseExit.jsx b/src/courseware/course/course-exit/CourseExit.jsx index b9e7de7a..6bd80b85 100644 --- a/src/courseware/course/course-exit/CourseExit.jsx +++ b/src/courseware/course/course-exit/CourseExit.jsx @@ -27,7 +27,10 @@ function CourseExit({ intl }) { userHasPassingGrade, } = useModel('coursewareMeta', courseId); - const { isMasquerading } = useModel('courseHomeMeta', courseId); + const { + isMasquerading, + canViewCertificate, + } = useModel('courseHomeMeta', courseId); const mode = getCourseExitMode( certificateData, @@ -35,6 +38,7 @@ function CourseExit({ intl }) { isEnrolled, userHasPassingGrade, courseExitPageIsActive, + canViewCertificate, ); // Audit users cannot fully complete a course, so we will diff --git a/src/courseware/course/course-exit/CourseExit.test.jsx b/src/courseware/course/course-exit/CourseExit.test.jsx index 5c8d0398..cd7b5588 100644 --- a/src/courseware/course/course-exit/CourseExit.test.jsx +++ b/src/courseware/course/course-exit/CourseExit.test.jsx @@ -38,6 +38,9 @@ describe('Course Exit Pages', () => { const discoveryRecommendationsUrl = new RegExp(`${getConfig().DISCOVERY_API_BASE_URL}/api/v1/course_recommendations/*`); const enrollmentsUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment*`); const learningSequencesUrlRegExp = new RegExp(`${getConfig().LMS_BASE_URL}/api/learning_sequences/v1/course_outline/*`); + const now = new Date(); + const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); + const overmorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2); function setMetadata(coursewareAttributes, courseHomeAttributes = {}) { const extendedCourseMetadata = { ...coursewareMetadata, ...coursewareAttributes }; @@ -363,6 +366,64 @@ describe('Course Exit Pages', () => { expect(screen.queryByText('Same Course')).not.toBeInTheDocument(); }); }); + + it('Shows not available messaging before certificates are available to nonpassing learners when theres no certificate data', async () => { + setMetadata({ + is_enrolled: true, + end: tomorrow.toISOString(), + user_has_passing_grade: false, + certificate_data: undefined, + }, { + can_view_certificate: false, + }); + await fetchAndRender(