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 ( +
+

+   + {text} +

+
+ ); +} + +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 (