AA-377: Add non-passing course exit screen (#246)
- Adds a non-passing cert learner course exit screen - Moves all the logic about what course-exit mode we're in into a utility method in the course-exit folder - Moves all the logic about how the 'Next' button should read into a utility method in the course-exit folder
This commit is contained in:
@@ -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 }) {
|
||||
<Helmet>
|
||||
<title>{`${intl.formatMessage(messages.congratulationsHeader)} | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="row w-100 mx-0 my-4 px-5 py-3 border border-light">
|
||||
<div className="row w-100 mx-0 mb-4 px-5 py-4 border border-light">
|
||||
<div className="col-12 p-0 h2 text-center">
|
||||
{intl.formatMessage(messages.congratulationsHeader)}
|
||||
</div>
|
||||
@@ -179,7 +178,7 @@ function CourseCelebration({ intl }) {
|
||||
</OnAtLeastTablet>
|
||||
</div>
|
||||
<div className="col-12 px-0 px-md-5">
|
||||
<div className="row w-100 m-0 p-4 bg-primary-100">
|
||||
<Alert variant="primary" className="row w-100 m-0">
|
||||
<div className="col order-1 order-md-0 pl-0 pr-0 pr-md-5">
|
||||
<div className="h4">{title}</div>
|
||||
<p>{message}</p>
|
||||
@@ -213,17 +212,8 @@ function CourseCelebration({ intl }) {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="row w-100 mx-0 my-3 justify-content-center">
|
||||
<p className="text-gray-700">
|
||||
<FontAwesomeIcon icon={faCalendarAlt} style={{ width: '20px' }} />
|
||||
<FormattedMessage
|
||||
id="courseCelebration.dashboardInfo"
|
||||
defaultMessage="You can access this course and its materials on your {dashboardLink}."
|
||||
values={{ dashboardLink }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</Alert>
|
||||
<DashboardFootnote />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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 = (<CourseNonPassing />);
|
||||
} else if (mode === COURSE_EXIT_MODES.celebration) {
|
||||
body = (<CourseCelebration />);
|
||||
} else {
|
||||
return (<Redirect to={`/course/${courseId}`} />);
|
||||
}
|
||||
|
||||
const {
|
||||
certStatus,
|
||||
} = certificateData;
|
||||
|
||||
if (CELEBRATION_STATUSES.indexOf(certStatus) !== -1) {
|
||||
return (
|
||||
<>
|
||||
<div className="row w-100 m-0 justify-content-end">
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
href={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||
>
|
||||
{intl.formatMessage(messages.viewCoursesButton)}
|
||||
</Button>
|
||||
</div>
|
||||
<CourseCelebration />
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Just to be safe
|
||||
return (<Redirect to={`/course/${courseId}`} />);
|
||||
return (
|
||||
<>
|
||||
<div className="row w-100 mt-2 mb-4 justify-content-end">
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
href={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||
>
|
||||
{intl.formatMessage(messages.viewCoursesButton)}
|
||||
</Button>
|
||||
</div>
|
||||
{body}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CourseExit.propTypes = {
|
||||
|
||||
51
src/courseware/course/course-exit/CourseNonPassing.jsx
Normal file
51
src/courseware/course/course-exit/CourseNonPassing.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{`${intl.formatMessage(messages.endOfCourseTitle)} | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="row w-100 mx-0 mb-4 px-5 py-4 border border-light justify-content-center">
|
||||
<div className="col-12 p-0 h2 text-center">
|
||||
{ intl.formatMessage(messages.endOfCourseHeader) }
|
||||
</div>
|
||||
<Alert variant="primary" className="col col-lg-10 mt-4 d-flex align-items-start">
|
||||
<div className="flex-grow-1 mr-5">{ intl.formatMessage(messages.endOfCourseDescription) }</div>
|
||||
{progressLink && (
|
||||
<Button variant="primary" className="flex-shrink-0" href={progressLink}>
|
||||
{intl.formatMessage(messages.viewGradesButton)}
|
||||
</Button>
|
||||
)}
|
||||
</Alert>
|
||||
<DashboardFootnote />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CourseNonPassing.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseNonPassing);
|
||||
42
src/courseware/course/course-exit/DashboardFootnote.jsx
Normal file
42
src/courseware/course/course-exit/DashboardFootnote.jsx
Normal file
@@ -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 = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||
className="text-reset"
|
||||
>
|
||||
{intl.formatMessage(messages.dashboardLink)}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
return (
|
||||
<Footnote
|
||||
icon={faCalendarAlt}
|
||||
text={(
|
||||
<FormattedMessage
|
||||
id="courseCelebration.dashboardInfo" // for historical reasons
|
||||
defaultMessage="You can access this course and its materials on your {dashboardLink}."
|
||||
values={{ dashboardLink }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
DashboardFootnote.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DashboardFootnote);
|
||||
21
src/courseware/course/course-exit/Footnote.jsx
Normal file
21
src/courseware/course/course-exit/Footnote.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
function Footnote({ icon, text }) {
|
||||
return (
|
||||
<div className="row w-100 mx-0 my-4 justify-content-center">
|
||||
<p className="text-gray-700">
|
||||
<FontAwesomeIcon icon={icon} style={{ width: '20px' }} />
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Footnote.propTypes = {
|
||||
icon: PropTypes.shape({}).isRequired,
|
||||
text: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default Footnote;
|
||||
4
src/courseware/course/course-exit/index.js
Normal file
4
src/courseware/course/course-exit/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import CourseExit from './CourseExit';
|
||||
import { getCourseExitText } from './utils';
|
||||
|
||||
export { CourseExit, getCourseExitText };
|
||||
@@ -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;
|
||||
|
||||
60
src/courseware/course/course-exit/utils.js
Normal file
60
src/courseware/course/course-exit/utils.js
Normal file
@@ -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 };
|
||||
@@ -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 = (
|
||||
// <FormattedMessage
|
||||
// defaultMessage="Next"
|
||||
// id="learn.sequence.navigation.endOfCourse.button.next"
|
||||
// description="The 'next' text in the end of course button in the sequence nav"
|
||||
// />
|
||||
// <FormattedMessage
|
||||
// defaultMessage="(end of course)"
|
||||
// id="learn.sequence.navigation.endOfCourse.button.endOfCourse"
|
||||
// description="The '(end of course)' text in the end of course button in the sequence nav"
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
const exitText = getCourseExitText(courseId, intl);
|
||||
const buttonOnClick = isLastUnit ? goToCourseExitPage : nextSequenceHandler;
|
||||
const buttonText = isLastUnit && exitText ? exitText : intl.formatMessage(messages.nextButton);
|
||||
const disabled = isLastUnit && !exitText;
|
||||
return (
|
||||
<Button variant="link" className="next-btn" onClick={buttonOnClick} disabled={disabled}>
|
||||
{buttonText}
|
||||
|
||||
@@ -5,9 +5,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSequenceNavigationMetadata } from './hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import { getCourseExitText } from '../../course-exit';
|
||||
|
||||
import { useSequenceNavigationMetadata } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
function UnitNavigation({
|
||||
@@ -20,38 +21,12 @@ function UnitNavigation({
|
||||
}) {
|
||||
const { isFirstUnit, isLastUnit } = useSequenceNavigationMetadata(sequenceId, unitId);
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const {
|
||||
courseExitPageIsActive,
|
||||
userHasPassingGrade,
|
||||
} = useModel('courses', courseId);
|
||||
|
||||
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 : onClickNext);
|
||||
// AA-198: The userHasPassingGrade condition can be removed once we have a view for learners with failing grades
|
||||
const disabled = isLastUnit && (!courseExitPageIsActive || !userHasPassingGrade);
|
||||
|
||||
// This is just to support what used to show while we are getting courseExitPageIsActive turned on.
|
||||
// This should be good to remove once disabled goes away.
|
||||
if (disabled) {
|
||||
return (
|
||||
<div className="m-2">
|
||||
<span role="img" aria-hidden="true">🤗</span> {/* This is a hugging face emoji */}
|
||||
{' '}
|
||||
{intl.formatMessage(messages.endOfCourse)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (`${intl.formatMessage(messages.nextButton)} (${intl.formatMessage(messages.endOfCourse)})`);
|
||||
// }
|
||||
const exitText = getCourseExitText(courseId, intl);
|
||||
const buttonOnClick = isLastUnit ? goToCourseExitPage : onClickNext;
|
||||
const buttonText = isLastUnit && exitText ? exitText : intl.formatMessage(messages.nextButton);
|
||||
const disabled = isLastUnit && !exitText;
|
||||
return (
|
||||
<Button variant="outline-primary" className="next-button" onClick={buttonOnClick} disabled={disabled}>
|
||||
{buttonText}
|
||||
|
||||
@@ -7,7 +7,11 @@ import UnitNavigation from './UnitNavigation';
|
||||
|
||||
describe('Unit Navigation', () => {
|
||||
let mockData;
|
||||
const courseMetadata = Factory.build('courseMetadata');
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
certificate_data: {
|
||||
cert_status: 'notpassing', // some interesting status that will trigger the last unit button to be active
|
||||
},
|
||||
});
|
||||
const unitBlocks = Array.from({ length: 3 }).map(() => Factory.build(
|
||||
'block',
|
||||
{ type: 'vertical' },
|
||||
@@ -73,11 +77,10 @@ describe('Unit Navigation', () => {
|
||||
expect(screen.getByRole('button', { name: /next/i })).toBeEnabled();
|
||||
});
|
||||
|
||||
it('displays "learn.end.of.course" message instead of the "Next" button for the last unit in the sequence', () => {
|
||||
it('displays end of course message instead of the "Next" button as needed', () => {
|
||||
render(<UnitNavigation {...mockData} unitId={unitBlocks[unitBlocks.length - 1].id} />);
|
||||
|
||||
expect(screen.getByRole('button', { name: /previous/i })).toBeEnabled();
|
||||
expect(screen.queryByRole('button', { name: /next/i })).not.toBeInTheDocument();
|
||||
expect(screen.getByText("You've reached the end of this course!")).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /next \(end of course\)/i })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
completeCourseButton: {
|
||||
id: 'learn.sequence.navigation.complete.button',
|
||||
defaultMessage: 'Complete the course',
|
||||
description: 'Button to advance to the course completion page',
|
||||
},
|
||||
endOfCourse: {
|
||||
id: 'learn.end.of.course',
|
||||
defaultMessage: "You've reached the end of this course!",
|
||||
},
|
||||
nextButton: {
|
||||
id: 'learn.sequence.navigation.next.button',
|
||||
defaultMessage: 'Next',
|
||||
|
||||
@@ -51,6 +51,11 @@ Factory.define('courseMetadata')
|
||||
marketing_url: null,
|
||||
celebrations: null,
|
||||
enroll_alert: null,
|
||||
course_exit_page_is_active: true,
|
||||
user_has_passing_grade: false,
|
||||
certificate_data: {
|
||||
cert_status: 'audit_passing',
|
||||
},
|
||||
}).attr(
|
||||
'tabs', ['tabs', 'id'], (passedTabs, id) => {
|
||||
if (passedTabs) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { UserMessagesProvider } from './generic/user-messages';
|
||||
import './index.scss';
|
||||
import './assets/favicon.ico';
|
||||
import OutlineTab from './course-home/outline-tab';
|
||||
import CourseExit from './courseware/course/course-exit/CourseExit';
|
||||
import { CourseExit } from './courseware/course/course-exit';
|
||||
import CoursewareContainer from './courseware';
|
||||
import CoursewareRedirectLandingPage from './courseware/CoursewareRedirectLandingPage';
|
||||
import DatesTab from './course-home/dates-tab';
|
||||
|
||||
Reference in New Issue
Block a user