diff --git a/src/courseware/course/course-exit/CourseCelebration.jsx b/src/courseware/course/course-exit/CourseCelebration.jsx
index 11434d12..b6b70081 100644
--- a/src/courseware/course/course-exit/CourseCelebration.jsx
+++ b/src/courseware/course/course-exit/CourseCelebration.jsx
@@ -7,9 +7,7 @@ import { layoutGenerator } from 'react-break';
import { Helmet } from 'react-helmet';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
-import { Button, Hyperlink } from '@edx/paragon';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
+import { Alert, Button, Hyperlink } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
@@ -19,6 +17,7 @@ import certificate from './assets/edx_certificate.png';
import messages from './messages';
import { useModel } from '../../../generic/model-store';
import { requestCert } from '../../../course-home/data/thunks';
+import DashboardFootnote from './DashboardFootnote';
function CourseCelebration({ intl }) {
const layout = layoutGenerator({
@@ -154,7 +153,7 @@ function CourseCelebration({ intl }) {
{`${intl.formatMessage(messages.congratulationsHeader)} | ${getConfig().SITE_NAME}`}
-
+
{intl.formatMessage(messages.congratulationsHeader)}
@@ -179,7 +178,7 @@ function CourseCelebration({ intl }) {
-
+
{title}
{message}
@@ -213,17 +212,8 @@ function CourseCelebration({ intl }) {
/>
)}
-
-
+
+
>
diff --git a/src/courseware/course/course-exit/CourseExit.jsx b/src/courseware/course/course-exit/CourseExit.jsx
index aa789376..7deeeb71 100644
--- a/src/courseware/course/course-exit/CourseExit.jsx
+++ b/src/courseware/course/course-exit/CourseExit.jsx
@@ -6,51 +6,36 @@ import { Button } from '@edx/paragon';
import { Redirect, useParams } from 'react-router-dom';
import CourseCelebration from './CourseCelebration';
+import CourseNonPassing from './CourseNonPassing';
+import { COURSE_EXIT_MODES, getCourseExitMode } from './utils';
import messages from './messages';
-import { useModel } from '../../../generic/model-store';
-
-// These are taken from the edx-platform `get_cert_data` function found in lms/courseware/views/views.py
-const CELEBRATION_STATUSES = [
- 'downloadable',
- 'earned_but_not_available',
- 'requesting',
- 'unverified',
-];
function CourseExit({ intl }) {
const { courseId } = useParams();
- const {
- courseExitPageIsActive,
- userHasPassingGrade,
- certificateData,
- } = useModel('courses', courseId);
+ const mode = getCourseExitMode(courseId);
- // userHasPassingGrade can be removed once there is an experience for failing learners
- if (!courseExitPageIsActive || !userHasPassingGrade) {
+ let body = null;
+ if (mode === COURSE_EXIT_MODES.nonPassing) {
+ body = ();
+ } else if (mode === COURSE_EXIT_MODES.celebration) {
+ body = ();
+ } else {
return ();
}
- const {
- certStatus,
- } = certificateData;
-
- if (CELEBRATION_STATUSES.indexOf(certStatus) !== -1) {
- return (
- <>
-
-
-
-
- >
- );
- }
- // Just to be safe
- return ();
+ return (
+ <>
+
+
+
+ {body}
+ >
+ );
}
CourseExit.propTypes = {
diff --git a/src/courseware/course/course-exit/CourseNonPassing.jsx b/src/courseware/course/course-exit/CourseNonPassing.jsx
new file mode 100644
index 00000000..d438ed49
--- /dev/null
+++ b/src/courseware/course/course-exit/CourseNonPassing.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+
+import {
+ injectIntl, intlShape,
+} from '@edx/frontend-platform/i18n';
+import { Helmet } from 'react-helmet';
+import { useParams } from 'react-router-dom';
+import { Alert, Button } from '@edx/paragon';
+import { getConfig } from '@edx/frontend-platform';
+
+import { useModel } from '../../../generic/model-store';
+
+import DashboardFootnote from './DashboardFootnote';
+import messages from './messages';
+
+function CourseNonPassing({ intl }) {
+ const { courseId } = useParams();
+ const { tabs } = useModel('courses', courseId);
+
+ // Get progress tab link for 'view grades' button
+ const progressTab = tabs.find(tab => tab.slug === 'progress');
+ const progressLink = progressTab && progressTab.url;
+
+ return (
+ <>
+
+ {`${intl.formatMessage(messages.endOfCourseTitle)} | ${getConfig().SITE_NAME}`}
+
+
+
+ { intl.formatMessage(messages.endOfCourseHeader) }
+
+
+ { intl.formatMessage(messages.endOfCourseDescription) }
+ {progressLink && (
+
+ )}
+
+
+
+ >
+ );
+}
+
+CourseNonPassing.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(CourseNonPassing);
diff --git a/src/courseware/course/course-exit/DashboardFootnote.jsx b/src/courseware/course/course-exit/DashboardFootnote.jsx
new file mode 100644
index 00000000..ab10c22c
--- /dev/null
+++ b/src/courseware/course/course-exit/DashboardFootnote.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import {
+ FormattedMessage, injectIntl, intlShape,
+} from '@edx/frontend-platform/i18n';
+import { Hyperlink } from '@edx/paragon';
+import { faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
+import { getConfig } from '@edx/frontend-platform';
+
+import Footnote from './Footnote';
+import messages from './messages';
+
+function DashboardFootnote({ intl }) {
+ const dashboardLink = (
+
+ {intl.formatMessage(messages.dashboardLink)}
+
+ );
+
+ return (
+
+ )}
+ />
+ );
+}
+
+DashboardFootnote.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(DashboardFootnote);
diff --git a/src/courseware/course/course-exit/Footnote.jsx b/src/courseware/course/course-exit/Footnote.jsx
new file mode 100644
index 00000000..b405edbe
--- /dev/null
+++ b/src/courseware/course/course-exit/Footnote.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+function Footnote({ icon, text }) {
+ return (
+
+ );
+}
+
+Footnote.propTypes = {
+ icon: PropTypes.shape({}).isRequired,
+ text: PropTypes.node.isRequired,
+};
+
+export default Footnote;
diff --git a/src/courseware/course/course-exit/index.js b/src/courseware/course/course-exit/index.js
new file mode 100644
index 00000000..d87cec62
--- /dev/null
+++ b/src/courseware/course/course-exit/index.js
@@ -0,0 +1,4 @@
+import CourseExit from './CourseExit';
+import { getCourseExitText } from './utils';
+
+export { CourseExit, getCourseExitText };
diff --git a/src/courseware/course/course-exit/messages.js b/src/courseware/course/course-exit/messages.js
index 63417595..e693f934 100644
--- a/src/courseware/course/course-exit/messages.js
+++ b/src/courseware/course/course-exit/messages.js
@@ -45,11 +45,31 @@ const messages = defineMessages({
defaultMessage: 'Download my certificate',
description: 'Button to download the course certificate',
},
+ endOfCourseDescription: {
+ id: 'courseExit.endOfCourseDescription',
+ defaultMessage: 'Unfortunately, you are not currently eligible for a certificate. You need to receive a passing grade to be eligible for a certificate.',
+ },
+ endOfCourseHeader: {
+ id: 'courseExit.endOfCourseHeader',
+ defaultMessage: 'You’ve reached the end of the course!',
+ },
+ endOfCourseTitle: {
+ id: 'courseExit.endOfCourseTitle',
+ defaultMessage: 'End of Course',
+ },
idVerificationSupportLink: {
id: 'courseExit.idVerificationSupportLink',
defaultMessage: 'Learn more about ID verification',
description: 'Link to an article about identity verification',
},
+ nextButtonComplete: {
+ id: 'learn.sequence.navigation.complete.button', // for historical reasons
+ defaultMessage: 'Complete the course',
+ },
+ nextButtonEnd: {
+ id: 'courseExit.nextButton.endOfCourse',
+ defaultMessage: 'Next (end of course)',
+ },
profileLink: {
id: 'courseExit.profileLink',
defaultMessage: 'Profile',
@@ -83,6 +103,10 @@ const messages = defineMessages({
defaultMessage: 'View my courses',
description: 'Button to redirect user to their course dashboard',
},
+ viewGradesButton: {
+ id: 'courseExit.viewGradesButton',
+ defaultMessage: 'View grades',
+ },
});
export default messages;
diff --git a/src/courseware/course/course-exit/utils.js b/src/courseware/course/course-exit/utils.js
new file mode 100644
index 00000000..a7bbfb16
--- /dev/null
+++ b/src/courseware/course/course-exit/utils.js
@@ -0,0 +1,60 @@
+import { useModel } from '../../../generic/model-store';
+
+import messages from './messages';
+
+const COURSE_EXIT_MODES = {
+ disabled: 0,
+ celebration: 1,
+ nonPassing: 2,
+};
+
+// These are taken from the edx-platform `get_cert_data` function found in lms/courseware/views/views.py
+const CELEBRATION_STATUSES = [
+ 'downloadable',
+ 'earned_but_not_available',
+ 'requesting',
+ 'unverified',
+];
+const NON_CERTIFICATE_STATUSES = [ // no certificate will be given, though a valid certificateData block is provided
+ 'audit_passing',
+ 'honor_passing', // honor can be configured to not give a certificate
+];
+
+function getCourseExitMode(courseId) {
+ const {
+ certificateData,
+ courseExitPageIsActive,
+ userHasPassingGrade,
+ } = useModel('courses', courseId);
+
+ if (!courseExitPageIsActive || !certificateData) {
+ return COURSE_EXIT_MODES.disabled;
+ }
+
+ const {
+ certStatus,
+ } = certificateData;
+ const isEligibleForCertificate = NON_CERTIFICATE_STATUSES.indexOf(certStatus) === -1;
+
+ if (isEligibleForCertificate && !userHasPassingGrade) {
+ return COURSE_EXIT_MODES.nonPassing;
+ }
+ if (CELEBRATION_STATUSES.indexOf(certStatus) !== -1) {
+ return COURSE_EXIT_MODES.celebration;
+ }
+ return COURSE_EXIT_MODES.disabled;
+}
+
+// Returns null if course exit is either not active or not handling the current case
+function getCourseExitText(courseId, intl) {
+ switch (getCourseExitMode(courseId)) {
+ case COURSE_EXIT_MODES.celebration:
+ return intl.formatMessage(messages.nextButtonComplete);
+ case COURSE_EXIT_MODES.nonPassing:
+ return intl.formatMessage(messages.nextButtonEnd);
+ default:
+ return null;
+ }
+}
+
+export { COURSE_EXIT_MODES, getCourseExitMode, getCourseExitText };
diff --git a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx
index 9076fc2f..8bccba2e 100644
--- a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx
+++ b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx
@@ -7,6 +7,7 @@ import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
+import { getCourseExitText } from '../../course-exit';
import UnitButton from './UnitButton';
import SequenceNavigationTabs from './SequenceNavigationTabs';
import { useSequenceNavigationMetadata } from './hooks';
@@ -34,10 +35,6 @@ function SequenceNavigation({
const isLocked = sequenceStatus === LOADED ? (
sequence.gatedContent !== undefined && sequence.gatedContent.gated
) : undefined;
- const {
- courseExitPageIsActive,
- userHasPassingGrade,
- } = useModel('courses', courseId);
const renderUnitButtons = () => {
if (isLocked) {
@@ -61,31 +58,10 @@ function SequenceNavigation({
};
const renderNextButton = () => {
- // AA-198: The userHasPassingGrade condition can be removed once we have a view for learners with failing grades
- const buttonOnClick = (isLastUnit && courseExitPageIsActive && userHasPassingGrade
- ? goToCourseExitPage : nextSequenceHandler);
- // AA-198: The userHasPassingGrade condition can be removed once we have a view for learners with failing grades
- const disabled = isLastUnit && (!courseExitPageIsActive || !userHasPassingGrade);
-
- let buttonText = (intl.formatMessage(messages.nextButton));
- if (isLastUnit && courseExitPageIsActive && userHasPassingGrade) {
- buttonText = (intl.formatMessage(messages.completeCourseButton));
- }
- // AA-198: Uncomment once there is a view for learners with failing grades
- // else if (isLastUnit && courseExitPageIsActive && !userHasPassingGrade) {
- // buttonText = (
- //
- //
- // )
- // }
+ const exitText = getCourseExitText(courseId, intl);
+ const buttonOnClick = isLastUnit ? goToCourseExitPage : nextSequenceHandler;
+ const buttonText = isLastUnit && exitText ? exitText : intl.formatMessage(messages.nextButton);
+ const disabled = isLastUnit && !exitText;
return (