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..7de2b563 100644
--- a/src/course-home/progress-tab/ProgressTab.test.jsx
+++ b/src/course-home/progress-tab/ProgressTab.test.jsx
@@ -31,7 +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);
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
@@ -806,11 +808,54 @@ describe('Progress Tab', () => {
sendTrackEvent.mockClear();
});
- it('Displays text for nonPassing case when learner does not have a passing grade', async () => {
+ it('Displays text for nonPassing case when learner does not have a passing grade and certificates are viewable', async () => {
await fetchAndRender();
expect(screen.getByText('In order to qualify for a certificate, you must have a passing grade.')).toBeInTheDocument();
});
+ it('Displays text for notAvailableNonPassing case when a learner does not have a passing grade and certificates are not viewable', async () => {
+ setMetadata({
+ can_view_certificate: false,
+ is_enrolled: true,
+ });
+ setTabData({
+ end: tomorrow.toISOString(),
+ });
+ await fetchAndRender();
+ expect(screen.getByText(`This course ends on ${tomorrow.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}, final grades and any earned certificates are scheduled to be available after ${tomorrow.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}.`)).toBeInTheDocument();
+ });
+
+ it('Handles certificate available dates for notAvailableNotPassing', async () => {
+ setMetadata({
+ can_view_certificate: false,
+ is_enrolled: true,
+ });
+ setTabData({
+ end: tomorrow.toISOString(),
+ certificate_data: {
+ certificate_available_date: overmorrow.toISOString(),
+ },
+ });
+ await fetchAndRender();
+ expect(screen.getByText(`This course ends on ${tomorrow.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}, final grades and any earned certificates are scheduled to be available after ${overmorrow.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ })}.`)).toBeInTheDocument();
+ });
+
it('sends event when visiting progress tab when learner is not passing', async () => {
await fetchAndRender();
diff --git a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
index e32b3532..9777216b 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,
+ isSelfPaced,
+ canViewCertificate,
} = useModel('courseHomeMeta', courseId);
const {
@@ -90,9 +92,25 @@ function CertificateStatus({ intl }) {
if (mode === COURSE_EXIT_MODES.disabled) {
certEventName = 'certificate_status_disabled';
} else if (mode === COURSE_EXIT_MODES.nonPassing && !certIsDownloadable) {
- certCase = 'notPassing';
certEventName = 'not_passing';
- body = intl.formatMessage(messages[`${certCase}Body`]);
+ // If the learner is not supposed to be able to view a certificate because
+ // of the certificate display behavior, we also don't want to show them a
+ // "not passing" message early.
+ certCase = canViewCertificate && !isSelfPaced ? 'notPassing' : 'notAvailableNotPassing';
+ if (!canViewCertificate) {
+ endDate = intl.formatDate(end, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ certAvailabilityDate = intl.formatDate(certificateAvailableDate || end, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ }
+ // These values just pass through if they are not needed by the message
+ body = intl.formatMessage(messages[`${certCase}Body`], { endDate, certAvailabilityDate });
} else if (mode === COURSE_EXIT_MODES.inProgress && !certIsDownloadable) {
certCase = 'inProgress';
certEventName = 'has_scheduled_content';
diff --git a/src/course-home/progress-tab/certificate-status/messages.js b/src/course-home/progress-tab/certificate-status/messages.js
index 332fea7b..3948a265 100644
--- a/src/course-home/progress-tab/certificate-status/messages.js
+++ b/src/course-home/progress-tab/certificate-status/messages.js
@@ -106,6 +106,16 @@ const messages = defineMessages({
defaultMessage: 'In order to generate a certificate for this course, you must complete the ID verification process.',
description: 'Body text when the learner needs to do verification to earn a certifcate',
},
+ notAvailableNotPassingHeader: {
+ id: 'progress.certificateStatus.notAvailableNotPassingHeader',
+ defaultMessage: 'Certificate status',
+ description: 'Header text when the certifcate is not available',
+ },
+ notAvailableNotPassingBody: {
+ id: 'progress.certificateStatus.notAvailableNotPassingBody',
+ defaultMessage: 'This course ends on {endDate}, final grades and any earned certificates are scheduled to be available after {certAvailabilityDate}.',
+ description: 'message that appears at the end of a course for a user who has finished a course early and has not passed or grades are not final',
+ },
});
export default messages;
diff --git a/src/courseware/course/course-exit/CourseCelebration.jsx b/src/courseware/course/course-exit/CourseCelebration.jsx
index 69ee7171..0b709a3a 100644
--- a/src/courseware/course/course-exit/CourseCelebration.jsx
+++ b/src/courseware/course/course-exit/CourseCelebration.jsx
@@ -55,6 +55,7 @@ function CourseCelebration({ intl }) {
const {
org,
verifiedMode,
+ canViewCertificate,
} = useModel('courseHomeMeta', courseId);
const {
@@ -248,6 +249,29 @@ function CourseCelebration({ intl }) {
}
break;
default:
+ // To properly handle messaging in the case where a course has not ended yet
+ // and certificateData is null, we want to make sure we serve a generic message
+ // whether they are in a nonpassing or earned state.
+ if (!canViewCertificate) {
+ const endDate =
+
+ {intl.formatMessage( + messages.courseFinishedEarlyBody, + { + courseEndDate, + certificateAvailableDate, + }, + )} +
+