Compare commits
3 Commits
dependabot
...
ttracy/MIC
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31c116cf58 | ||
|
|
b3f56f8f18 | ||
|
|
e74de71fb7 |
@@ -10,6 +10,7 @@ Factory.define('courseHomeMetadata')
|
|||||||
is_enrolled: false,
|
is_enrolled: false,
|
||||||
is_staff: false,
|
is_staff: false,
|
||||||
can_load_courseware: true,
|
can_load_courseware: true,
|
||||||
|
can_view_certificate: true,
|
||||||
celebrations: null,
|
celebrations: null,
|
||||||
course_access: {
|
course_access: {
|
||||||
additional_context_user_message: null,
|
additional_context_user_message: null,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ Object {
|
|||||||
"courseHomeMeta": Object {
|
"courseHomeMeta": Object {
|
||||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
"canLoadCourseware": true,
|
"canLoadCourseware": true,
|
||||||
|
"canViewCertificate": true,
|
||||||
"celebrations": null,
|
"celebrations": null,
|
||||||
"courseAccess": Object {
|
"courseAccess": Object {
|
||||||
"additionalContextUserMessage": null,
|
"additionalContextUserMessage": null,
|
||||||
@@ -340,6 +341,7 @@ Object {
|
|||||||
"courseHomeMeta": Object {
|
"courseHomeMeta": Object {
|
||||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
"canLoadCourseware": true,
|
"canLoadCourseware": true,
|
||||||
|
"canViewCertificate": true,
|
||||||
"celebrations": null,
|
"celebrations": null,
|
||||||
"courseAccess": Object {
|
"courseAccess": Object {
|
||||||
"additionalContextUserMessage": null,
|
"additionalContextUserMessage": null,
|
||||||
@@ -538,6 +540,7 @@ Object {
|
|||||||
"courseHomeMeta": Object {
|
"courseHomeMeta": Object {
|
||||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
"canLoadCourseware": true,
|
"canLoadCourseware": true,
|
||||||
|
"canViewCertificate": true,
|
||||||
"celebrations": null,
|
"celebrations": null,
|
||||||
"courseAccess": Object {
|
"courseAccess": Object {
|
||||||
"additionalContextUserMessage": null,
|
"additionalContextUserMessage": null,
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ describe('Progress Tab', () => {
|
|||||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||||
const progressUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/progress/*`);
|
const progressUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/progress/*`);
|
||||||
const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`;
|
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) {
|
function setMetadata(attributes, options) {
|
||||||
const courseMetadata = Factory.build('courseHomeMetadata', attributes, options);
|
const courseMetadata = Factory.build('courseHomeMetadata', attributes, options);
|
||||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||||
@@ -806,11 +808,54 @@ describe('Progress Tab', () => {
|
|||||||
sendTrackEvent.mockClear();
|
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();
|
await fetchAndRender();
|
||||||
expect(screen.getByText('In order to qualify for a certificate, you must have a passing grade.')).toBeInTheDocument();
|
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 () => {
|
it('sends event when visiting progress tab when learner is not passing', async () => {
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ function CertificateStatus({ intl }) {
|
|||||||
const {
|
const {
|
||||||
isEnrolled,
|
isEnrolled,
|
||||||
org,
|
org,
|
||||||
|
isSelfPaced,
|
||||||
|
canViewCertificate,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -90,9 +92,25 @@ function CertificateStatus({ intl }) {
|
|||||||
if (mode === COURSE_EXIT_MODES.disabled) {
|
if (mode === COURSE_EXIT_MODES.disabled) {
|
||||||
certEventName = 'certificate_status_disabled';
|
certEventName = 'certificate_status_disabled';
|
||||||
} else if (mode === COURSE_EXIT_MODES.nonPassing && !certIsDownloadable) {
|
} else if (mode === COURSE_EXIT_MODES.nonPassing && !certIsDownloadable) {
|
||||||
certCase = 'notPassing';
|
|
||||||
certEventName = 'not_passing';
|
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) {
|
} else if (mode === COURSE_EXIT_MODES.inProgress && !certIsDownloadable) {
|
||||||
certCase = 'inProgress';
|
certCase = 'inProgress';
|
||||||
certEventName = 'has_scheduled_content';
|
certEventName = 'has_scheduled_content';
|
||||||
|
|||||||
@@ -106,6 +106,16 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'In order to generate a certificate for this course, you must complete the ID verification process.',
|
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',
|
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;
|
export default messages;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ function CourseCelebration({ intl }) {
|
|||||||
const {
|
const {
|
||||||
org,
|
org,
|
||||||
verifiedMode,
|
verifiedMode,
|
||||||
|
canViewCertificate,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -248,6 +249,29 @@ function CourseCelebration({ intl }) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
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 = <FormattedDate value={end} day="numeric" month="long" year="numeric" />;
|
||||||
|
const certAvailableDate = <FormattedDate value={certificateAvailableDate || end} day="numeric" month="long" year="numeric" />;
|
||||||
|
certHeader = intl.formatMessage(messages.certificateHeaderNotAvailable);
|
||||||
|
message = (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="courseCelebration.certificateBody.notAvailable.v2"
|
||||||
|
defaultMessage="This course ends on {endDate}. Final grades and any earned certificates are
|
||||||
|
scheduled to be available after {certAvailableDate}."
|
||||||
|
values={{ endDate, certAvailableDate }}
|
||||||
|
description="This shown for leaner when they are eligible for certifcate but it't not available yet, it could because leaners just finished the course quickly!"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
visitEvent = 'celebration_with_unavailable_cert';
|
||||||
|
footnote = <DashboardFootnote variant={visitEvent} />;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ describe('Course Exit Pages', () => {
|
|||||||
const discoveryRecommendationsUrl = new RegExp(`${getConfig().DISCOVERY_API_BASE_URL}/api/v1/course_recommendations/*`);
|
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 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 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 = {}) {
|
function setMetadata(coursewareAttributes, courseHomeAttributes = {}) {
|
||||||
const extendedCourseMetadata = { ...coursewareMetadata, ...coursewareAttributes };
|
const extendedCourseMetadata = { ...coursewareMetadata, ...coursewareAttributes };
|
||||||
@@ -367,11 +370,54 @@ describe('Course Exit Pages', () => {
|
|||||||
|
|
||||||
describe('Course Non-passing Experience', () => {
|
describe('Course Non-passing Experience', () => {
|
||||||
it('Displays link to progress tab', async () => {
|
it('Displays link to progress tab', async () => {
|
||||||
setMetadata({ user_has_passing_grade: false });
|
setMetadata({
|
||||||
|
user_has_passing_grade: false,
|
||||||
|
});
|
||||||
await fetchAndRender(<CourseNonPassing />);
|
await fetchAndRender(<CourseNonPassing />);
|
||||||
expect(screen.getByText('You’ve reached the end of the course!')).toBeInTheDocument();
|
expect(screen.getByText('You’ve reached the end of the course!')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('link', { name: 'View grades' })).toBeInTheDocument();
|
expect(screen.getByRole('link', { name: 'View grades' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Displays text for notAvailableNonPassing case when a learner does not have a passing grade and certificates are not viewable', async () => {
|
||||||
|
setMetadata({
|
||||||
|
user_has_passing_grade: false,
|
||||||
|
end: tomorrow.toISOString(),
|
||||||
|
}, {
|
||||||
|
can_view_certificate: false,
|
||||||
|
});
|
||||||
|
await fetchAndRender(<CourseNonPassing />);
|
||||||
|
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({
|
||||||
|
user_has_passing_grade: false,
|
||||||
|
end: tomorrow.toISOString(),
|
||||||
|
certificate_data: {
|
||||||
|
certificate_available_date: overmorrow.toISOString(),
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
can_view_certificate: false,
|
||||||
|
});
|
||||||
|
await fetchAndRender(<CourseNonPassing />);
|
||||||
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Course in progress experience', () => {
|
describe('Course in progress experience', () => {
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
|||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Alert, Button } from '@edx/paragon';
|
import {
|
||||||
|
Alert, breakpoints, Button, useWindowSize,
|
||||||
|
} from '@edx/paragon';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
|
import { CheckCircle } from '@edx/paragon/icons';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
|
import CelebrationMobile from './assets/celebration_456x328.gif';
|
||||||
|
import CelebrationDesktop from './assets/celebration_750x540.gif';
|
||||||
|
import certificate from '../../../generic/assets/edX_certificate.png';
|
||||||
import CatalogSuggestion from './CatalogSuggestion';
|
import CatalogSuggestion from './CatalogSuggestion';
|
||||||
import DashboardFootnote from './DashboardFootnote';
|
import DashboardFootnote from './DashboardFootnote';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -20,15 +26,27 @@ function CourseNonPassing({ intl }) {
|
|||||||
org,
|
org,
|
||||||
tabs,
|
tabs,
|
||||||
title,
|
title,
|
||||||
|
isSelfPaced,
|
||||||
|
canViewCertificate,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
const { end, certificateData } = useModel('coursewareMeta', courseId);
|
||||||
const { administrator } = getAuthenticatedUser();
|
const { administrator } = getAuthenticatedUser();
|
||||||
|
let certificateAvailableDate = certificateData?.certificateAvailableDate || end;
|
||||||
|
certificateAvailableDate = intl.formatDate(certificateAvailableDate, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
const courseEndDate = intl.formatDate(end, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
||||||
// Get progress tab link for 'view grades' button
|
// Get progress tab link for 'view grades' button
|
||||||
const progressTab = tabs.find(tab => tab.slug === 'progress');
|
const progressTab = tabs.find(tab => tab.slug === 'progress');
|
||||||
const progressLink = progressTab && progressTab.url;
|
const progressLink = progressTab && progressTab.url;
|
||||||
|
|
||||||
useEffect(() => logVisit(org, courseId, administrator, 'nonpassing'), [org, courseId, administrator]);
|
useEffect(() => logVisit(org, courseId, administrator, 'nonpassing'), [org, courseId, administrator]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -38,21 +56,79 @@ function CourseNonPassing({ intl }) {
|
|||||||
<div className="col-12 p-0 h2 text-center">
|
<div className="col-12 p-0 h2 text-center">
|
||||||
{ intl.formatMessage(messages.endOfCourseHeader) }
|
{ intl.formatMessage(messages.endOfCourseHeader) }
|
||||||
</div>
|
</div>
|
||||||
<Alert variant="primary" className="col col-lg-10 mt-4">
|
{!canViewCertificate && !isSelfPaced && (
|
||||||
<div className="row w-100 m-0 align-items-start">
|
<>
|
||||||
<div className="flex-grow-1 col-sm p-0">{ intl.formatMessage(messages.endOfCourseDescription) }</div>
|
{!wideScreen && (
|
||||||
{progressLink && (
|
<div className="col-12 mt-3 mb-4 px-0 px-md-5 text-center">
|
||||||
<Button
|
<img
|
||||||
variant="primary"
|
src={CelebrationMobile}
|
||||||
className="flex-shrink-0 mt-3 mt-sm-0 mb-1 mb-sm-0 ml-sm-5"
|
alt={`${intl.formatMessage(messages.congratulationsImage)}`}
|
||||||
href={progressLink}
|
className="img-fluid"
|
||||||
onClick={() => logClick(org, courseId, administrator, 'view_grades')}
|
/>
|
||||||
>
|
</div>
|
||||||
{intl.formatMessage(messages.viewGradesButton)}
|
)}
|
||||||
</Button>
|
{wideScreen && (
|
||||||
)}
|
<div className="col-12 mt-3 mb-4 px-0 px-md-5 text-center">
|
||||||
</div>
|
<img
|
||||||
</Alert>
|
src={CelebrationDesktop}
|
||||||
|
alt={`${intl.formatMessage(messages.congratulationsImage)}`}
|
||||||
|
className="img-fluid"
|
||||||
|
style={{ width: '36rem' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{ // We don't want to show a learner an nonPassing message
|
||||||
|
// before the certificates are viewable for the course.
|
||||||
|
canViewCertificate || isSelfPaced
|
||||||
|
? (
|
||||||
|
<Alert variant="primary" className="col col-lg-10 mt-4">
|
||||||
|
<div className="row w-100 m-0 align-items-start">
|
||||||
|
<div className="flex-grow-1 col-sm p-0">
|
||||||
|
{intl.formatMessage(messages.endOfCourseDescription)}
|
||||||
|
</div>
|
||||||
|
{progressLink && (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
className="flex-shrink-0 mt-3 mt-sm-0 mb-1 mb-sm-0 ml-sm-5"
|
||||||
|
href={progressLink}
|
||||||
|
onClick={() => logClick(org, courseId, administrator, 'view_grades')}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.viewGradesButton)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<div className="col-12 px-0 px-md-5">
|
||||||
|
<Alert Alert variant="success" icon={CheckCircle}>
|
||||||
|
<div className="row w-100 m-0">
|
||||||
|
<div className="col order-1 order-md-0 pl-0 pr-0 pr-md-5">
|
||||||
|
<h4 className="h4">{intl.formatMessage(messages.courseFinishedEarlyHeading)}</h4>
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage(
|
||||||
|
messages.courseFinishedEarlyBody,
|
||||||
|
{
|
||||||
|
courseEndDate,
|
||||||
|
certificateAvailableDate,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 order-0 col-md-3 order-md-1 w-100 mb-3 p-0 text-center">
|
||||||
|
<img
|
||||||
|
src={certificate}
|
||||||
|
alt={`${intl.formatMessage(messages.certificateImage)}`}
|
||||||
|
className="w-100"
|
||||||
|
style={{ maxWidth: '13rem' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
<DashboardFootnote variant="nonpassing" />
|
<DashboardFootnote variant="nonpassing" />
|
||||||
<CatalogSuggestion variant="nonpassing" />
|
<CatalogSuggestion variant="nonpassing" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,6 +66,16 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'More content is coming soon!',
|
defaultMessage: 'More content is coming soon!',
|
||||||
description: 'Header when the status of the course not all of (contents or assignments) available yet',
|
description: 'Header when the status of the course not all of (contents or assignments) available yet',
|
||||||
},
|
},
|
||||||
|
courseFinishedEarlyHeading: {
|
||||||
|
id: 'courseExit.courseFinishedEarlyHeading',
|
||||||
|
defaultMessage: 'Your grade and certificate status will be available soon.',
|
||||||
|
description: 'Heading to a 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',
|
||||||
|
},
|
||||||
|
courseFinishedEarlyBody: {
|
||||||
|
id: 'courseExit.courseFinishedEarlyBody',
|
||||||
|
defaultMessage: 'This course ends on {courseEndDate}, final grades and any earned certificates are scheduled to be available after {certificateAvailableDate}.',
|
||||||
|
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',
|
||||||
|
},
|
||||||
dashboardLink: {
|
dashboardLink: {
|
||||||
id: 'courseExit.dashboardLink',
|
id: 'courseExit.dashboardLink',
|
||||||
defaultMessage: 'Dashboard',
|
defaultMessage: 'Dashboard',
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ function normalizeMetadata(metadata) {
|
|||||||
relatedPrograms: camelCaseObject(data.related_programs),
|
relatedPrograms: camelCaseObject(data.related_programs),
|
||||||
userNeedsIntegritySignature: data.user_needs_integrity_signature,
|
userNeedsIntegritySignature: data.user_needs_integrity_signature,
|
||||||
canAccessProctoredExams: data.can_access_proctored_exams,
|
canAccessProctoredExams: data.can_access_proctored_exams,
|
||||||
|
canViewCertificate: data.can_view_certificate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user