Compare commits
32 Commits
ttracy/MIC
...
sajjad/VAN
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d17f08451 | ||
|
|
a7b584c566 | ||
|
|
193a184142 | ||
|
|
3e76f7ac78 | ||
|
|
36062ff3a6 | ||
|
|
6257cb4b58 | ||
|
|
792d9eb758 | ||
|
|
cd84a15891 | ||
|
|
cafb881a61 | ||
|
|
fd94da0a43 | ||
|
|
1e41547b3e | ||
|
|
bf2f123367 | ||
|
|
0211ecf45e | ||
|
|
36ac129267 | ||
|
|
20d4c35d83 | ||
|
|
bbff8e719e | ||
|
|
5461c08169 | ||
|
|
ee88a12d8f | ||
|
|
9b316bd859 | ||
|
|
7e7eb83596 | ||
|
|
aaa367780d | ||
|
|
6d42ee9c6f | ||
|
|
41047f4c88 | ||
|
|
d83551c809 | ||
|
|
7c3088901d | ||
|
|
518c9ef6c2 | ||
|
|
ae97efaf2b | ||
|
|
361a099ed1 | ||
|
|
7f3757539a | ||
|
|
44f5132e2a | ||
|
|
53b19c9be3 | ||
|
|
abc374b60a |
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#check package-lock file version
|
||||
|
||||
name: Lockfile Version check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
6
.github/workflows/validate.yml
vendored
6
.github/workflows/validate.yml
vendored
@@ -2,7 +2,7 @@ name: validate
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14, 16]
|
||||
node: [16]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
@@ -19,6 +19,6 @@ jobs:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: make validate.ci
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
1
.husky/_/.gitignore
vendored
Normal file
1
.husky/_/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*
|
||||
31
.husky/_/husky.sh
Normal file
31
.husky/_/husky.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
if [ -z "$husky_skip_init" ]; then
|
||||
debug () {
|
||||
if [ "$HUSKY_DEBUG" = "1" ]; then
|
||||
echo "husky (debug) - $1"
|
||||
fi
|
||||
}
|
||||
|
||||
readonly hook_name="$(basename "$0")"
|
||||
debug "starting $hook_name..."
|
||||
|
||||
if [ "$HUSKY" = "0" ]; then
|
||||
debug "HUSKY env variable is set to 0, skipping hook"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f ~/.huskyrc ]; then
|
||||
debug "sourcing ~/.huskyrc"
|
||||
. ~/.huskyrc
|
||||
fi
|
||||
|
||||
export readonly husky_skip_init=1
|
||||
sh -e "$0" "$@"
|
||||
exitCode="$?"
|
||||
|
||||
if [ $exitCode != 0 ]; then
|
||||
echo "husky - $hook_name hook exited with code $exitCode (error)"
|
||||
fi
|
||||
|
||||
exit $exitCode
|
||||
fi
|
||||
1
Makefile
1
Makefile
@@ -58,7 +58,6 @@ validate:
|
||||
npm run lint -- --max-warnings 0
|
||||
npm run test
|
||||
npm run build
|
||||
npm run is-es5
|
||||
|
||||
.PHONY: validate.ci
|
||||
validate.ci:
|
||||
|
||||
9897
package-lock.json
generated
9897
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -7,13 +7,11 @@
|
||||
"url": "git+https://github.com/edx/frontend-app-learning.git"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie 11"
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
||||
"prepare": "husky install",
|
||||
@@ -32,11 +30,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "10.2.2",
|
||||
"@edx/frontend-component-footer": "10.2.4",
|
||||
"@edx/frontend-component-header": "2.4.6",
|
||||
"@edx/frontend-lib-special-exams": "1.16.0",
|
||||
"@edx/frontend-lib-special-exams": "1.16.3",
|
||||
"@edx/frontend-platform": "1.15.6",
|
||||
"@edx/paragon": "19.13.6",
|
||||
"@edx/paragon": "19.18.3",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
@@ -45,12 +43,14 @@
|
||||
"@popperjs/core": "2.11.5",
|
||||
"@reduxjs/toolkit": "1.8.1",
|
||||
"classnames": "2.3.1",
|
||||
"core-js": "3.21.1",
|
||||
"core-js": "3.22.2",
|
||||
"history": "^5.3.0",
|
||||
"js-cookie": "3.0.1",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"query-string": "^7.1.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.8",
|
||||
"react-router": "5.2.1",
|
||||
@@ -63,14 +63,14 @@
|
||||
"util": "0.12.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.0.2",
|
||||
"@edx/frontend-build": "9.1.4",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@edx/reactifex": "2.0.1",
|
||||
"@pact-foundation/pact": "9.17.3",
|
||||
"@testing-library/jest-dom": "5.16.4",
|
||||
"@testing-library/react": "12.1.4",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/user-event": "13.5.0",
|
||||
"axios-mock-adapter": "1.20.0",
|
||||
"codecov": "3.8.3",
|
||||
"es-check": "6.2.1",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
|
||||
@@ -9,7 +9,7 @@ Factory.define('courseHomeMetadata')
|
||||
is_self_paced: false,
|
||||
is_enrolled: false,
|
||||
is_staff: false,
|
||||
can_load_courseware: true,
|
||||
can_view_certificate: true,
|
||||
celebrations: null,
|
||||
course_access: {
|
||||
additional_context_user_message: null,
|
||||
|
||||
@@ -35,7 +35,6 @@ Factory.define('outlineTabData')
|
||||
cert_status: null,
|
||||
cert_web_view_url: null,
|
||||
certificate_available_date: null,
|
||||
download_url: null,
|
||||
},
|
||||
course_goals: {
|
||||
goal_options: [],
|
||||
|
||||
@@ -21,7 +21,7 @@ Object {
|
||||
"models": Object {
|
||||
"courseHomeMeta": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||
"canLoadCourseware": true,
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": Object {
|
||||
"additionalContextUserMessage": null,
|
||||
@@ -339,7 +339,7 @@ Object {
|
||||
"models": Object {
|
||||
"courseHomeMeta": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||
"canLoadCourseware": true,
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": Object {
|
||||
"additionalContextUserMessage": null,
|
||||
@@ -411,7 +411,6 @@ Object {
|
||||
"certStatus": null,
|
||||
"certWebViewUrl": null,
|
||||
"certificateAvailableDate": null,
|
||||
"downloadUrl": null,
|
||||
},
|
||||
"courseBlocks": Object {
|
||||
"courses": Object {
|
||||
@@ -445,7 +444,6 @@ Object {
|
||||
"effortTime": 15,
|
||||
"icon": null,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||
"legacyWebUrl": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1?experience=legacy",
|
||||
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
"showLink": true,
|
||||
"title": "Title of Sequence",
|
||||
@@ -537,7 +535,7 @@ Object {
|
||||
"models": Object {
|
||||
"courseHomeMeta": Object {
|
||||
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||
"canLoadCourseware": true,
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": Object {
|
||||
"additionalContextUserMessage": null,
|
||||
|
||||
@@ -148,12 +148,9 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
||||
effortTime: block.effort_time,
|
||||
icon: block.icon,
|
||||
id: block.id,
|
||||
legacyWebUrl: block.legacy_web_url,
|
||||
// The presence of an legacy URL for the sequence indicates that we want this
|
||||
// sequence to be a clickable link in the outline (even though, if the new
|
||||
// courseware experience is active, we will ignore `legacyWebUrl` and build a
|
||||
// link to the MFE ourselves).
|
||||
showLink: !!block.legacy_web_url,
|
||||
// The presence of a URL for the sequence indicates that we want this sequence to be a clickable
|
||||
// link in the outline (even though we ignore the given url and use an internal <Link> to ourselves).
|
||||
showLink: !!block.lms_web_url,
|
||||
title: block.display_name,
|
||||
};
|
||||
break;
|
||||
@@ -208,10 +205,6 @@ export async function getDatesTabData(courseId) {
|
||||
return camelCaseObject(data);
|
||||
} catch (error) {
|
||||
const { httpErrorStatus } = error && error.customAttributes;
|
||||
if (httpErrorStatus === 404) {
|
||||
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/dates`);
|
||||
return {};
|
||||
}
|
||||
if (httpErrorStatus === 401) {
|
||||
// The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
|
||||
// courseAccess in the metadata call, so just ignore this status for now.
|
||||
@@ -297,6 +290,20 @@ export async function getProctoringInfoData(courseId, username) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLiveTabIframe(courseId) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/course_live/iframe/${courseId}/`;
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
} catch (error) {
|
||||
const { httpErrorStatus } = error && error.customAttributes;
|
||||
if (httpErrorStatus === 404) {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTimeOffsetMillis(headerDate, requestTime, responseTime) {
|
||||
// Time offset computation should move down into the HttpClient wrapper to maintain a global time correction reference
|
||||
// Requires 'Access-Control-Expose-Headers: Date' on the server response per https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-expose-headers
|
||||
@@ -314,21 +321,9 @@ export function getTimeOffsetMillis(headerDate, requestTime, responseTime) {
|
||||
|
||||
export async function getOutlineTabData(courseId) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
|
||||
let { tabData } = {};
|
||||
let requestTime = Date.now();
|
||||
let responseTime = requestTime;
|
||||
try {
|
||||
requestTime = Date.now();
|
||||
tabData = await getAuthenticatedHttpClient().get(url);
|
||||
responseTime = Date.now();
|
||||
} catch (error) {
|
||||
const { httpErrorStatus } = error && error.customAttributes;
|
||||
if (httpErrorStatus === 404) {
|
||||
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/course/`);
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const requestTime = Date.now();
|
||||
const tabData = await getAuthenticatedHttpClient().get(url);
|
||||
const responseTime = Date.now();
|
||||
|
||||
const {
|
||||
data,
|
||||
|
||||
@@ -58,7 +58,6 @@ describe('Course Home Service', () => {
|
||||
sku: '8CF08E5',
|
||||
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
}),
|
||||
can_load_courseware: boolean(true),
|
||||
celebrations: like({
|
||||
first_section: false,
|
||||
streak_length_to_celebrate: null,
|
||||
@@ -106,7 +105,6 @@ describe('Course Home Service', () => {
|
||||
sku: '8CF08E5',
|
||||
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
},
|
||||
canLoadCourseware: true,
|
||||
celebrations: {
|
||||
firstSection: false,
|
||||
streakLengthToCelebrate: null,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
postWeeklyLearningGoal,
|
||||
postDismissWelcomeMessage,
|
||||
postRequestCert,
|
||||
getLiveTabIframe,
|
||||
} from './api';
|
||||
|
||||
import {
|
||||
@@ -32,46 +33,38 @@ const eventTypes = {
|
||||
export function fetchTab(courseId, tab, getTabData, targetUserId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(fetchTabRequest({ courseId }));
|
||||
Promise.allSettled([
|
||||
getCourseHomeCourseMetadata(courseId, 'outline'),
|
||||
getTabData(courseId, targetUserId),
|
||||
]).then(([courseHomeCourseMetadataResult, tabDataResult]) => {
|
||||
const fetchedCourseHomeCourseMetadata = courseHomeCourseMetadataResult.status === 'fulfilled';
|
||||
const fetchedTabData = tabDataResult.status === 'fulfilled';
|
||||
|
||||
if (fetchedCourseHomeCourseMetadata) {
|
||||
dispatch(addModel({
|
||||
modelType: 'courseHomeMeta',
|
||||
model: {
|
||||
id: courseId,
|
||||
...courseHomeCourseMetadataResult.value,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
logError(courseHomeCourseMetadataResult.reason);
|
||||
}
|
||||
|
||||
if (fetchedTabData) {
|
||||
try {
|
||||
const courseHomeCourseMetadata = await getCourseHomeCourseMetadata(courseId, 'outline');
|
||||
dispatch(addModel({
|
||||
modelType: 'courseHomeMeta',
|
||||
model: {
|
||||
id: courseId,
|
||||
...courseHomeCourseMetadata,
|
||||
},
|
||||
}));
|
||||
const tabDataResult = getTabData && await getTabData(courseId, targetUserId);
|
||||
if (tabDataResult) {
|
||||
dispatch(addModel({
|
||||
modelType: tab,
|
||||
model: {
|
||||
id: courseId,
|
||||
...tabDataResult.value,
|
||||
...tabDataResult,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
logError(tabDataResult.reason);
|
||||
}
|
||||
|
||||
// Disable the access-denied path for now - it caused a regression
|
||||
if (fetchedCourseHomeCourseMetadata && !courseHomeCourseMetadataResult.value.courseAccess.hasAccess) {
|
||||
if (!courseHomeCourseMetadata.courseAccess.hasAccess) {
|
||||
dispatch(fetchTabDenied({ courseId }));
|
||||
} else if (fetchedCourseHomeCourseMetadata && fetchedTabData) {
|
||||
dispatch(fetchTabSuccess({ courseId, targetUserId }));
|
||||
} else {
|
||||
dispatch(fetchTabFailure({ courseId }));
|
||||
} else if (tabDataResult || !getTabData) {
|
||||
dispatch(fetchTabSuccess({
|
||||
courseId,
|
||||
targetUserId,
|
||||
}));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
dispatch(fetchTabFailure({ courseId }));
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,6 +80,14 @@ export function fetchOutlineTab(courseId) {
|
||||
return fetchTab(courseId, 'outline', getOutlineTabData);
|
||||
}
|
||||
|
||||
export function fetchLiveTab(courseId) {
|
||||
return fetchTab(courseId, 'live', getLiveTabIframe);
|
||||
}
|
||||
|
||||
export function fetchDiscussionTab(courseId) {
|
||||
return fetchTab(courseId, 'discussion');
|
||||
}
|
||||
|
||||
export function dismissWelcomeMessage(courseId) {
|
||||
return async () => postDismissWelcomeMessage(courseId);
|
||||
}
|
||||
|
||||
@@ -341,12 +341,12 @@ describe('DatesTab', () => {
|
||||
|
||||
it('redirects to the home page when unauthenticated', async () => {
|
||||
await renderDenied('authentication_required');
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
|
||||
expect(global.location.href).toEqual(`http://localhost/course/${courseMetadata.id}/home`);
|
||||
});
|
||||
|
||||
it('redirects to the home page when unenrolled', async () => {
|
||||
await renderDenied('enrollment_required');
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
|
||||
expect(global.location.href).toEqual(`http://localhost/course/${courseMetadata.id}/home`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
36
src/course-home/discussion-tab/DiscussionTab.jsx
Normal file
36
src/course-home/discussion-tab/DiscussionTab.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath, useHistory } from 'react-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useIFrameHeight, useIFramePluginEvents } from '../../generic/hooks';
|
||||
|
||||
function DiscussionTab() {
|
||||
const { courseId } = useSelector(state => state.courseHome);
|
||||
const { path } = useParams();
|
||||
const [originalPath] = useState(path);
|
||||
const history = useHistory();
|
||||
|
||||
const [, iFrameHeight] = useIFrameHeight();
|
||||
useIFramePluginEvents({
|
||||
'discussions.navigate': (payload) => {
|
||||
const basePath = generatePath('/course/:courseId/discussion', { courseId });
|
||||
history.push(`${basePath}/${payload.path}`);
|
||||
},
|
||||
});
|
||||
const discussionsUrl = `${getConfig().DISCUSSIONS_MFE_BASE_URL}/${courseId}/${originalPath}`;
|
||||
return (
|
||||
<iframe
|
||||
src={discussionsUrl}
|
||||
className="d-flex w-100 border-0"
|
||||
height={iFrameHeight}
|
||||
style={{ minHeight: '60rem' }}
|
||||
title="discussion"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
DiscussionTab.propTypes = {};
|
||||
|
||||
export default injectIntl(DiscussionTab);
|
||||
61
src/course-home/discussion-tab/DiscussionTab.test.jsx
Normal file
61
src/course-home/discussion-tab/DiscussionTab.test.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { getConfig, history } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router';
|
||||
import { Factory } from 'rosie';
|
||||
import { UserMessagesProvider } from '../../generic/user-messages';
|
||||
import {
|
||||
initializeMockApp, messageEvent, screen, waitFor,
|
||||
} from '../../setupTest';
|
||||
import initializeStore from '../../store';
|
||||
import { TabContainer } from '../../tab-page';
|
||||
import { appendBrowserTimezoneToUrl } from '../../utils';
|
||||
import { fetchDiscussionTab } from '../data/thunks';
|
||||
import DiscussionTab from './DiscussionTab';
|
||||
|
||||
initializeMockApp();
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
describe('DiscussionTab', () => {
|
||||
let axiosMock;
|
||||
let store;
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
store = initializeStore();
|
||||
component = (
|
||||
<AppProvider store={store}>
|
||||
<UserMessagesProvider>
|
||||
<Route path="/course/:courseId/discussion">
|
||||
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
|
||||
<DiscussionTab />
|
||||
</TabContainer>
|
||||
</Route>
|
||||
</UserMessagesProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
});
|
||||
|
||||
const courseMetadata = Factory.build('courseHomeMetadata', { user_timezone: 'America/New_York' });
|
||||
const { id: courseId } = courseMetadata;
|
||||
|
||||
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||
history.push(`/course/${courseId}/discussion`); // so tab can pull course id from url
|
||||
|
||||
render(component);
|
||||
});
|
||||
|
||||
it('resizes when it gets a size hint from iframe', async () => {
|
||||
window.postMessage({ ...messageEvent, payload: { height: 1234 } }, '*');
|
||||
await waitFor(() => expect(screen.getByTitle('discussion'))
|
||||
.toHaveAttribute('height', String(1234)));
|
||||
});
|
||||
});
|
||||
22
src/course-home/live-tab/LiveTab.jsx
Normal file
22
src/course-home/live-tab/LiveTab.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
function LiveTab() {
|
||||
const { courseId } = useSelector(state => state.courseHome);
|
||||
const liveModel = useSelector(state => state.models.live);
|
||||
useEffect(() => {
|
||||
const iframe = document.getElementById('lti-tab-embed');
|
||||
if (iframe) {
|
||||
iframe.className += ' vh-100 w-100 border-0';
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div
|
||||
id="live_tab"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: liveModel[courseId]?.iframe }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default LiveTab;
|
||||
@@ -2,6 +2,7 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
@@ -51,9 +52,14 @@ describe('Outline Tab', () => {
|
||||
axiosMock.onGet(outlineUrl).reply(200, outlineTabData);
|
||||
}
|
||||
|
||||
async function fetchAndRender() {
|
||||
async function fetchAndRender(path = '') {
|
||||
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
|
||||
await act(async () => render(<OutlineTab />, { store }));
|
||||
await act(async () => render(
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<OutlineTab />
|
||||
</MemoryRouter>,
|
||||
{ store },
|
||||
));
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -138,25 +144,8 @@ describe('Outline Tab', () => {
|
||||
expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('SequenceLink displays points to legacy courseware', async () => {
|
||||
it('SequenceLink displays link', async () => {
|
||||
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
||||
setMetadata({
|
||||
can_load_courseware: false,
|
||||
});
|
||||
setTabData({
|
||||
course_blocks: { blocks: courseBlocks.blocks },
|
||||
});
|
||||
await fetchAndRender();
|
||||
|
||||
const sequenceLink = screen.getByText('Title of Sequence');
|
||||
expect(sequenceLink.getAttribute('href')).toContain(`/courses/${courseId}`);
|
||||
});
|
||||
|
||||
it('SequenceLink displays points to courseware MFE', async () => {
|
||||
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
||||
setMetadata({
|
||||
can_load_courseware: true,
|
||||
});
|
||||
setTabData({
|
||||
course_blocks: { blocks: courseBlocks.blocks },
|
||||
});
|
||||
@@ -165,6 +154,21 @@ describe('Outline Tab', () => {
|
||||
const sequenceLink = screen.getByText('Title of Sequence');
|
||||
expect(sequenceLink.getAttribute('href')).toContain(`/course/${courseId}`);
|
||||
});
|
||||
|
||||
it('set event property fromEmail true in case of from_email query param', async () => {
|
||||
await fetchAndRender('http://localhost/?from_email=true');
|
||||
|
||||
const startCourseButton = screen.getByRole('link', { name: messages.start.defaultMessage });
|
||||
fireEvent.click(startCourseButton);
|
||||
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.course.home.resume_course.clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
event_type: 'start',
|
||||
from_email: true,
|
||||
url: `${getConfig().LMS_BASE_URL}/courses/${courseId}/jump_to/block-v1:edX+Test+Block@12345abcde`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Suggested schedule alerts', () => {
|
||||
@@ -355,6 +359,27 @@ describe('Outline Tab', () => {
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('post goal via query param', async () => {
|
||||
setTabData({
|
||||
course_goals: {
|
||||
weekly_learning_goal_enabled: true,
|
||||
},
|
||||
});
|
||||
const spy = jest.spyOn(thunks, 'saveWeeklyLearningGoal');
|
||||
sendTrackEvent.mockClear();
|
||||
|
||||
await fetchAndRender('http://localhost/?weekly_goal=3');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.goal.days-per-week.changed', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
is_staff: false,
|
||||
num_days: 3,
|
||||
reminder_selected: true,
|
||||
triggeredFromEmail: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('weekly learning goal is not set', () => {
|
||||
beforeEach(async () => {
|
||||
setTabData({
|
||||
@@ -659,7 +684,6 @@ describe('Outline Tab', () => {
|
||||
cert_status: CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE,
|
||||
cert_web_view_url: null,
|
||||
certificate_available_date: tomorrow.toISOString(),
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
@@ -687,7 +711,6 @@ describe('Outline Tab', () => {
|
||||
cert_data: {
|
||||
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
|
||||
cert_web_view_url: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
@@ -756,7 +779,6 @@ describe('Outline Tab', () => {
|
||||
cert_data: {
|
||||
cert_status: CERT_STATUS_TYPE.REQUESTING,
|
||||
cert_web_view_url: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
@@ -790,50 +812,7 @@ describe('Outline Tab', () => {
|
||||
org_key: 'edX',
|
||||
});
|
||||
});
|
||||
it('tracks download cert button', async () => {
|
||||
sendTrackEvent.mockClear();
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
||||
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||
setMetadata({ is_enrolled: true });
|
||||
setTabData({
|
||||
cert_data: {
|
||||
cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
|
||||
cert_web_view_url: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
{
|
||||
date_type: 'course-end-date',
|
||||
date: yesterday.toISOString(),
|
||||
title: 'End',
|
||||
},
|
||||
{
|
||||
date_type: 'certificate-available-date',
|
||||
date: tomorrow.toISOString(),
|
||||
title: 'Cert Available',
|
||||
},
|
||||
{
|
||||
date_type: 'verification-deadline-date',
|
||||
date: tomorrow.toISOString(),
|
||||
link_text: 'Verify',
|
||||
title: 'Verification Upgrade Deadline',
|
||||
},
|
||||
],
|
||||
});
|
||||
await fetchAndRender();
|
||||
sendTrackEvent.mockClear();
|
||||
const requestingButton = screen.getByRole('button', { name: 'View my certificate' });
|
||||
fireEvent.click(requestingButton);
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_downloadable_button.clicked',
|
||||
{
|
||||
courserun_key: courseId,
|
||||
is_staff: false,
|
||||
org_key: 'edX',
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks unverified cert button', async () => {
|
||||
sendTrackEvent.mockClear();
|
||||
const now = new Date();
|
||||
@@ -844,7 +823,6 @@ describe('Outline Tab', () => {
|
||||
cert_data: {
|
||||
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
|
||||
cert_web_view_url: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
@@ -932,7 +910,6 @@ describe('Outline Tab', () => {
|
||||
cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
|
||||
cert_web_view_url: 'certificate/testuuid',
|
||||
certificate_available_date: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
@@ -958,7 +935,6 @@ describe('Outline Tab', () => {
|
||||
cert_status: CERT_STATUS_TYPE.REQUESTING,
|
||||
cert_web_view_url: null,
|
||||
certificate_available_date: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
@@ -975,33 +951,6 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Certificate (pdf) Complete Alert', () => {
|
||||
it('appears', async () => {
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
||||
setMetadata({ is_enrolled: true });
|
||||
setTabData({
|
||||
cert_data: {
|
||||
cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
|
||||
cert_web_view_url: null,
|
||||
certificate_available_date: null,
|
||||
download_url: 'download/url',
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
{
|
||||
date_type: 'course-end-date',
|
||||
date: yesterday.toISOString(),
|
||||
title: 'End',
|
||||
},
|
||||
],
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByText('Congratulations! Your certificate is ready.')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('link', { name: 'Download my certificate' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Proctoring Info Panel', () => {
|
||||
const onboardingReleaseDate = new Date();
|
||||
onboardingReleaseDate.setDate(new Date().getDate() - 7);
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import {
|
||||
FormattedMessage,
|
||||
FormattedTime,
|
||||
@@ -28,25 +27,16 @@ function SequenceLink({
|
||||
complete,
|
||||
description,
|
||||
due,
|
||||
legacyWebUrl,
|
||||
showLink,
|
||||
title,
|
||||
} = sequence;
|
||||
const {
|
||||
userTimezone,
|
||||
} = useModel('outline', courseId);
|
||||
const {
|
||||
canLoadCourseware,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
// canLoadCourseware is true if the Courseware MFE is enabled, false otherwise
|
||||
const coursewareUrl = (
|
||||
canLoadCourseware
|
||||
? <Link to={`/course/${courseId}/${id}`}>{title}</Link>
|
||||
: <Hyperlink destination={legacyWebUrl}>{title}</Hyperlink>
|
||||
);
|
||||
const coursewareUrl = <Link to={`/course/${courseId}/${id}`}>{title}</Link>;
|
||||
const displayTitle = showLink ? coursewareUrl : title;
|
||||
|
||||
return (
|
||||
|
||||
@@ -33,7 +33,6 @@ function CertificateStatusAlert({ intl, payload }) {
|
||||
courseEndDate,
|
||||
courseId,
|
||||
certURL,
|
||||
isWebCert,
|
||||
userTimezone,
|
||||
org,
|
||||
notPassingCourseEnded,
|
||||
@@ -79,11 +78,7 @@ function CertificateStatusAlert({ intl, payload }) {
|
||||
);
|
||||
} else if (certStatus === CERT_STATUS_TYPE.DOWNLOADABLE) {
|
||||
alertProps.header = intl.formatMessage(certMessages.certStatusDownloadableHeader);
|
||||
if (isWebCert) {
|
||||
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.viewableButton);
|
||||
} else {
|
||||
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.downloadableButton);
|
||||
}
|
||||
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.viewableButton);
|
||||
alertProps.buttonVisible = true;
|
||||
alertProps.buttonLink = certURL;
|
||||
alertProps.buttonAction = () => {
|
||||
@@ -204,7 +199,6 @@ CertificateStatusAlert.propTypes = {
|
||||
courseEndDate: PropTypes.string,
|
||||
courseId: PropTypes.string,
|
||||
certURL: PropTypes.string,
|
||||
isWebCert: PropTypes.bool,
|
||||
userTimezone: PropTypes.string,
|
||||
org: PropTypes.string,
|
||||
notPassingCourseEnded: PropTypes.bool,
|
||||
|
||||
@@ -51,10 +51,8 @@ function useCertificateStatusAlert(courseId) {
|
||||
certStatus,
|
||||
certWebViewUrl,
|
||||
certificateAvailableDate,
|
||||
downloadUrl,
|
||||
} = certData || {};
|
||||
const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
|
||||
const isWebCert = downloadUrl === null;
|
||||
const isVerifiedEnrollmentMode = (
|
||||
enrollmentMode !== null
|
||||
&& enrollmentMode !== undefined
|
||||
@@ -63,9 +61,6 @@ function useCertificateStatusAlert(courseId) {
|
||||
let certURL = '';
|
||||
if (certWebViewUrl) {
|
||||
certURL = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
|
||||
} else if (downloadUrl) {
|
||||
// PDF Certificate
|
||||
certURL = downloadUrl;
|
||||
}
|
||||
const hasAlertingCertStatus = verifyCertStatusType(certStatus);
|
||||
|
||||
@@ -87,7 +82,6 @@ function useCertificateStatusAlert(courseId) {
|
||||
courseId,
|
||||
courseEndDate: endBlock && endBlock.date,
|
||||
userTimezone,
|
||||
isWebCert,
|
||||
org,
|
||||
notPassingCourseEnded,
|
||||
tabs,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { Button, Card } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -16,6 +18,9 @@ function StartOrResumeCourseCard({ intl }) {
|
||||
org,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const [fromEmail, setFromEmail] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const eventProperties = {
|
||||
org_key: org,
|
||||
courserun_key: courseId,
|
||||
@@ -37,9 +42,25 @@ function StartOrResumeCourseCard({ intl }) {
|
||||
...eventProperties,
|
||||
event_type: hasVisitedCourse ? 'resume' : 'start',
|
||||
url: resumeCourseUrl,
|
||||
from_email: fromEmail,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const currentParams = new URLSearchParams(location.search);
|
||||
const fromEmailQueryParam = Boolean(currentParams.get('from_email'));
|
||||
if (fromEmailQueryParam) {
|
||||
setFromEmail(true);
|
||||
|
||||
// Deleting the from_email query param as it only needs to be set once
|
||||
// whenever passed in query params.
|
||||
currentParams.delete('from_email');
|
||||
history.replace({
|
||||
search: currentParams.toString(),
|
||||
});
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
return (
|
||||
<Card className="mb-3 raised-card" data-testid="start-resume-card">
|
||||
<Card.Header
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Form, Card, Icon } from '@edx/paragon';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
@@ -32,8 +34,9 @@ function WeeklyLearningGoalCard({
|
||||
const [daysPerWeekGoal, setDaysPerWeekGoal] = useState(daysPerWeek);
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const [isGetReminderSelected, setGetReminderSelected] = useState(subscribedToReminders);
|
||||
const location = useLocation();
|
||||
|
||||
function handleSelect(days) {
|
||||
function handleSelect(days, triggeredFromEmail = false) {
|
||||
// Set the subscription button if this is the first time selecting a goal
|
||||
const selectReminders = daysPerWeekGoal === null ? true : isGetReminderSelected;
|
||||
setGetReminderSelected(selectReminders);
|
||||
@@ -46,6 +49,7 @@ function WeeklyLearningGoalCard({
|
||||
is_staff: administrator,
|
||||
num_days: days,
|
||||
reminder_selected: selectReminders,
|
||||
triggeredFromEmail,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -65,6 +69,21 @@ function WeeklyLearningGoalCard({
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const currentParams = new URLSearchParams(location.search);
|
||||
const weeklyGoal = Number(currentParams.get('weekly_goal'));
|
||||
if ([1, 3, 5].includes(weeklyGoal)) {
|
||||
handleSelect(weeklyGoal, true);
|
||||
|
||||
// Deleting the weekly_goal query param as it only needs to be set once
|
||||
// whenever passed in query params.
|
||||
currentParams.delete('weekly_goal');
|
||||
history.replace({
|
||||
search: currentParams.toString(),
|
||||
});
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
id="courseHome-weeklyLearningGoal"
|
||||
|
||||
@@ -31,6 +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);
|
||||
@@ -956,49 +959,6 @@ describe('Progress Tab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Displays download link', async () => {
|
||||
setTabData({
|
||||
certificate_data: {
|
||||
cert_status: 'downloadable',
|
||||
download_url: 'fake.download.url',
|
||||
},
|
||||
user_has_passing_grade: true,
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByRole('link', { name: 'Download my certificate' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sends events on view of progress tab and on click of downloadable certificate link', async () => {
|
||||
setTabData({
|
||||
certificate_data: {
|
||||
cert_status: 'downloadable',
|
||||
download_url: 'fake.download.url',
|
||||
},
|
||||
user_has_passing_grade: true,
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
is_staff: false,
|
||||
track_variant: 'audit',
|
||||
grade_variant: 'passing',
|
||||
certificate_status_variant: 'earned_downloadable',
|
||||
});
|
||||
|
||||
const downloadCertificateLink = screen.getByRole('link', { name: 'Download my certificate' });
|
||||
fireEvent.click(downloadCertificateLink);
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
|
||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.course_progress.certificate_status.clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
is_staff: false,
|
||||
certificate_status_variant: 'earned_downloadable',
|
||||
});
|
||||
});
|
||||
|
||||
it('Displays webview link', async () => {
|
||||
setTabData({
|
||||
certificate_data: {
|
||||
@@ -1220,6 +1180,65 @@ describe('Progress Tab', () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByTestId('certificate-status-component')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Shows not available messaging before certificates are available to nonpassing learners when theres no certificate data', async () => {
|
||||
setMetadata({
|
||||
can_view_certificate: false,
|
||||
is_enrolled: true,
|
||||
});
|
||||
setTabData({
|
||||
end: tomorrow.toISOString(),
|
||||
certificate_data: undefined,
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText(`Final grades and any earned certificates are scheduled to be available after ${tomorrow.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}.`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Shows not available messaging before certificates are available to passing learners when theres no certificate data', async () => {
|
||||
setMetadata({
|
||||
can_view_certificate: false,
|
||||
is_enrolled: true,
|
||||
});
|
||||
setTabData({
|
||||
end: tomorrow.toISOString(),
|
||||
user_has_passing_grade: true,
|
||||
certificate_data: undefined,
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText(`Final grades and any earned certificates are scheduled to be available after ${tomorrow.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}.`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Shows certificate_available_date if learner is passing', async () => {
|
||||
setMetadata({
|
||||
can_view_certificate: false,
|
||||
is_enrolled: true,
|
||||
});
|
||||
setTabData({
|
||||
end: tomorrow.toISOString(),
|
||||
user_has_passing_grade: true,
|
||||
certificate_data: {
|
||||
cert_status: 'earned_but_not_available',
|
||||
certificate_available_date: overmorrow.toISOString(),
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Certificate status'));
|
||||
expect(screen.getByText(
|
||||
overmorrow.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Credit Information', () => {
|
||||
|
||||
@@ -22,6 +22,8 @@ function CertificateStatus({ intl }) {
|
||||
const {
|
||||
isEnrolled,
|
||||
org,
|
||||
canViewCertificate,
|
||||
userTimezone,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const {
|
||||
@@ -45,6 +47,8 @@ function CertificateStatus({ intl }) {
|
||||
hasScheduledContent,
|
||||
isEnrolled,
|
||||
userHasPassingGrade,
|
||||
null, // CourseExitPageIsActive
|
||||
canViewCertificate,
|
||||
);
|
||||
|
||||
const eventProperties = {
|
||||
@@ -57,12 +61,11 @@ function CertificateStatus({ intl }) {
|
||||
|
||||
let certStatus;
|
||||
let certWebViewUrl;
|
||||
let downloadUrl;
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
if (certificateData) {
|
||||
certStatus = certificateData.certStatus;
|
||||
certWebViewUrl = certificateData.certWebViewUrl;
|
||||
downloadUrl = certificateData.downloadUrl;
|
||||
}
|
||||
|
||||
let certCase;
|
||||
@@ -138,15 +141,10 @@ function CertificateStatus({ intl }) {
|
||||
values={{ dashboardLink, profileLink }}
|
||||
/>
|
||||
);
|
||||
|
||||
if (certWebViewUrl) {
|
||||
certEventName = 'earned_viewable';
|
||||
buttonLocation = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
|
||||
buttonText = intl.formatMessage(messages.viewableButton);
|
||||
} else if (downloadUrl) {
|
||||
certEventName = 'earned_downloadable';
|
||||
buttonLocation = downloadUrl;
|
||||
buttonText = intl.formatMessage(messages.downloadableButton);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -178,10 +176,22 @@ function CertificateStatus({ intl }) {
|
||||
}
|
||||
break;
|
||||
|
||||
// This code shouldn't be hit but coding defensively since switch expects a default statement
|
||||
default:
|
||||
certCase = null;
|
||||
certEventName = 'no_certificate_status';
|
||||
// if user completes a course before certificates are available, treat it as notAvailable
|
||||
// regardless of passing or nonpassing status
|
||||
if (!canViewCertificate) {
|
||||
certCase = 'notAvailable';
|
||||
endDate = intl.formatDate(end, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
...timezoneFormatArgs,
|
||||
});
|
||||
body = intl.formatMessage(messages.notAvailableEndDateBody, { endDate });
|
||||
} else {
|
||||
certCase = null;
|
||||
certEventName = 'no_certificate_status';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,11 +61,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.',
|
||||
description: 'Recommending an action for learner when course certificate is available',
|
||||
},
|
||||
downloadableButton: {
|
||||
id: 'progress.certificateStatus.downloadableButton',
|
||||
defaultMessage: 'Download my certificate',
|
||||
description: 'Button text when learner certifcate status is downloadable',
|
||||
},
|
||||
viewableButton: {
|
||||
id: 'progress.certificateStatus.viewableButton',
|
||||
defaultMessage: 'View my certificate',
|
||||
@@ -76,6 +71,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Certificate status',
|
||||
description: 'Header text when the certifcate is not available',
|
||||
},
|
||||
notAvailableEndDateBody: {
|
||||
id: 'progress.certificateBody.notAvailable.endDate',
|
||||
defaultMessage: 'Final grades and any earned certificates are scheduled to be available after {endDate}.',
|
||||
description: 'Shown for learners who have finished a course before grades and certificates are available.',
|
||||
},
|
||||
upgradeHeader: {
|
||||
id: 'progress.certificateStatus.upgradeHeader',
|
||||
defaultMessage: 'Earn a certificate',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
@@ -41,7 +40,7 @@ function DetailedGrades({ intl }) {
|
||||
<Hyperlink
|
||||
variant="muted"
|
||||
isInline
|
||||
destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`}
|
||||
destination={`/course/${courseId}/home`}
|
||||
onClick={logOutlineLinkClick}
|
||||
tabIndex={gradesFeatureIsFullyLocked ? '-1' : '0'}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
@@ -33,13 +32,13 @@ function RelatedLinks({ intl }) {
|
||||
<h3 className="h4">{intl.formatMessage(messages.relatedLinks)}</h3>
|
||||
<ul className="pl-4">
|
||||
<li>
|
||||
<Hyperlink destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/dates`} onClick={() => logLinkClicked('dates')}>
|
||||
<Hyperlink destination={`/course/${courseId}/dates`} onClick={() => logLinkClicked('dates')}>
|
||||
{intl.formatMessage(messages.datesCardLink)}
|
||||
</Hyperlink>
|
||||
<p>{intl.formatMessage(messages.datesCardDescription)}</p>
|
||||
</li>
|
||||
<li>
|
||||
<Hyperlink destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`} onClick={() => logLinkClicked('course_outline')}>
|
||||
<Hyperlink destination={`/course/${courseId}/home`} onClick={() => logLinkClicked('course_outline')}>
|
||||
{intl.formatMessage(messages.outlineCardLink)}
|
||||
</Hyperlink>
|
||||
<p>{intl.formatMessage(messages.outlineCardDescription)}</p>
|
||||
|
||||
@@ -450,6 +450,7 @@ describe('CoursewareContainer', () => {
|
||||
has_access: false,
|
||||
error_code: errorCode,
|
||||
additional_context_user_message: 'uhoh oh no', // only used by audit_expired
|
||||
developer_message: 'data_sharing_consent_url', // only used by data_sharing_access_required
|
||||
},
|
||||
});
|
||||
|
||||
@@ -465,7 +466,7 @@ describe('CoursewareContainer', () => {
|
||||
const { courseMetadata } = setUpWithDeniedStatus('enrollment_required');
|
||||
await loadContainer();
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
|
||||
expect(global.location.href).toEqual(`http://localhost/course/${courseMetadata.id}/home`);
|
||||
});
|
||||
|
||||
it('should go to course survey for a survey_required error code', async () => {
|
||||
@@ -475,11 +476,18 @@ describe('CoursewareContainer', () => {
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/survey/${courseMetadata.id}`);
|
||||
});
|
||||
|
||||
it('should go to consent page for a data_sharing_access_required error code', async () => {
|
||||
setUpWithDeniedStatus('data_sharing_access_required');
|
||||
await loadContainer();
|
||||
|
||||
expect(global.location.href).toEqual('http://localhost/redirect/consent?consentPath=data_sharing_consent_url');
|
||||
});
|
||||
|
||||
it('should go to course home for an authentication_required error code', async () => {
|
||||
const { courseMetadata } = setUpWithDeniedStatus('authentication_required');
|
||||
await loadContainer();
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
|
||||
expect(global.location.href).toEqual(`http://localhost/course/${courseMetadata.id}/home`);
|
||||
});
|
||||
|
||||
it('should go to dashboard for an unfulfilled_milestones error code', async () => {
|
||||
@@ -504,21 +512,4 @@ describe('CoursewareContainer', () => {
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/dashboard?notlive=${startDate}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirects when canLoadCourseware is false', () => {
|
||||
it('should go to legacy courseware for disabled frontend', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata');
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata', {
|
||||
can_load_courseware: false,
|
||||
});
|
||||
const courseId = courseMetadata.id;
|
||||
const { courseBlocks, sequenceBlocks, unitBlocks } = buildSimpleCourseBlocks(courseId, courseMetadata.name);
|
||||
setUpMockRequests({ courseBlocks, courseMetadata, courseHomeMetadata });
|
||||
history.push(`/course/${courseId}/${sequenceBlocks[0].id}/${unitBlocks[0].id}`);
|
||||
|
||||
await loadContainer();
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/redirect/courseware/${courseMetadata.id}/unit/${unitBlocks[0].id}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { PageRoute } from '@edx/frontend-platform/react';
|
||||
|
||||
import queryString from 'query-string';
|
||||
import PageLoading from '../generic/PageLoading';
|
||||
|
||||
export default () => {
|
||||
@@ -20,18 +21,6 @@ export default () => {
|
||||
/>
|
||||
|
||||
<Switch>
|
||||
<PageRoute
|
||||
path={`${path}/courseware/:courseId/unit/:unitId`}
|
||||
render={({ match }) => {
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/jump_to/${match.params.unitId}?experience=legacy`);
|
||||
}}
|
||||
/>
|
||||
<PageRoute
|
||||
path={`${path}/course-home/:courseId`}
|
||||
render={({ match }) => {
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}/courses/${match.params.courseId}/course/`);
|
||||
}}
|
||||
/>
|
||||
<PageRoute
|
||||
path={`${path}/survey/:courseId`}
|
||||
render={({ match }) => {
|
||||
@@ -44,6 +33,13 @@ export default () => {
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}/dashboard${location.search}`);
|
||||
}}
|
||||
/>
|
||||
<PageRoute
|
||||
path={`${path}/consent/`}
|
||||
render={({ location }) => {
|
||||
const { consentPath } = queryString.parse(location.search);
|
||||
global.location.assign(`${getConfig().LMS_BASE_URL}${consentPath}`);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
|
||||
37
src/courseware/CoursewareRedirectLandingPage.test.jsx
Normal file
37
src/courseware/CoursewareRedirectLandingPage.test.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Router } from 'react-router';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import CoursewareRedirectLandingPage from './CoursewareRedirectLandingPage';
|
||||
|
||||
const redirectUrl = jest.fn();
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useRouteMatch: () => ({
|
||||
path: '/redirect',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('CoursewareRedirectLandingPage', () => {
|
||||
beforeEach(async () => {
|
||||
delete global.location;
|
||||
global.location = { assign: redirectUrl };
|
||||
});
|
||||
|
||||
it('Redirects to correct consent URL', () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/redirect/consent/?consentPath=%2Fgrant_data_sharing_consent'],
|
||||
});
|
||||
|
||||
render(
|
||||
<Router history={history}>
|
||||
<CoursewareRedirectLandingPage />
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(redirectUrl).toHaveBeenCalledWith('http://localhost:18000/grant_data_sharing_consent');
|
||||
});
|
||||
});
|
||||
@@ -55,12 +55,13 @@ function CourseCelebration({ intl }) {
|
||||
const {
|
||||
org,
|
||||
verifiedMode,
|
||||
canViewCertificate,
|
||||
userTimezone,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const {
|
||||
certStatus,
|
||||
certWebViewUrl,
|
||||
downloadUrl,
|
||||
certificateAvailableDate,
|
||||
} = certificateData || {};
|
||||
|
||||
@@ -69,6 +70,7 @@ function CourseCelebration({ intl }) {
|
||||
const dashboardLink = <DashboardLink />;
|
||||
const idVerificationSupportLink = <IdVerificationSupportLink />;
|
||||
const profileLink = <ProfileLink />;
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
let buttonPrefix = null;
|
||||
let buttonLocation;
|
||||
@@ -101,9 +103,6 @@ function CourseCelebration({ intl }) {
|
||||
if (certWebViewUrl) {
|
||||
buttonLocation = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
|
||||
buttonText = intl.formatMessage(messages.viewCertificateButton);
|
||||
} else if (downloadUrl) {
|
||||
buttonLocation = downloadUrl;
|
||||
buttonText = intl.formatMessage(messages.downloadButton);
|
||||
}
|
||||
if (linkedinAddToProfileUrl) {
|
||||
buttonPrefix = (
|
||||
@@ -248,6 +247,29 @@ function CourseCelebration({ intl }) {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!canViewCertificate) {
|
||||
// We reuse the cert event here. Since this default state is so
|
||||
// Similar to the earned_not_available state, this event name should be fine
|
||||
// to cover the same cases.
|
||||
visitEvent = 'celebration_with_unavailable_cert';
|
||||
certHeader = intl.formatMessage(messages.certificateHeaderNotAvailable);
|
||||
const endDate = intl.formatDate(end, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
...timezoneFormatArgs,
|
||||
});
|
||||
message = (
|
||||
<>
|
||||
<p>
|
||||
{intl.formatMessage(messages.certificateNotAvailableEndDateBody, { endDate })}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(messages.certificateNotAvailableBodyAccessCert)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ function CourseExit({ intl }) {
|
||||
userHasPassingGrade,
|
||||
} = useModel('coursewareMeta', courseId);
|
||||
|
||||
const { isMasquerading } = useModel('courseHomeMeta', courseId);
|
||||
const {
|
||||
isMasquerading,
|
||||
canViewCertificate,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const mode = getCourseExitMode(
|
||||
certificateData,
|
||||
@@ -35,6 +38,7 @@ function CourseExit({ intl }) {
|
||||
isEnrolled,
|
||||
userHasPassingGrade,
|
||||
courseExitPageIsActive,
|
||||
canViewCertificate,
|
||||
);
|
||||
|
||||
// Audit users cannot fully complete a course, so we will
|
||||
|
||||
@@ -38,6 +38,9 @@ describe('Course Exit Pages', () => {
|
||||
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 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 = {}) {
|
||||
const extendedCourseMetadata = { ...coursewareMetadata, ...coursewareAttributes };
|
||||
@@ -105,17 +108,6 @@ describe('Course Exit Pages', () => {
|
||||
});
|
||||
|
||||
describe('Course Celebration Experience', () => {
|
||||
it('Displays download link', async () => {
|
||||
setMetadata({
|
||||
certificate_data: {
|
||||
cert_status: 'downloadable',
|
||||
download_url: 'fake.download.url',
|
||||
},
|
||||
});
|
||||
await fetchAndRender(<CourseCelebration />);
|
||||
expect(screen.getByRole('link', { name: 'Download my certificate' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays webview link', async () => {
|
||||
setMetadata({
|
||||
certificate_data: {
|
||||
@@ -363,6 +355,64 @@ describe('Course Exit Pages', () => {
|
||||
expect(screen.queryByText('Same Course')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Shows not available messaging before certificates are available to nonpassing learners when theres no certificate data', async () => {
|
||||
setMetadata({
|
||||
is_enrolled: true,
|
||||
end: tomorrow.toISOString(),
|
||||
user_has_passing_grade: false,
|
||||
certificate_data: undefined,
|
||||
}, {
|
||||
can_view_certificate: false,
|
||||
});
|
||||
await fetchAndRender(<CourseCelebration />);
|
||||
expect(screen.getByText(`Final grades and any earned certificates are scheduled to be available after ${tomorrow.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}.`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Shows not available messaging before certificates are available to passing learners when theres no certificate data', async () => {
|
||||
setMetadata({
|
||||
is_enrolled: true,
|
||||
end: tomorrow.toISOString(),
|
||||
user_has_passing_grade: true,
|
||||
certificate_data: undefined,
|
||||
}, {
|
||||
can_view_certificate: false,
|
||||
});
|
||||
await fetchAndRender(<CourseCelebration />);
|
||||
expect(screen.getByText(`Final grades and any earned certificates are scheduled to be available after ${tomorrow.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}.`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Shows certificate_available_date if learner is passing', async () => {
|
||||
setMetadata({
|
||||
is_enrolled: true,
|
||||
end: tomorrow.toISOString(),
|
||||
user_has_passing_grade: true,
|
||||
certificate_data: {
|
||||
cert_status: 'earned_but_not_available',
|
||||
certificate_available_date: overmorrow.toISOString(),
|
||||
},
|
||||
}, {
|
||||
can_view_certificate: false,
|
||||
});
|
||||
|
||||
await fetchAndRender(<CourseCelebration />);
|
||||
expect(screen.getByText('Your grade and certificate status will be available soon.'));
|
||||
expect(screen.getByText(
|
||||
overmorrow.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Course Non-passing Experience', () => {
|
||||
|
||||
@@ -21,6 +21,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'If you have earned a passing grade, your certificate will be automatically issued.',
|
||||
description: 'Text displayed when course certificate is not yet available to be viewed',
|
||||
},
|
||||
certificateNotAvailableEndDateBody: {
|
||||
id: 'courseCelebration.certificateBody.notAvailable.endDate',
|
||||
defaultMessage: 'Final grades and any earned certificates are scheduled to be available after {endDate}.',
|
||||
description: 'Shown for learners who have finished a course before grades and certificates are available.',
|
||||
},
|
||||
certificateHeaderUnverified: {
|
||||
id: 'courseCelebration.certificateHeader.unverified',
|
||||
defaultMessage: 'You must complete verification to receive your certificate.',
|
||||
@@ -71,11 +76,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Dashboard',
|
||||
description: 'Link to user’s dashboard',
|
||||
},
|
||||
downloadButton: {
|
||||
id: 'courseCelebration.downloadButton',
|
||||
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.',
|
||||
|
||||
@@ -31,6 +31,7 @@ function getCourseExitMode(
|
||||
isEnrolled,
|
||||
userHasPassingGrade,
|
||||
courseExitPageIsActive = null,
|
||||
canImmediatelyViewCertificate = false,
|
||||
) {
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
|
||||
@@ -55,7 +56,7 @@ function getCourseExitMode(
|
||||
if (hasScheduledContent && !userHasPassingGrade) {
|
||||
return COURSE_EXIT_MODES.inProgress;
|
||||
}
|
||||
if (isEligibleForCertificate && !userHasPassingGrade) {
|
||||
if (isEligibleForCertificate && !userHasPassingGrade && canImmediatelyViewCertificate) {
|
||||
return COURSE_EXIT_MODES.nonPassing;
|
||||
}
|
||||
if (isCelebratoryStatus) {
|
||||
@@ -73,12 +74,14 @@ function getCourseExitNavigation(courseId, intl) {
|
||||
userHasPassingGrade,
|
||||
courseExitPageIsActive,
|
||||
} = useModel('coursewareMeta', courseId);
|
||||
const { canViewCertificate } = useModel('courseHomeMeta', courseId);
|
||||
const exitMode = getCourseExitMode(
|
||||
certificateData,
|
||||
hasScheduledContent,
|
||||
isEnrolled,
|
||||
userHasPassingGrade,
|
||||
courseExitPageIsActive,
|
||||
canViewCertificate,
|
||||
);
|
||||
const exitActive = exitMode !== COURSE_EXIT_MODES.disabled;
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ SidebarBase.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
ariaLabel: PropTypes.string.isRequired,
|
||||
sidebarId: PropTypes.string.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.element.isRequired,
|
||||
showTitleBar: PropTypes.bool,
|
||||
width: PropTypes.string,
|
||||
@@ -97,6 +97,7 @@ SidebarBase.propTypes = {
|
||||
SidebarBase.defaultProps = {
|
||||
width: '31rem',
|
||||
showTitleBar: true,
|
||||
className: '',
|
||||
};
|
||||
|
||||
export default injectIntl(SidebarBase);
|
||||
|
||||
@@ -16,7 +16,7 @@ function DiscussionsSidebar({ intl }) {
|
||||
courseId,
|
||||
} = useContext(SidebarContext);
|
||||
const topic = useModel('discussionTopics', unitId);
|
||||
if (!topic) {
|
||||
if (!topic?.id) {
|
||||
return null;
|
||||
}
|
||||
const discussionsUrl = `${getConfig().DISCUSSIONS_MFE_BASE_URL}/${courseId}/topics/${topic.id}`;
|
||||
@@ -31,8 +31,6 @@ function DiscussionsSidebar({ intl }) {
|
||||
<iframe
|
||||
src={`${discussionsUrl}?inContext`}
|
||||
className="d-flex w-100 border-0"
|
||||
// Need to set minHeight so there is enough space for the add post UI
|
||||
// TODO: Use postMessage API to dynamically update iframe size.
|
||||
style={{ minHeight: '60rem' }}
|
||||
title={intl.formatMessage(messages.discussionsTitle)}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import React from 'react';
|
||||
import {
|
||||
initializeMockApp, initializeTestStore, render, screen,
|
||||
} from '../../../../../setupTest';
|
||||
import { executeThunk } from '../../../../../utils';
|
||||
import { buildTopicsFromUnits } from '../../../../data/__factories__/discussionTopics.factory';
|
||||
import { getCourseDiscussionTopics } from '../../../../data/thunks';
|
||||
import SidebarContext from '../../SidebarContext';
|
||||
import DiscussionsSidebar from './DiscussionsSidebar';
|
||||
|
||||
initializeMockApp();
|
||||
|
||||
describe('Discussions Trigger', () => {
|
||||
let axiosMock;
|
||||
let mockData;
|
||||
let courseId;
|
||||
let unitId;
|
||||
|
||||
beforeEach(async () => {
|
||||
const store = await initializeTestStore({
|
||||
excludeFetchCourse: false,
|
||||
excludeFetchSequence: false,
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
const state = store.getState();
|
||||
courseId = state.courseware.courseId;
|
||||
[unitId] = Object.keys(state.models.units);
|
||||
|
||||
mockData = {
|
||||
courseId,
|
||||
unitId,
|
||||
currentSidebar: 'DISCUSSIONS',
|
||||
};
|
||||
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/${courseId}`).reply(
|
||||
200,
|
||||
{
|
||||
provider: 'openedx',
|
||||
},
|
||||
);
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/discussion/v2/course_topics/${courseId}`)
|
||||
.reply(200, buildTopicsFromUnits(state.models.units));
|
||||
await executeThunk(getCourseDiscussionTopics(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
function renderWithProvider(testData = {}) {
|
||||
const { container } = render(
|
||||
<SidebarContext.Provider value={{ ...mockData, ...testData }}>
|
||||
<DiscussionsSidebar />
|
||||
</SidebarContext.Provider>,
|
||||
);
|
||||
return container;
|
||||
}
|
||||
|
||||
it('should show up if unit discussions associated with it', async () => {
|
||||
renderWithProvider();
|
||||
expect(screen.queryByTitle('Discussions')).toBeInTheDocument();
|
||||
expect(screen.queryByTitle('Discussions'))
|
||||
.toHaveAttribute('src', `http://localhost:2002/${courseId}/topics/topic-1?inContext`);
|
||||
});
|
||||
|
||||
it('should show nothing if unit has no discussions associated with it', async () => {
|
||||
renderWithProvider({ unitId: 'no-discussion' });
|
||||
expect(screen.queryByTitle('Discussions')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,6 @@ export function normalizeLearningSequencesData(learningSequencesData) {
|
||||
models.sequences[seqId] = {
|
||||
id: seqId,
|
||||
title: sequence.title,
|
||||
legacyWebUrl: `${getConfig().LMS_BASE_URL}/courses/${learningSequencesData.course_key}/jump_to/${seqId}?experience=legacy`,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -106,7 +105,6 @@ function normalizeMetadata(metadata) {
|
||||
start: data.start,
|
||||
enrollmentMode: data.enrollment.mode,
|
||||
isEnrolled: data.enrollment.is_active,
|
||||
canViewLegacyCourseware: data.can_view_legacy_courseware,
|
||||
license: data.license,
|
||||
userTimezone: data.user_timezone,
|
||||
showCalculator: data.show_calculator,
|
||||
|
||||
@@ -171,13 +171,11 @@ describe('Courseware Service', () => {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
|
||||
title: 'Can access',
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
|
||||
legacyWebUrl: `${getConfig().LMS_BASE_URL}/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible?experience=legacy`,
|
||||
},
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released': {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
|
||||
title: 'Released and inaccessible',
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
|
||||
legacyWebUrl: `${getConfig().LMS_BASE_URL}/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@released?experience=legacy`,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -271,7 +269,6 @@ describe('Courseware Service', () => {
|
||||
}),
|
||||
show_calculator: boolean(false),
|
||||
original_user_is_staff: boolean(true),
|
||||
can_view_legacy_courseware: boolean(true),
|
||||
is_staff: boolean(true),
|
||||
course_access: like({
|
||||
has_access: true,
|
||||
@@ -286,7 +283,7 @@ describe('Courseware Service', () => {
|
||||
user_has_passing_grade: boolean(false),
|
||||
course_exit_page_is_active: boolean(false),
|
||||
certificate_data: {
|
||||
cert_status: string('audit_passing'), cert_web_view_url: null, download_url: null, certificate_available_date: null,
|
||||
cert_status: string('audit_passing'), cert_web_view_url: null, certificate_available_date: null,
|
||||
},
|
||||
verify_identity_url: null,
|
||||
verification_status: string('none'),
|
||||
@@ -321,7 +318,6 @@ describe('Courseware Service', () => {
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
enrollmentMode: 'audit',
|
||||
isEnrolled: true,
|
||||
canViewLegacyCourseware: true,
|
||||
license: 'all-rights-reserved',
|
||||
userTimezone: null,
|
||||
showCalculator: false,
|
||||
@@ -332,7 +328,6 @@ describe('Courseware Service', () => {
|
||||
certificateData: {
|
||||
certStatus: 'audit_passing',
|
||||
certWebViewUrl: null,
|
||||
downloadUrl: null,
|
||||
certificateAvailableDate: null,
|
||||
},
|
||||
timeOffsetMillis: 0,
|
||||
|
||||
@@ -95,9 +95,7 @@ export function fetchCourse(courseId) {
|
||||
logError(courseHomeMetadataResult.reason);
|
||||
}
|
||||
if (fetchedMetadata && fetchedCourseHomeMetadata) {
|
||||
if (courseHomeMetadataResult.value.courseAccess.hasAccess
|
||||
&& courseHomeMetadataResult.value.canLoadCourseware
|
||||
&& fetchedOutline) {
|
||||
if (courseHomeMetadataResult.value.courseAccess.hasAccess && fetchedOutline) {
|
||||
// User has access
|
||||
dispatch(fetchCourseSuccess({ courseId }));
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import {
|
||||
useCallback, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
|
||||
export function useEventListener(type, handler) {
|
||||
// We use this ref so that we can hold a reference to the currently active event listener.
|
||||
@@ -19,3 +21,41 @@ export function useEventListener(type, handler) {
|
||||
return () => global.removeEventListener(type, eventListenerRef.current);
|
||||
}, [type, handler]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks up post messages to callbacks
|
||||
* @param {Object.<string, function>} events A mapping of message type to callback
|
||||
*/
|
||||
export function useIFramePluginEvents(events) {
|
||||
const receiveMessage = useCallback(({ data }) => {
|
||||
const {
|
||||
type,
|
||||
payload,
|
||||
} = data;
|
||||
if (events[type]) {
|
||||
events[type](payload);
|
||||
}
|
||||
}, [events]);
|
||||
useEventListener('message', receiveMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to monitor message about changes in iframe content height
|
||||
* @param onIframeLoaded A callback for when the frame is loaded
|
||||
* @returns {[boolean, number]}
|
||||
*/
|
||||
export function useIFrameHeight(onIframeLoaded = null) {
|
||||
const [iframeHeight, setIframeHeight] = useState(null);
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const receiveResizeMessage = useCallback(({ height }) => {
|
||||
setIframeHeight(height);
|
||||
if (!hasLoaded && !iframeHeight && height > 0) {
|
||||
setHasLoaded(true);
|
||||
if (onIframeLoaded) {
|
||||
onIframeLoaded();
|
||||
}
|
||||
}
|
||||
}, [setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onIframeLoaded]);
|
||||
useIFramePluginEvents({ 'plugin.resize': receiveResizeMessage });
|
||||
return [hasLoaded, iframeHeight];
|
||||
}
|
||||
|
||||
45
src/generic/hooks.test.jsx
Normal file
45
src/generic/hooks.test.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { useEventListener, useIFrameHeight } from './hooks';
|
||||
|
||||
describe('Hooks', () => {
|
||||
test('useEventListener', async () => {
|
||||
const handler = jest.fn();
|
||||
const TestComponent = () => {
|
||||
useEventListener('message', handler);
|
||||
return (<div data-testid="testid" />);
|
||||
};
|
||||
render(<TestComponent />);
|
||||
|
||||
await screen.findByTestId('testid');
|
||||
window.postMessage({ test: 'test' }, '*');
|
||||
await waitFor(() => expect(handler).toHaveBeenCalled());
|
||||
});
|
||||
test('useIFrameHeight', async () => {
|
||||
const onLoaded = jest.fn();
|
||||
const TestComponent = () => {
|
||||
const [hasLoaded, height] = useIFrameHeight(onLoaded);
|
||||
return (
|
||||
<div data-testid="testid">
|
||||
<span data-testid="loaded">
|
||||
{String(hasLoaded)}
|
||||
</span>
|
||||
<span data-testid="height">
|
||||
{String(height)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
render(<TestComponent />);
|
||||
|
||||
await screen.findByTestId('testid');
|
||||
expect(screen.getByTestId('loaded')).toHaveTextContent('false');
|
||||
expect(screen.getByTestId('height')).toHaveTextContent('null');
|
||||
window.postMessage({
|
||||
type: 'plugin.resize',
|
||||
payload: { height: 1234 },
|
||||
}, '*');
|
||||
await waitFor(() => expect(onLoaded).toHaveBeenCalled());
|
||||
await waitFor(() => expect(screen.getByTestId('loaded')).toHaveTextContent('true'));
|
||||
expect(screen.getByTestId('height')).toHaveTextContent('1234');
|
||||
});
|
||||
});
|
||||
@@ -98,9 +98,9 @@ export function SupportMissionBullet() {
|
||||
<CheckmarkBullet />
|
||||
<FormattedMessage
|
||||
id="learning.generic.upsell.supportMissionBullet"
|
||||
defaultMessage="Support our {missionInBoldText} at edX"
|
||||
defaultMessage="Support our {missionInBoldText} at {siteName}"
|
||||
description="Bullet encouraging user to support edX's goals."
|
||||
values={{ missionInBoldText }}
|
||||
values={{ missionInBoldText, siteName: getConfig().SITE_NAME }}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
"learning.goals.unsubscribe.header": "قمت بالغاء اشتراكك في شعارات التذكيرية لاهداف",
|
||||
"learning.goals.unsubscribe.loading": "يجري الغاء الاشتراك...",
|
||||
"learning.goals.unsubscribe.errorDescription": "لم نستطع الغاء اشتراكك في اشعارات التذكيرية باهدافك على بريدك الالكتروني. رجاءا حاول مرة اخرى او {contactSupport} للمساعدة.",
|
||||
"learning.outline.alert.cert.when": "تنتهي هذه الدورة التدريبية في {courseEndDateFormatted}. تمت جدولة الدرجات النهائية والشهادات\n لتكون متاحة بعد {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header": "ستكون درجتك وشهادتك جاهزة قريبًا!",
|
||||
"learning.outline.alert.cert.earnedNotAvailable": "This course ends on {courseEndDateFormatted}. Final grades and any earned certificates are\n scheduled to be available after {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header.v2": "Your grade and certificate status will be available soon.",
|
||||
"cert.alert.earned.ready.header": "ألف مبروك! شهادتك جاهزة.",
|
||||
"cert.alert.notPassing.header": "أنت غير مؤهل بعد للحصول على شهادة",
|
||||
"cert.alert.notPassing.button": "عرض الدرجات",
|
||||
@@ -113,7 +113,7 @@
|
||||
"learning.outline.sequence-due": "{description} في{assignmentDue}",
|
||||
"progress.certificateStatus.unverifiedBody": "لإنشاء شهادة ، يجب عليك إكمال عملية التحقق من الهوية. {idVerificationSupportLink}.",
|
||||
"progress.certificateStatus.downloadableBody": "اعرض إنجازاتك على لينكد ان أو على سيرتك الذاتية اليوم. يمكنك تنزيل شهادتك الآن والوصول إليها في أي وقت من لوحة التحكم والملف الشخصي.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "تنتهي هذه الدورة التدريبية في {endDate} ومن المقرر أن تكون الدرجات النهائية والشهادات\n متاحة بعد {certAvailableDate}.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",
|
||||
"progress.certificateStatus.notPassingHeader": "حالة الشهادة",
|
||||
"progress.certificateStatus.notPassingBody": "من أجل التأهل للحصول على شهادة ، يجب أن تكون حاصلاً على درجة النجاح.",
|
||||
"progress.certificateStatus.inProgressHeader": "المزيد من المحتوى قريبا!",
|
||||
@@ -125,13 +125,13 @@
|
||||
"progress.certificateStatus.unverifiedButton": "التحقق من الهوية",
|
||||
"progress.certificateStatus.courseCelebration.verificationPending": "عملية التحقق من المعرّف الخاص بك معلّقة وستتوفر شهادتك بمجرد الموافقة.",
|
||||
"progress.certificateStatus.downloadableHeader": "!شهادتك جاهزة",
|
||||
"progress.certificateStatus.downloadableButton": "تحميل شهاداتي",
|
||||
"progress.certificateStatus.viewableButton": "عرض شهاداتي",
|
||||
"progress.certificateStatus.notAvailableHeader": "حالة الشهادة",
|
||||
"progress.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",
|
||||
"progress.certificateStatus.upgradeHeader": "احصل على شهادة.",
|
||||
"progress.certificateStatus.upgradeBody": "أنت في مسجل في المساق كمستمع ولست مؤهلاً للحصول على شهادة. من أجل الحصول على شهادة ، قم بترقية تسجيلك في المسافق اليوم.",
|
||||
"progress.certificateStatus.upgradeButton": "الترقية الآن",
|
||||
"progress.certificateStatus.unverifiedHomeHeader": "يجب التحقق من هويتك لتتمكن من الحصول على شهادة!",
|
||||
"progress.certificateStatus.unverifiedHomeHeader.v2": "Verify your identity to qualify for a certificate.",
|
||||
"progress.certificateStatus.unverifiedHomeButton": "تحقق من هويتي",
|
||||
"progress.certificateStatus.unverifiedHomeBody": "من أجل إنشاء شهادة لهذه الدورة ، يجب عليك إكمال عملية التحقق من الهوية.",
|
||||
"progress.completion.donut.label": "مكتمل",
|
||||
@@ -262,6 +262,7 @@
|
||||
"notes.button.hide": "إخفاء الملاحظات",
|
||||
"courseExit.catalogSearchSuggestion": "هل تطمح إلى تعلّم المزيد؟{searchOurCatalogLink} لاستكشاف المزيد من المساقات والبرامج.",
|
||||
"courseCelebration.certificateBody.available": "اعرض إنجازاتك على لينكد إن أو سيرتك الذاتية اليوم.\nيمكنك تنزيل الشهادة الآن والوصول إليها في أي وقت من\n{dashboardLink} و{profileLink}.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate.v2": "This course ends on {endDate}. Final grades and any earned certificates are\n scheduled to be available after {certAvailableDate}.",
|
||||
"courseCelebration.certificateBody.unverified": "لإنشاء شهادة يجب عليك إتمام عملية التحقق من الهوية.\n{idVerificationSupportLink} الآن.",
|
||||
"courseCelebration.certificateBody.upgradable": "لم يفت الأوان للترقية. بالنسبة لـ {price} ستقوم بإلغاء تأمين الوصول إلى كافة أنواع \nالواجبات في هذا المساق. عند الانتهاء، ستحصل على شهادة تم التحقق منها وهي إحدى\nالوثائق القيّمة لتحسين فرصك الوظيفية وتطويرك المهني، أو لتسليط الضوء على\nشهادة في التطبيقات التعليمية.",
|
||||
"courseCelebration.upgradeDiscountCodePrompt": "استخدم الرمز {code} عند إتمام الطلب لخصم {percent}%!",
|
||||
@@ -274,7 +275,7 @@
|
||||
"courseCelebration.dashboardInfo": "يمكنك الوصول إلى هذا المساق ومواده على لوحة معلوماتك {dashboardLink}.",
|
||||
"courseExit.programs.applyForCredit": "تقدم بطلب للحصول على ائتمان",
|
||||
"courseCelebration.certificateHeader.downloadable": "!شهادتك جاهزة",
|
||||
"courseCelebration.certificateHeader.notAvailable": "سيكون كل من درجتك وشهادتك جاهزين قريبًا!",
|
||||
"courseCelebration.certificateHeader.notAvailable": "Your grade and certificate status will be available soon.",
|
||||
"courseCelebration.certificateBody.notAvailable.accessCertificate": "إذا كنت قد حصلت على درجة النجاح ، فسيتم إصدار شهادتك تلقائيًا.",
|
||||
"courseCelebration.certificateHeader.unverified": "يجب إكمال عملية التحقق للحصول على شهادتك",
|
||||
"courseCelebration.certificateHeader.requestable": "تهانينا، لقد تأهلت للحصول على شهادة!",
|
||||
@@ -286,7 +287,6 @@
|
||||
"courseExit.courseInProgressDescription": "يبدو أن هناك المزيد من المحتوى في هذه الدورة والذي سيتم إصداره في المستقبل. ابقى على اطلاع بتحديثات البريد الإلكتروني أو تحقق مرة أخرى من الدورة التدريبية الخاصة بك لمعرفة وقت توفر هذا المحتوى.",
|
||||
"courseExit.courseInProgressHeader": "المزيد من المحتوى قريبا!",
|
||||
"courseExit.dashboardLink": "لوحة المعلومات",
|
||||
"courseCelebration.downloadButton": "تحميل شهاداتي",
|
||||
"courseExit.endOfCourseDescription": "لسوء الحظ، لست مؤهلًا الآن للحصول على شهادة. تحتاج إلى تحقيق درجة الاجتياز تؤهلك للحصول على شهادة.",
|
||||
"courseExit.endOfCourseHeader": "لقد أتممت المساق!",
|
||||
"courseExit.endOfCourseTitle": "نهاية المساق",
|
||||
@@ -396,7 +396,7 @@
|
||||
"learning.generic.upsell.fullAccessBullet.fullAccess": "Full access",
|
||||
"learning.generic.upsell.fullAccessBullet": "{fullAccessInBoldText} to course content and materials, even after the course ends",
|
||||
"learning.generic.upsell.supportMissionBullet.mission": "mission",
|
||||
"learning.generic.upsell.supportMissionBullet": "ادعم {missionInBoldText} في edX",
|
||||
"learning.generic.upsell.supportMissionBullet": "Support our {missionInBoldText} at {siteName}",
|
||||
"masquerade-widget.userName.error.generic": "حدث خطأ؛ يرجى المحاولة مرة أخرى.",
|
||||
"masquerade-widget.userName.input.placeholder": "اسم المستخدم أو البريد الإلكتروني",
|
||||
"masquerade-widget.userName.input.label": "عرف كهذا المستخدم",
|
||||
@@ -408,7 +408,7 @@
|
||||
"tours.button.okay": "تمام",
|
||||
"tours.button.beginTour": "ابدأ الجولة",
|
||||
"tours.button.launchTour": "انطلاق الجولة",
|
||||
"tours.newUserModal.body": "Let’s take a quick tour of edX so you can get the most out of your course.",
|
||||
"tours.newUserModal.body": "Let’s take a quick tour of {siteName} so you can get the most out of your course.",
|
||||
"tours.newUserModal.title.welcome": "مرحبًا بك في",
|
||||
"tours.button.skipForNow": "تخطي في الوقت الراهن",
|
||||
"tours.datesCheckpoint.body": "Important dates can help you stay on track.",
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"account-activation.resend.link": "reenviar el email",
|
||||
"learning.logistration.alert": "Para ver el contenido del curso, {signIn} o {register}.",
|
||||
"account-activation.alert.title": "Activa tu cuenta para poder volver a conectarte",
|
||||
"learn.sequence.entranceExamTextNotPassing": "To access course materials, you must score {entranceExamMinimumScorePct}% or higher on this exam. Your current score is {entranceExamCurrentScore}%.",
|
||||
"learn.sequence.entranceExamTextPassed": "Your score is {entranceExamCurrentScore}%. You have passed the entrance exam.",
|
||||
"learn.sequence.entranceExamTextNotPassing": "Para acceder a los materiales del curso, debe obtener un {entranceExamMinimumScorePct}% o más en este examen. Su puntaje actual es {entranceExamCurrentScore}%.",
|
||||
"learn.sequence.entranceExamTextPassed": "Tu puntuación es {entranceExamCurrentScore}%. Has aprobado el examen de ingreso.",
|
||||
"learning.dates.badge.completed": "Completado",
|
||||
"learning.dates.badge.dueNext": "Próximo vencimiento",
|
||||
"learning.dates.badge.pastDue": "Vencido",
|
||||
@@ -34,8 +34,8 @@
|
||||
"learning.goals.unsubscribe.header": "Te has desinscrito de los recordatorios de objetivos.",
|
||||
"learning.goals.unsubscribe.loading": "Desinscribiendo...",
|
||||
"learning.goals.unsubscribe.errorDescription": "No fue posible desinscribirte de tus correos de recordatorios de objetivos. Por favor inténtalo más tarde o ponte en contacto con el equipo de soporte para solicitar ayuda {contactSupport}",
|
||||
"learning.outline.alert.cert.when": "Este curso finaliza el {courseEndDateFormatted}. Las calificaciones finales y los certificados están\n programados para estar disponibles después de {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header": "Tu calificación y tu certificado estarán listos en breve.",
|
||||
"learning.outline.alert.cert.earnedNotAvailable": "Este curso finaliza el {courseEndDateFormatted}. Las calificaciones finales y los certificados obtenidos estarán disponibles después de esta fecha: {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header.v2": "El estado de su calificación y certificado estarán disponible pronto.",
|
||||
"cert.alert.earned.ready.header": "¡Felicitaciones! Tu certificado está listo.",
|
||||
"cert.alert.notPassing.header": "Aún no eres elegible para obtener un certificado",
|
||||
"cert.alert.notPassing.button": "Ver calificaciones",
|
||||
@@ -113,7 +113,7 @@
|
||||
"learning.outline.sequence-due": "Fecha límite para {description}: {assignmentDue}",
|
||||
"progress.certificateStatus.unverifiedBody": "Para generar un certificado, debes completar la verificación de identidad. {idVerificationSupportLink}.",
|
||||
"progress.certificateStatus.downloadableBody": "Muestra tu logro en LinkedIn o en tu currículum. Puedes descargar tu certificado ahora y acceder a él en cualquier momento desde tu panel de estudiante y tu perfil.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "Este curso terminó el {endDate} y las calificaciones finales y los certificados están programados para estar\ndisponibles después de {certAvailableDate}.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "Las calificaciones finales y los certificados obtenidos están programados para estar disponibles después de {endDate}.",
|
||||
"progress.certificateStatus.notPassingHeader": "Estado del certificado",
|
||||
"progress.certificateStatus.notPassingBody": "Para poder obtener un certificado, es necesario tener una calificación de aprobado.",
|
||||
"progress.certificateStatus.inProgressHeader": "¡Pronto habrá más contenido!",
|
||||
@@ -125,13 +125,13 @@
|
||||
"progress.certificateStatus.unverifiedButton": "Verificar tu identidad",
|
||||
"progress.certificateStatus.courseCelebration.verificationPending": "Su verificación de ID está pendiente y su certificado estará disponible una vez que se haya aprobado.",
|
||||
"progress.certificateStatus.downloadableHeader": "¡Tu certificado está disponible!",
|
||||
"progress.certificateStatus.downloadableButton": "Descargar mi certificado",
|
||||
"progress.certificateStatus.viewableButton": "Ver mi certificado",
|
||||
"progress.certificateStatus.notAvailableHeader": "Estado del certificado",
|
||||
"progress.certificateBody.notAvailable.endDate": "Las calificaciones finales y los certificados obtenidos estarán disponibles después de {endDate}.",
|
||||
"progress.certificateStatus.upgradeHeader": "Obtén un certificado",
|
||||
"progress.certificateStatus.upgradeBody": "Estás en la opción auditada y no calificas para un certificado. Para poder obtener un certificado, cambiate a la opción verificada del curso hoy mismo.",
|
||||
"progress.certificateStatus.upgradeButton": "Actualizar Ahora",
|
||||
"progress.certificateStatus.unverifiedHomeHeader": "Verifica tu identidad para obtener un certificado.",
|
||||
"progress.certificateStatus.unverifiedHomeHeader.v2": "Verifique su identidad a fin de calificar para un certificado.",
|
||||
"progress.certificateStatus.unverifiedHomeButton": "Verificar mi identidad",
|
||||
"progress.certificateStatus.unverifiedHomeBody": "Para generar un certificado para este curso, debes completar el proceso de verificación de identidad.",
|
||||
"progress.completion.donut.label": "Completado",
|
||||
@@ -246,7 +246,7 @@
|
||||
"calculator.instruction.table.to.use.operators.type3": "(resistencias en paralelo)",
|
||||
"calculator.instruction.table.to.use.constants": "Constantes",
|
||||
"calculator.instruction.table.to.use.affixes": "Añadidos",
|
||||
"calculator.instruction.table.to.use.affixes.type": "Percent sign (%)",
|
||||
"calculator.instruction.table.to.use.affixes.type": "Signo de porcentaje (%)",
|
||||
"calculator.instruction.table.to.use.basic.functions": "Funciones basicas",
|
||||
"calculator.instruction.table.to.use.trig.functions": "Funciones trigonometricas",
|
||||
"calculator.instruction.table.to.use.scientific.notation": "Notación Científica",
|
||||
@@ -262,6 +262,7 @@
|
||||
"notes.button.hide": "Ocultar Notas",
|
||||
"courseExit.catalogSearchSuggestion": "¿Quieres saber más? {searchOurCatalogLink} para buscar más cursos y programas por explorar.",
|
||||
"courseCelebration.certificateBody.available": "\n Muestra tu logro en LinkedIn o en tu currículum hoy mismo.\n Puedes descargar tu certificado ahora y acceder a él en cualquier momento desde tu\n {dashboardLink} y {profileLink}.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate.v2": "Este curso finaliza el {endDate}. Las calificaciones finales y los certificados obtenidos estarán disponibles después de la siguiente fecha {certAvailableDate}.",
|
||||
"courseCelebration.certificateBody.unverified": "Para generar un certificado, debes completar la verificación de ID.\n {idVerificationSupportLink} ahora.",
|
||||
"courseCelebration.certificateBody.upgradable": "No es demasiado tarde para mejorar de categoría. Por {price}, obtendrás acceso a todas las asignaciones\n calificadas de este curso. Al terminar, recibirás un certificado verificado que es una\n valiosa credencial para mejorar tus perspectivas de trabajo y avanzar en tu carrera, o puedes usar dicho\n certificado para destacarlo en solicitudes universitarias.",
|
||||
"courseCelebration.upgradeDiscountCodePrompt": "Utiliza el código {code} en el momento de la compra para obtener un {percent} % de descuento.",
|
||||
@@ -274,7 +275,7 @@
|
||||
"courseCelebration.dashboardInfo": "Puedes acceder a este curso y a sus materiales en tu {dashboardLink}.",
|
||||
"courseExit.programs.applyForCredit": "Solicitar crédito",
|
||||
"courseCelebration.certificateHeader.downloadable": "¡Tu certificado está disponible!",
|
||||
"courseCelebration.certificateHeader.notAvailable": "Tu calificación y tu certificado estarán listos en breve.",
|
||||
"courseCelebration.certificateHeader.notAvailable": "El estado de su calificación y certificado estarán disponible pronto.",
|
||||
"courseCelebration.certificateBody.notAvailable.accessCertificate": "Si has obtenido una calificación de aprobado, tu certificado se emitirá automáticamente.",
|
||||
"courseCelebration.certificateHeader.unverified": "Debes completar la verificación para recibir tu certificado.",
|
||||
"courseCelebration.certificateHeader.requestable": "¡Felicitaciones, usted califica para recibir un certificado!",
|
||||
@@ -286,7 +287,6 @@
|
||||
"courseExit.courseInProgressDescription": "Parece que hay más contenido en este curso que se publicará en el futuro. Presta atención a las novedades por correo electrónico o consulta tu curso para saber cuándo estará disponible este contenido.",
|
||||
"courseExit.courseInProgressHeader": "¡Pronto habrá más contenido!",
|
||||
"courseExit.dashboardLink": "Panel de Control",
|
||||
"courseCelebration.downloadButton": "Descargar mi certificado",
|
||||
"courseExit.endOfCourseDescription": "Lamentablemente, no puedes obtener un certificado en este momento. Debes recibir una calificación aprobatoria para poder obtener un certificado.",
|
||||
"courseExit.endOfCourseHeader": "¡Has llegado al fin del curso!",
|
||||
"courseExit.endOfCourseTitle": "Fin del curso",
|
||||
@@ -343,8 +343,8 @@
|
||||
"learn.honorCode.agree": "Estoy de acuerdo ",
|
||||
"learn.lockPaywall.title": "Las tareas calificadas están bloqueadas",
|
||||
"learn.lockPaywall.content": "Cámbiate a la opción verificada para obtener acceso a funciones bloqueadas como esta y aprovechar al máximo tu curso.",
|
||||
"learn.lockPaywall.content.pastExpiration": "The upgrade deadline for this course passed. To upgrade, enroll in the next available session. ",
|
||||
"learn.lockPaywall.courseDetails": "View Course Details",
|
||||
"learn.lockPaywall.content.pastExpiration": "La fecha límite de actualización para este curso expiró. Para actualizarlo, inscríbase en la siguiente sesión disponible.",
|
||||
"learn.lockPaywall.courseDetails": "Ver detalles del curso",
|
||||
"learn.lockPaywall.example.alt": "Certificado de ejemplo",
|
||||
"learn.lockPaywall.list.intro": "Cuando te cambias a la opción verificada, tú:",
|
||||
"learn.header.h2.placeholder": "Enunciados de nivel 2 podrían ser creados por proveedores del curso en el futuro. ",
|
||||
@@ -357,8 +357,8 @@
|
||||
"learn.sequence.navigation.next.up.button": "Siguiente: {title}",
|
||||
"learn.sequence.navigation.previous.button": "Anterior",
|
||||
"learn.course.sequence.navigation.mobile.menu": "{current} de {total}",
|
||||
"discussions.sidebar.title": "Discussions",
|
||||
"discussions.sidebar.open.button": "Show discussions tray",
|
||||
"discussions.sidebar.title": "Debates",
|
||||
"discussions.sidebar.open.button": "Mostrar bandeja de discusiones",
|
||||
"learn.redirect.interstitial.message": "Redirigiendo...",
|
||||
"learn.loading.error": "Error: {error}",
|
||||
"learning.celebration.emailBody": "¿A qué dedicas tu tiempo para aprender?",
|
||||
@@ -377,16 +377,16 @@
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "beneficios del cambio",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss": "Perderás todo el acceso a este curso, {includingAnyProgress}, el {date}.",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert": "Cambiarte a la opción verificada permite obtener un certificado verificado y obtener acceso a numerosas funciones. Obtén más información sobre los {benefitsOfUpgrading}.",
|
||||
"learning.generic.upgradeNotification.pastExpiration.content": "The upgrade deadline for this course passed. To upgrade, enroll in the next available session.",
|
||||
"learning.generic.upgradeNotification.pastExpiration.content": "La fecha límite de actualización para este curso expiró. Para actualizarlo, inscríbase en la siguiente sesión disponible.",
|
||||
"learning.generic.upgradeNotification.expirationDays": "Quedan {dayCount, number} {dayCount, plural, \none {day}\nother {days}}",
|
||||
"learning.generic.upgradeNotification.expirationHours": "Quedan {hourCount, number} {hourCount, plural,\none {hour}\nother {hours}}",
|
||||
"learning.generic.upgradeNotification.expirationMinutes": "Queda menos de 1 hora",
|
||||
"learning.generic.upgradeNotification.expiration": "El acceso al curso expirará el {date}",
|
||||
"learning.generic.upgradeNotification.pastExpiration.banner": "Upgrade deadline passed on {date}",
|
||||
"learning.generic.upgradeNotification.pastExpiration.banner": "La fecha límite de actualización expiró el {date}",
|
||||
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% de descuento de bienvenida para estudiantes nuevos",
|
||||
"learning.generic.upgradeNotification.accessExpiration": "Cámbiate a la opción verificada",
|
||||
"learning.generic.upgradeNotification.accessExpirationUrgent": "Vencimiento del acceso al curso",
|
||||
"learning.generic.upgradeNotification.accessExpirationPast": "Course Access Expiration",
|
||||
"learning.generic.upgradeNotification.accessExpirationPast": "Vencimiento del acceso al curso",
|
||||
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Obtenga un certificado verificado",
|
||||
"learning.generic.upgradeNotification.code": "Usa el código {code} al finalizar la compra",
|
||||
"learning.generic.upsell.verifiedCertBullet.verifiedCert": "certificado verificado",
|
||||
@@ -396,7 +396,7 @@
|
||||
"learning.generic.upsell.fullAccessBullet.fullAccess": "Acceso completo",
|
||||
"learning.generic.upsell.fullAccessBullet": "{fullAccessInBoldText} al contenido y los materiales del curso, incluso después de que finalice el curso",
|
||||
"learning.generic.upsell.supportMissionBullet.mission": "misión",
|
||||
"learning.generic.upsell.supportMissionBullet": "Apoya nuestra {missionInBoldText} en edX",
|
||||
"learning.generic.upsell.supportMissionBullet": "Apoye a nuestro {missionInBoldText} en {siteName}",
|
||||
"masquerade-widget.userName.error.generic": "Se ha producido un error. Inténtalo de nuevo.",
|
||||
"masquerade-widget.userName.input.placeholder": "Nombre de usuario o correo electrónico",
|
||||
"masquerade-widget.userName.input.label": "Hazte pasar por este usuario",
|
||||
@@ -408,8 +408,8 @@
|
||||
"tours.button.okay": "Okey",
|
||||
"tours.button.beginTour": "Comenzar recorrido",
|
||||
"tours.button.launchTour": "gira de lanzamiento",
|
||||
"tours.newUserModal.body": "Hagamos un recorrido rápido por edX para que pueda aprovechar al máximo su curso.",
|
||||
"tours.newUserModal.title.welcome": "Bienvenido a tu",
|
||||
"tours.newUserModal.body": "Hagamos un recorrido rápido por {siteName} para que pueda aprovechar al máximo su curso.",
|
||||
"tours.newUserModal.title.welcome": "Te damos la bienvenida a tu",
|
||||
"tours.button.skipForNow": "Saltar por ahora ",
|
||||
"tours.datesCheckpoint.body": "Las fechas importantes pueden ayudarlo a mantenerse encaminado.",
|
||||
"tours.datesCheckpoint.title": "Manténgase al tanto de las fechas clave",
|
||||
@@ -421,11 +421,11 @@
|
||||
"tours.upgradeCheckpoint.title": "Desbloquea tu curso",
|
||||
"tours.weeklyGoalsCheckpoint.body": "Establecer una meta aumenta las probabilidades de completar el curso.",
|
||||
"tours.weeklyGoalsCheckpoint.title": "Establecer un objetivo del curso",
|
||||
"tours.newUserModal.title": "{welcome} {siteName} ¡Claro!",
|
||||
"tours.newUserModal.title": "¡{welcome} curso en {siteName}!",
|
||||
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
|
||||
"learning.effortEstimation.activities": "{activityCount, plural, one {# de actividades} other {# actividades}}",
|
||||
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# minutos} other {# min}}",
|
||||
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# minutos} other {# minutos}}",
|
||||
"learning.effortEstimation.activities": "{activityCount, plural, one {# de actividades} many {# actividades} other {# actividades}}",
|
||||
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# minutos} many {# min} other {# min}}",
|
||||
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# minutos} many {# minutos} other {# minutos}}",
|
||||
"learning.streakCelebration.congratulations": "¡Felicitaciones!",
|
||||
"learning.streakCelebration.body": "Sigue así, ¡estás de buena racha!",
|
||||
"learning.streakCelebration.button": "Sigue así",
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
"learning.goals.unsubscribe.header": "Vous vous êtes désabonné des rappels d'objectifs",
|
||||
"learning.goals.unsubscribe.loading": "Désinscription...",
|
||||
"learning.goals.unsubscribe.errorDescription": "Nous n'avons pas pu vous désinscrire des courriels de rappel d'objectif. Veuillez réessayer plus tard ou {contactSupport} pour obtenir de l'aide.",
|
||||
"learning.outline.alert.cert.when": "Ce cours se termine le {courseEndDateFormatted}. Les notes finales et les attestations\n devraient être disponibles après {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header": "Votre note et votre attestation seront bientôt prêtes !",
|
||||
"learning.outline.alert.cert.earnedNotAvailable": "Ce cours se termine le {courseEndDateFormatted}. Les notes finales et les attestations obtenues\n devraient être disponibles après le {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header.v2": "Le statut de votre note et de votre certificat seront bientôt disponibles.",
|
||||
"cert.alert.earned.ready.header": "Félicitations ! Votre attestation est prête.",
|
||||
"cert.alert.notPassing.header": "Vous n'êtes pas encore éligible pour une attestation",
|
||||
"cert.alert.notPassing.button": "Voir les notes",
|
||||
@@ -113,7 +113,7 @@
|
||||
"learning.outline.sequence-due": "{description} échéance {assignmentDue}",
|
||||
"progress.certificateStatus.unverifiedBody": "Afin de générer une attestation, vous devez effectuer une vérification d'identité. {idVerificationSupportLink}.",
|
||||
"progress.certificateStatus.downloadableBody": "Présentez vos réalisations sur LinkedIn ou votre curriculum vitae aujourd'hui. Vous pouvez télécharger votre certificat maintenant et y accéder à tout moment depuis votre tableau de bord et votre profil.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "Ce cours se termine le {endDate} et les notes finales et les attestations sont programmées pour être\n disponibles après le {certAvailableDate}.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "Les notes finales et toutes les attestations obtenues devraient être disponibles après le {endDate}.",
|
||||
"progress.certificateStatus.notPassingHeader": "État de l'attestation",
|
||||
"progress.certificateStatus.notPassingBody": "Pour être admissible à une attestation, vous devez avoir la note de passage.",
|
||||
"progress.certificateStatus.inProgressHeader": "Plus de contenu sera bientôt disponible!",
|
||||
@@ -125,13 +125,13 @@
|
||||
"progress.certificateStatus.unverifiedButton": "Vérifiez votre identité",
|
||||
"progress.certificateStatus.courseCelebration.verificationPending": "La vérification de votre identité est en attente et votre attestation sera disponible une fois approuvé.",
|
||||
"progress.certificateStatus.downloadableHeader": "Votre attestation est disponible!",
|
||||
"progress.certificateStatus.downloadableButton": "Téléchargez mon attestation",
|
||||
"progress.certificateStatus.viewableButton": "Voir mon attestation",
|
||||
"progress.certificateStatus.notAvailableHeader": "État de l'attestation",
|
||||
"progress.certificateBody.notAvailable.endDate": "Les notes finales et toutes les attestations obtenues devraient être disponibles après le {endDate}.",
|
||||
"progress.certificateStatus.upgradeHeader": "Obtenir un certificat",
|
||||
"progress.certificateStatus.upgradeBody": "Vous êtes dans une piste d'audit et n'êtes pas admissible à une attestation. Afin d'obtenir vers une attestation, mettez à niveau votre cours dès aujourd'hui.",
|
||||
"progress.certificateStatus.upgradeButton": "Mettre à jour dès maintenant",
|
||||
"progress.certificateStatus.unverifiedHomeHeader": "Vérifiez votre identité pour obtenir une attestation !",
|
||||
"progress.certificateStatus.unverifiedHomeHeader.v2": "Vérifiez votre identité pour bénéficier d'un certificat.",
|
||||
"progress.certificateStatus.unverifiedHomeButton": "Vérifiez mon identité",
|
||||
"progress.certificateStatus.unverifiedHomeBody": "Afin de générer une attestation pour ce cours, vous devez compléter le processus de vérification d'identité.",
|
||||
"progress.completion.donut.label": "achevée",
|
||||
@@ -262,6 +262,7 @@
|
||||
"notes.button.hide": "Masquer les notes",
|
||||
"courseExit.catalogSearchSuggestion": "Vous souhaitez en apprendre plus? {searchOurCatalogLink} pour trouver plus de cours et de programmes à explorer.",
|
||||
"courseCelebration.certificateBody.available": "\n Affichez vos accomplissements sur LinkedIn ou votre CV dès aujourd'hui.\n Vous pouvez télécharger votre attestation maintenant et y accéder à tout moment depuis vos\n {dashboardLink} et {profileLink}.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate.v2": "Ce cours se termine le {endDate}. Les notes finales et les certificats obtenus\n devraient être disponibles après le {certAvailableDate}.",
|
||||
"courseCelebration.certificateBody.unverified": "Afin de générer une attestation, vous devez effectuer une vérification d'identité.\n {idVerificationSupportLink} maintenant.",
|
||||
"courseCelebration.certificateBody.upgradable": "Il n’est pas trop tard pour effectuer une mise à niveau. Pour {price}, vous débloquerez l'accès à tous les\n devoirs dans ce cours. À la fin, vous recevrez une attestation qui est une source\n d'informations précieuses pour améliorer vos perspectives d'emploi et faire progresser votre carrière, ou mettre en valeur votre\n attestation dans des demandes d'admission.",
|
||||
"courseCelebration.upgradeDiscountCodePrompt": "Utilisez le code {code} lors du paiement pour {percent}% de réduction!",
|
||||
@@ -274,7 +275,7 @@
|
||||
"courseCelebration.dashboardInfo": "Vous pouvez accéder à ce cours et à ses supports sur votre {dashboardLink}.",
|
||||
"courseExit.programs.applyForCredit": "Demander un crédit",
|
||||
"courseCelebration.certificateHeader.downloadable": "Votre attestation est disponible!",
|
||||
"courseCelebration.certificateHeader.notAvailable": "Votre note et votre attestation seront bientôt prêtes !",
|
||||
"courseCelebration.certificateHeader.notAvailable": "Le statut de votre note et de votre certificat seront bientôt disponibles.",
|
||||
"courseCelebration.certificateBody.notAvailable.accessCertificate": "Si vous avez obtenu une note de passage, votre attestation sera automatiquement générée.",
|
||||
"courseCelebration.certificateHeader.unverified": "Vous devez avoir complété votre vérification pour recevoir votre attestation.",
|
||||
"courseCelebration.certificateHeader.requestable": "Félicitations, vous avez terminé le processus pour passer un certificat !",
|
||||
@@ -286,7 +287,6 @@
|
||||
"courseExit.courseInProgressDescription": "Il semble qu'il y ait plus de contenu dans ce cours qui sera publié dans le futur. Attendez les mises à jour par courriel ou revenez sur votre cours pour savoir quand ce contenu sera disponible.",
|
||||
"courseExit.courseInProgressHeader": "Plus de contenu sera bientôt disponible!",
|
||||
"courseExit.dashboardLink": "Tableau de bord",
|
||||
"courseCelebration.downloadButton": "Téléchargez mon attestation",
|
||||
"courseExit.endOfCourseDescription": "Malheureusement, vous n'êtes actuellement pas éligible pour une attestatation. Vous devez recevoir une note de passage pour être admissible à une attestation.",
|
||||
"courseExit.endOfCourseHeader": "Vous avez atteint la fin du cours!",
|
||||
"courseExit.endOfCourseTitle": "Fin du cours",
|
||||
@@ -396,7 +396,7 @@
|
||||
"learning.generic.upsell.fullAccessBullet.fullAccess": "Accès complet",
|
||||
"learning.generic.upsell.fullAccessBullet": "{fullAccessInBoldText} au contenu et aux supports du cours, même après la fin du cours",
|
||||
"learning.generic.upsell.supportMissionBullet.mission": "mission",
|
||||
"learning.generic.upsell.supportMissionBullet": "Supportez notre {missionInBoldText} chez edX",
|
||||
"learning.generic.upsell.supportMissionBullet": "Soutenez notre {missionInBoldText} à {siteName}",
|
||||
"masquerade-widget.userName.error.generic": "Une erreur est survenue; veuillez réessayer.",
|
||||
"masquerade-widget.userName.input.placeholder": "Nom d'utilisateur ou courriel",
|
||||
"masquerade-widget.userName.input.label": "Se faire passer pour cet utilisateur",
|
||||
@@ -408,7 +408,7 @@
|
||||
"tours.button.okay": "Okay",
|
||||
"tours.button.beginTour": "Commencer la visite guidée",
|
||||
"tours.button.launchTour": "Lancer la visite guidée",
|
||||
"tours.newUserModal.body": "Faisons une visite guidée de edX afin de profiter au maximum de votre cours.",
|
||||
"tours.newUserModal.body": "Faisons un tour rapide de {siteName} afin que vous puissiez tirer le meilleur parti de votre cours.",
|
||||
"tours.newUserModal.title.welcome": "Bienvenue à votre",
|
||||
"tours.button.skipForNow": "Ignorer pour l'instant",
|
||||
"tours.datesCheckpoint.body": "Dates importantes afin de vous maintenir sur la bonne voie.",
|
||||
@@ -423,9 +423,9 @@
|
||||
"tours.weeklyGoalsCheckpoint.title": "Paramétrer un objectif de cours",
|
||||
"tours.newUserModal.title": "{welcome} {siteName} au cours !",
|
||||
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
|
||||
"learning.effortEstimation.activities": "{activityCount, plural, one {# activité} other {# activités}}",
|
||||
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# min} other {# min}}",
|
||||
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# minute} other {# minutes}}",
|
||||
"learning.effortEstimation.activities": "{activityCount, plural, one {# activité} many {# activités} other {# activités}}",
|
||||
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# min} many {# min} other {# min}}",
|
||||
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# minute} many {# minutes} other {# minutes}}",
|
||||
"learning.streakCelebration.congratulations": "Félicitations !",
|
||||
"learning.streakCelebration.body": "Continuez comme ça, vous êtes sur une lancée!",
|
||||
"learning.streakCelebration.button": "Continuez ainsi",
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
"learning.goals.unsubscribe.header": "You’ve unsubscribed from goal reminders",
|
||||
"learning.goals.unsubscribe.loading": "Unsubscribing…",
|
||||
"learning.goals.unsubscribe.errorDescription": "We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help.",
|
||||
"learning.outline.alert.cert.when": "This course ends on {courseEndDateFormatted}. Final grades and certificates are\n scheduled to be available after {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header": "Your grade and certificate will be ready soon!",
|
||||
"learning.outline.alert.cert.earnedNotAvailable": "This course ends on {courseEndDateFormatted}. Final grades and any earned certificates are\n scheduled to be available after {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header.v2": "Your grade and certificate status will be available soon.",
|
||||
"cert.alert.earned.ready.header": "Congratulations! Your certificate is ready.",
|
||||
"cert.alert.notPassing.header": "You are not yet eligible for a certificate",
|
||||
"cert.alert.notPassing.button": "View grades",
|
||||
@@ -113,7 +113,7 @@
|
||||
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||
"progress.certificateStatus.unverifiedBody": "In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}.",
|
||||
"progress.certificateStatus.downloadableBody": "Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "This course ended on {endDate} and final grades and certificates are scheduled to be\n available after {certAvailableDate}.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",
|
||||
"progress.certificateStatus.notPassingHeader": "Certificate status",
|
||||
"progress.certificateStatus.notPassingBody": "In order to qualify for a certificate, you must have a passing grade.",
|
||||
"progress.certificateStatus.inProgressHeader": "More content is coming soon!",
|
||||
@@ -125,13 +125,13 @@
|
||||
"progress.certificateStatus.unverifiedButton": "Verify ID",
|
||||
"progress.certificateStatus.courseCelebration.verificationPending": "Your ID verification is pending and your certificate will be available once approved.",
|
||||
"progress.certificateStatus.downloadableHeader": "Your certificate is available!",
|
||||
"progress.certificateStatus.downloadableButton": "Download my certificate",
|
||||
"progress.certificateStatus.viewableButton": "View my certificate",
|
||||
"progress.certificateStatus.notAvailableHeader": "Certificate status",
|
||||
"progress.certificateBody.notAvailable.endDate": "Final grades and any earned certificates are scheduled to be available after {endDate}.",
|
||||
"progress.certificateStatus.upgradeHeader": "Earn a certificate",
|
||||
"progress.certificateStatus.upgradeBody": "You are in an audit track and do not qualify for a certificate. In order to work towards a certificate, upgrade your course today.",
|
||||
"progress.certificateStatus.upgradeButton": "Upgrade now",
|
||||
"progress.certificateStatus.unverifiedHomeHeader": "Verify your identity to earn a certificate!",
|
||||
"progress.certificateStatus.unverifiedHomeHeader.v2": "Verify your identity to qualify for a certificate.",
|
||||
"progress.certificateStatus.unverifiedHomeButton": "Verify my ID",
|
||||
"progress.certificateStatus.unverifiedHomeBody": "In order to generate a certificate for this course, you must complete the ID verification process.",
|
||||
"progress.completion.donut.label": "completed",
|
||||
@@ -262,6 +262,7 @@
|
||||
"notes.button.hide": "Hide Notes",
|
||||
"courseExit.catalogSearchSuggestion": "Looking to learn more? {searchOurCatalogLink} to find more courses and programs to explore.",
|
||||
"courseCelebration.certificateBody.available": "\n Showcase your accomplishment on LinkedIn or your resumé today.\n You can download your certificate now and access it any time from your\n {dashboardLink} and {profileLink}.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate.v2": "This course ends on {endDate}. Final grades and any earned certificates are\n scheduled to be available after {certAvailableDate}.",
|
||||
"courseCelebration.certificateBody.unverified": "In order to generate a certificate, you must complete ID verification.\n {idVerificationSupportLink} now.",
|
||||
"courseCelebration.certificateBody.upgradable": "It’s not too late to upgrade. For {price} you will unlock access to all graded\n assignments in this course. Upon completion, you will receive a verified certificate which is a\n valuable credential to improve your job prospects and advance your career, or highlight your\n certificate in school applications.",
|
||||
"courseCelebration.upgradeDiscountCodePrompt": "Use code {code} at checkout for {percent}% off!",
|
||||
@@ -274,7 +275,7 @@
|
||||
"courseCelebration.dashboardInfo": "You can access this course and its materials on your {dashboardLink}.",
|
||||
"courseExit.programs.applyForCredit": "Apply for credit",
|
||||
"courseCelebration.certificateHeader.downloadable": "Your certificate is available!",
|
||||
"courseCelebration.certificateHeader.notAvailable": "Your grade and certificate will be ready soon!",
|
||||
"courseCelebration.certificateHeader.notAvailable": "Your grade and certificate status will be available soon.",
|
||||
"courseCelebration.certificateBody.notAvailable.accessCertificate": "If you have earned a passing grade, your certificate will be automatically issued.",
|
||||
"courseCelebration.certificateHeader.unverified": "You must complete verification to receive your certificate.",
|
||||
"courseCelebration.certificateHeader.requestable": "Congratulations, you qualified for a certificate!",
|
||||
@@ -286,7 +287,6 @@
|
||||
"courseExit.courseInProgressDescription": "It looks like there is more content in this course that will be released in the future. Look out for email updates or check back on your course for when this content will be available.",
|
||||
"courseExit.courseInProgressHeader": "More content is coming soon!",
|
||||
"courseExit.dashboardLink": "Dashboard",
|
||||
"courseCelebration.downloadButton": "Download my certificate",
|
||||
"courseExit.endOfCourseDescription": "Unfortunately, you are not currently eligible for a certificate. You need to receive a passing grade to be eligible for a certificate.",
|
||||
"courseExit.endOfCourseHeader": "You’ve reached the end of the course!",
|
||||
"courseExit.endOfCourseTitle": "End of Course",
|
||||
@@ -396,7 +396,7 @@
|
||||
"learning.generic.upsell.fullAccessBullet.fullAccess": "Full access",
|
||||
"learning.generic.upsell.fullAccessBullet": "{fullAccessInBoldText} to course content and materials, even after the course ends",
|
||||
"learning.generic.upsell.supportMissionBullet.mission": "mission",
|
||||
"learning.generic.upsell.supportMissionBullet": "Support our {missionInBoldText} at edX",
|
||||
"learning.generic.upsell.supportMissionBullet": "Support our {missionInBoldText} at {siteName}",
|
||||
"masquerade-widget.userName.error.generic": "An error has occurred; please try again.",
|
||||
"masquerade-widget.userName.input.placeholder": "Username or email",
|
||||
"masquerade-widget.userName.input.label": "Masquerade as this user",
|
||||
@@ -408,7 +408,7 @@
|
||||
"tours.button.okay": "Okay",
|
||||
"tours.button.beginTour": "Begin tour",
|
||||
"tours.button.launchTour": "Launch tour",
|
||||
"tours.newUserModal.body": "Let’s take a quick tour of edX so you can get the most out of your course.",
|
||||
"tours.newUserModal.body": "Let’s take a quick tour of {siteName} so you can get the most out of your course.",
|
||||
"tours.newUserModal.title.welcome": "Welcome to your",
|
||||
"tours.button.skipForNow": "Skip for now",
|
||||
"tours.datesCheckpoint.body": "Important dates can help you stay on track.",
|
||||
|
||||
@@ -12,6 +12,8 @@ import { Switch } from 'react-router-dom';
|
||||
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { fetchDiscussionTab, fetchLiveTab } from './course-home/data/thunks';
|
||||
import DiscussionTab from './course-home/discussion-tab/DiscussionTab';
|
||||
|
||||
import appMessages from './i18n';
|
||||
import { UserMessagesProvider } from './generic/user-messages';
|
||||
@@ -31,6 +33,7 @@ import { fetchCourse } from './courseware/data';
|
||||
import initializeStore from './store';
|
||||
import NoticesProvider from './generic/notices';
|
||||
import PathFixesProvider from './generic/path-fixes';
|
||||
import LiveTab from './course-home/live-tab/LiveTab';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
@@ -46,11 +49,21 @@ subscribe(APP_READY, () => {
|
||||
<OutlineTab />
|
||||
</TabContainer>
|
||||
</PageRoute>
|
||||
<PageRoute path="/course/:courseId/live">
|
||||
<TabContainer tab="live" fetch={fetchLiveTab} slice="courseHome">
|
||||
<LiveTab />
|
||||
</TabContainer>
|
||||
</PageRoute>
|
||||
<PageRoute path="/course/:courseId/dates">
|
||||
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
|
||||
<DatesTab />
|
||||
</TabContainer>
|
||||
</PageRoute>
|
||||
<PageRoute path="/course/:courseId/discussion/:path*">
|
||||
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
|
||||
<DiscussionTab />
|
||||
</TabContainer>
|
||||
</PageRoute>
|
||||
<PageRoute
|
||||
path={[
|
||||
'/course/:courseId/progress/:targetUserId/',
|
||||
|
||||
@@ -36,14 +36,6 @@ function getStudioUrl(courseId, unitId) {
|
||||
return urlFull;
|
||||
}
|
||||
|
||||
function getLegacyWebUrl(canViewLegacyCourseware, courseId, unitId) {
|
||||
if (!canViewLegacyCourseware || !unitId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${getConfig().LMS_BASE_URL}/courses/${courseId}/jump_to/${unitId}?experience=legacy`;
|
||||
}
|
||||
|
||||
export default function InstructorToolbar(props) {
|
||||
// This didMount logic became necessary once we had a page that does a redirect on a quick exit.
|
||||
// As a result, it unmounts the InstructorToolbar (which will be remounted by the new component),
|
||||
@@ -62,12 +54,10 @@ export default function InstructorToolbar(props) {
|
||||
const {
|
||||
courseId,
|
||||
unitId,
|
||||
canViewLegacyCourseware,
|
||||
tab,
|
||||
} = props;
|
||||
|
||||
const urlInsights = getInsightsUrl(courseId);
|
||||
const urlLegacy = getLegacyWebUrl(canViewLegacyCourseware, courseId, unitId);
|
||||
const urlStudio = getStudioUrl(courseId, unitId);
|
||||
const [masqueradeErrorMessage, showMasqueradeError] = useState(null);
|
||||
|
||||
@@ -81,17 +71,12 @@ export default function InstructorToolbar(props) {
|
||||
<div className="align-items-center flex-grow-1 d-md-flex mx-1 my-1">
|
||||
<MasqueradeWidget courseId={courseId} onError={showMasqueradeError} />
|
||||
</div>
|
||||
{(urlLegacy || urlStudio || urlInsights) && (
|
||||
{(urlStudio || urlInsights) && (
|
||||
<>
|
||||
<hr className="border-light" />
|
||||
<span className="mr-2 mt-1 col-form-label">View course in:</span>
|
||||
</>
|
||||
)}
|
||||
{urlLegacy && (
|
||||
<span className="mx-1 my-1">
|
||||
<a className="btn btn-inverse-outline-primary" href={urlLegacy}>Legacy experience</a>
|
||||
</span>
|
||||
)}
|
||||
{urlStudio && (
|
||||
<span className="mx-1 my-1">
|
||||
<a className="btn btn-inverse-outline-primary" href={urlStudio}>Studio</a>
|
||||
@@ -128,13 +113,11 @@ export default function InstructorToolbar(props) {
|
||||
InstructorToolbar.propTypes = {
|
||||
courseId: PropTypes.string,
|
||||
unitId: PropTypes.string,
|
||||
canViewLegacyCourseware: PropTypes.bool,
|
||||
tab: PropTypes.string,
|
||||
};
|
||||
|
||||
InstructorToolbar.defaultProps = {
|
||||
courseId: undefined,
|
||||
unitId: undefined,
|
||||
canViewLegacyCourseware: undefined,
|
||||
tab: '',
|
||||
};
|
||||
|
||||
@@ -34,7 +34,6 @@ describe('Instructor Toolbar', () => {
|
||||
mockData = {
|
||||
courseId: courseware.courseId,
|
||||
unitId: Object.values(models.units)[0].id,
|
||||
canViewLegacyCourseware: true,
|
||||
};
|
||||
axiosMock.reset();
|
||||
axiosMock.onGet(masqueradeUrl).reply(200, { success: true });
|
||||
@@ -63,32 +62,6 @@ describe('Instructor Toolbar', () => {
|
||||
getConfig.mockImplementation(() => config);
|
||||
render(<InstructorToolbar {...mockData} />);
|
||||
|
||||
const linksContainer = screen.getByText('View course in:').parentElement;
|
||||
['Legacy experience', 'Studio', 'Insights'].forEach(service => {
|
||||
expect(getByText(linksContainer, service).getAttribute('href')).toMatch(/http.*/);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays links to view course in available services - false legacy courseware flag', () => {
|
||||
const config = { ...originalConfig };
|
||||
config.INSIGHTS_BASE_URL = 'http://localhost:18100';
|
||||
getConfig.mockImplementation(() => config);
|
||||
mockData.canViewLegacyCourseware = false;
|
||||
render(<InstructorToolbar {...mockData} />);
|
||||
|
||||
const linksContainer = screen.getByText('View course in:').parentElement;
|
||||
['Studio', 'Insights'].forEach(service => {
|
||||
expect(getByText(linksContainer, service).getAttribute('href')).toMatch(/http.*/);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays links to view course in available services - empty unit', () => {
|
||||
const config = { ...originalConfig };
|
||||
config.INSIGHTS_BASE_URL = 'http://localhost:18100';
|
||||
getConfig.mockImplementation(() => config);
|
||||
mockData.unitId = undefined;
|
||||
render(<InstructorToolbar {...mockData} />);
|
||||
|
||||
const linksContainer = screen.getByText('View course in:').parentElement;
|
||||
['Studio', 'Insights'].forEach(service => {
|
||||
expect(getByText(linksContainer, service).getAttribute('href')).toMatch(/http.*/);
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"sku": "8CF08E5",
|
||||
"upgrade_url": "http://localhost:18130/basket/add/?sku=8CF08E5"
|
||||
},
|
||||
"can_load_courseware": true,
|
||||
"celebrations": {
|
||||
"first_section": false,
|
||||
"streak_length_to_celebrate": null,
|
||||
@@ -66,9 +65,6 @@
|
||||
"$.body.verified_mode": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.can_load_courseware": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.celebrations": {
|
||||
"match": "type"
|
||||
},
|
||||
@@ -280,7 +276,6 @@
|
||||
},
|
||||
"show_calculator": false,
|
||||
"original_user_is_staff": true,
|
||||
"can_view_legacy_courseware": true,
|
||||
"is_staff": true,
|
||||
"course_access": {
|
||||
"has_access": true,
|
||||
@@ -304,8 +299,7 @@
|
||||
"course_exit_page_is_active": false,
|
||||
"certificate_data": {
|
||||
"cert_status": "audit_passing",
|
||||
"cert_web_view_url": null,
|
||||
"download_url": null,
|
||||
"cert_web_view_url": null,
|
||||
"certificate_available_date": null
|
||||
},
|
||||
"verify_identity_url": null,
|
||||
@@ -414,9 +408,6 @@
|
||||
"$.body.original_user_is_staff": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.can_view_legacy_courseware": {
|
||||
"match": "type"
|
||||
},
|
||||
"$.body.is_staff": {
|
||||
"match": "type"
|
||||
},
|
||||
@@ -759,4 +750,4 @@
|
||||
"version": "2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const messages = defineMessages({
|
||||
},
|
||||
newUserModalBody: {
|
||||
id: 'tours.newUserModal.body',
|
||||
defaultMessage: 'Let’s take a quick tour of edX so you can get the most out of your course.',
|
||||
defaultMessage: 'Let’s take a quick tour of {siteName} so you can get the most out of your course.',
|
||||
},
|
||||
newUserModalTitleWelcome: {
|
||||
id: 'tours.newUserModal.title.welcome',
|
||||
|
||||
@@ -58,7 +58,7 @@ function NewUserCourseHomeTourModal({
|
||||
)}
|
||||
onClose={onDismiss}
|
||||
>
|
||||
<p className="text-dark-900">{intl.formatMessage(messages.newUserModalBody)}</p>
|
||||
<p className="text-dark-900">{intl.formatMessage(messages.newUserModalBody, { siteName: getConfig().SITE_NAME })}</p>
|
||||
</MarketingModal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ import '@testing-library/jest-dom/extend-expect';
|
||||
import './courseware/data/__factories__';
|
||||
import './course-home/data/__factories__';
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { configure as configureI18n } from '@edx/frontend-platform/i18n';
|
||||
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { configure as configureLogging } from '@edx/frontend-platform/logging';
|
||||
import { configure as configureAuth, getAuthenticatedHttpClient, MockAuthService } from '@edx/frontend-platform/auth';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { render as rtlRender } from '@testing-library/react';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import AppProvider from '@edx/frontend-platform/react/AppProvider';
|
||||
import { reducer as courseHomeReducer } from './course-home/data';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getLocale } from '@edx/frontend-platform/i18n';
|
||||
// This function inspects an access denied error and provides a redirect url (looks like a /redirect/... path),
|
||||
// which then renders a nice little message while the browser loads the next page.
|
||||
// This is basically a frontend version of check_course_access_with_redirect in the backend.
|
||||
export function getAccessDeniedRedirectUrl(courseId, activeTabSlug, canLoadCourseware, courseAccess, start, unitId) {
|
||||
export function getAccessDeniedRedirectUrl(courseId, activeTabSlug, courseAccess, start) {
|
||||
let url = null;
|
||||
switch (courseAccess.errorCode) {
|
||||
case 'audit_expired':
|
||||
@@ -18,18 +18,17 @@ export function getAccessDeniedRedirectUrl(courseId, activeTabSlug, canLoadCours
|
||||
case 'survey_required':
|
||||
url = `/redirect/survey/${courseId}`;
|
||||
break;
|
||||
case 'data_sharing_access_required':
|
||||
url = `/redirect/consent?consentPath=${encodeURIComponent(courseAccess.developerMessage)}`;
|
||||
break;
|
||||
case 'unfulfilled_milestones':
|
||||
url = '/redirect/dashboard';
|
||||
break;
|
||||
case 'authentication_required':
|
||||
case 'enrollment_required':
|
||||
default:
|
||||
// if the learner has access to the course, but it is not enabled in the mfe, there is no
|
||||
// error message, canLoadCourseware will be false.
|
||||
if (activeTabSlug === 'courseware' && canLoadCourseware === false && unitId) {
|
||||
url = `/redirect/courseware/${courseId}/unit/${unitId}`;
|
||||
} else if (activeTabSlug !== 'outline') {
|
||||
url = `/redirect/course-home/${courseId}`;
|
||||
if (activeTabSlug !== 'outline') {
|
||||
url = `/course/${courseId}/home`;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
|
||||
@@ -47,13 +47,13 @@ Factory.define('block')
|
||||
},
|
||||
)
|
||||
.attr(
|
||||
'legacy_web_url',
|
||||
['legacy_web_url', 'host', 'courseId', 'id'],
|
||||
'lms_web_url',
|
||||
['lms_web_url', 'host', 'courseId', 'id'],
|
||||
(url, host, courseId, id) => {
|
||||
if (url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return `${host}/courses/${courseId}/jump_to/${id}?experience=legacy`;
|
||||
return `${host}/courses/${courseId}/jump_to/${id}`;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ function LoadedTabPage({
|
||||
}) {
|
||||
const {
|
||||
celebrations,
|
||||
canViewLegacyCourseware,
|
||||
org,
|
||||
originalUserIsStaff,
|
||||
tabs,
|
||||
@@ -58,7 +57,6 @@ function LoadedTabPage({
|
||||
<InstructorToolbar
|
||||
courseId={courseId}
|
||||
unitId={unitId}
|
||||
canViewLegacyCourseware={canViewLegacyCourseware}
|
||||
tab={activeTabSlug}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,6 @@ function TabPage({ intl, ...props }) {
|
||||
courseId,
|
||||
courseStatus,
|
||||
metadataModel,
|
||||
unitId,
|
||||
} = props;
|
||||
const {
|
||||
toastBodyLink,
|
||||
@@ -32,7 +31,6 @@ function TabPage({ intl, ...props }) {
|
||||
} = useSelector(state => state.courseHome);
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
canLoadCourseware,
|
||||
courseAccess,
|
||||
number,
|
||||
org,
|
||||
@@ -53,9 +51,7 @@ function TabPage({ intl, ...props }) {
|
||||
}
|
||||
|
||||
if (courseStatus === 'denied') {
|
||||
const redirectUrl = getAccessDeniedRedirectUrl(
|
||||
courseId, activeTabSlug, canLoadCourseware, courseAccess, start, unitId,
|
||||
);
|
||||
const redirectUrl = getAccessDeniedRedirectUrl(courseId, activeTabSlug, courseAccess, start);
|
||||
if (redirectUrl) {
|
||||
return (<Redirect to={redirectUrl} />);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user