Compare commits
45 Commits
open-relea
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08c7d2d118 | ||
|
|
17a102d5cf | ||
|
|
77e3b17f03 | ||
|
|
b1d51a0468 | ||
|
|
13f884fc56 | ||
|
|
a701ea5e15 | ||
|
|
108fb314f5 | ||
|
|
75d2abe1a0 | ||
|
|
d00961d85c | ||
|
|
96e2c88837 | ||
|
|
14a4fae421 | ||
|
|
0d1f01628e | ||
|
|
95b285d371 | ||
|
|
1892edaade | ||
|
|
381be9a26b | ||
|
|
b3841ef446 | ||
|
|
5a897e4ea1 | ||
|
|
96ceab8b2f | ||
|
|
f9806d0759 | ||
|
|
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 |
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
|
||||
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: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
@@ -15,7 +15,7 @@ Please tag **@edx/engage-squad** on any PRs or issues. Thanks.
|
||||
.. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3
|
||||
:target: https://codecov.io/gh/edx/frontend-app-learning
|
||||
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational
|
||||
:target: https://github.com/edx/frontend-app-account/blob/master/LICENSE
|
||||
:target: https://github.com/openedx/frontend-app-account/blob/master/LICENSE
|
||||
|
||||
Development
|
||||
-----------
|
||||
@@ -23,7 +23,7 @@ Development
|
||||
Start Devstack
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
To use this application, `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
|
||||
To use this application, `devstack <https://github.com/openedx/devstack>`__ must be running and you must be logged into it.
|
||||
|
||||
- Run ``make dev.up.lms``
|
||||
- Visit http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course to view the demo course. You can replace ``course-v1:edX+DemoX+Demo_Course`` with a different course key.
|
||||
@@ -52,7 +52,7 @@ file (which is git-ignored) that defines where to find your local modules, for i
|
||||
],
|
||||
};
|
||||
|
||||
See https://github.com/edx/frontend-build#local-module-configuration-for-webpack for more details.
|
||||
See https://github.com/openedx/frontend-build#local-module-configuration-for-webpack for more details.
|
||||
|
||||
Deployment
|
||||
----------
|
||||
|
||||
9970
package-lock.json
generated
9970
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -4,7 +4,7 @@
|
||||
"description": "Frontend learning application.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-learning.git"
|
||||
"url": "git+https://github.com/openedx/frontend-app-learning.git"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
@@ -21,20 +21,20 @@
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/edx/frontend-app-learning#readme",
|
||||
"homepage": "https://github.com/openedx/frontend-app-learning#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/edx/frontend-app-learning/issues"
|
||||
"url": "https://github.com/openedx/frontend-app-learning/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "10.2.2",
|
||||
"@edx/frontend-component-header": "2.4.6",
|
||||
"@edx/frontend-lib-special-exams": "1.16.3",
|
||||
"@edx/frontend-platform": "1.15.6",
|
||||
"@edx/paragon": "19.14.1",
|
||||
"@edx/frontend-component-footer": "11.1.0",
|
||||
"@edx/frontend-component-header": "3.1.0",
|
||||
"@edx/frontend-lib-special-exams": "2.1.0",
|
||||
"@edx/frontend-platform": "2.5.1",
|
||||
"@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",
|
||||
@@ -43,10 +43,12 @@
|
||||
"@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",
|
||||
"query-string": "^7.1.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "6.1.0",
|
||||
@@ -63,13 +65,13 @@
|
||||
"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",
|
||||
"rosie": "2.1.0"
|
||||
|
||||
@@ -5,5 +5,12 @@
|
||||
"patch": {
|
||||
"automerge": true
|
||||
},
|
||||
"rebaseStalePrs": true
|
||||
"rebaseStalePrs": true,
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackagePatterns": ["@edx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
48
src/alerts/active-enteprise-alert/ActiveEnterpriseAlert.jsx
Normal file
48
src/alerts/active-enteprise-alert/ActiveEnterpriseAlert.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Hyperlink } from '@edx/paragon';
|
||||
import { WarningFilled } from '@edx/paragon/icons';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import genericMessages from './messages';
|
||||
|
||||
function ActiveEnterpriseAlert({ intl, payload }) {
|
||||
const { text, courseId } = payload;
|
||||
const changeActiveEnterprise = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={
|
||||
`${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=${encodeURIComponent(
|
||||
`${global.location.origin}/course/${courseId}/home`,
|
||||
)}`
|
||||
}
|
||||
>
|
||||
{intl.formatMessage(genericMessages.changeActiveEnterpriseLowercase)}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
return (
|
||||
<Alert variant="warning" icon={WarningFilled}>
|
||||
{text}
|
||||
<FormattedMessage
|
||||
id="learning.activeEnterprise.alert"
|
||||
description="Prompts the user to log-in with the correct enterprise to access the course content."
|
||||
defaultMessage=" {changeActiveEnterprise}."
|
||||
values={{
|
||||
changeActiveEnterprise,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
ActiveEnterpriseAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
courseId: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ActiveEnterpriseAlert);
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
initializeTestStore, render, screen,
|
||||
} from '../../setupTest';
|
||||
import ActiveEnterpriseAlert from './ActiveEnterpriseAlert';
|
||||
|
||||
describe('ActiveEnterpriseAlert', () => {
|
||||
const mockData = {
|
||||
payload: {
|
||||
text: 'test message',
|
||||
courseId: 'test-course-id',
|
||||
},
|
||||
};
|
||||
beforeAll(async () => {
|
||||
await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true });
|
||||
});
|
||||
|
||||
it('Shows alert message and links', () => {
|
||||
render(<ActiveEnterpriseAlert {...mockData} />);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('test message', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'change enterprise now' })).toHaveAttribute(
|
||||
'href', `${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=http%3A%2F%2Flocalhost%2Fcourse%2Ftest-course-id%2Fhome`,
|
||||
);
|
||||
});
|
||||
});
|
||||
28
src/alerts/active-enteprise-alert/hooks.js
Normal file
28
src/alerts/active-enteprise-alert/hooks.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { ALERT_TYPES, useAlert } from '../../generic/user-messages';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
const ActiveEnterpriseAlert = React.lazy(() => import('./ActiveEnterpriseAlert'));
|
||||
|
||||
export default function useActiveEnterpriseAlert(courseId) {
|
||||
const { courseAccess } = useModel('courseHomeMeta', courseId);
|
||||
/**
|
||||
* This alert should render if
|
||||
* 1. course access code is incorrect_active_enterprise
|
||||
*/
|
||||
const isVisible = courseAccess && !courseAccess.hasAccess && courseAccess.errorCode === 'incorrect_active_enterprise';
|
||||
|
||||
const payload = {
|
||||
text: courseAccess && courseAccess.userMessage,
|
||||
courseId,
|
||||
};
|
||||
useAlert(isVisible, {
|
||||
code: 'clientActiveEnterpriseAlert',
|
||||
topic: 'outline',
|
||||
dismissible: false,
|
||||
type: ALERT_TYPES.ERROR,
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
});
|
||||
|
||||
return { clientActiveEnterpriseAlert: ActiveEnterpriseAlert };
|
||||
}
|
||||
3
src/alerts/active-enteprise-alert/index.js
Normal file
3
src/alerts/active-enteprise-alert/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import useActiveEnterpriseAlert from './hooks';
|
||||
|
||||
export default useActiveEnterpriseAlert;
|
||||
11
src/alerts/active-enteprise-alert/messages.js
Normal file
11
src/alerts/active-enteprise-alert/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
changeActiveEnterpriseLowercase: {
|
||||
id: 'learning.activeEnterprise.change.alert',
|
||||
defaultMessage: 'change enterprise now',
|
||||
description: 'Text in a link, prompting the user to change active enterprise. Used in learning.activeEnterprise.change.alert"',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedDate,
|
||||
FormattedMessage,
|
||||
FormattedRelative,
|
||||
FormattedRelativeTime,
|
||||
FormattedTime,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
@@ -26,7 +26,7 @@ function CourseStartAlert({ payload }) {
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
const timeRemaining = (
|
||||
<FormattedRelative
|
||||
<FormattedRelativeTime
|
||||
key="timeRemaining"
|
||||
value={startDate}
|
||||
{...timezoneFormatArgs}
|
||||
|
||||
@@ -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;
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { AlertList } from '../../generic/user-messages';
|
||||
|
||||
@@ -109,6 +110,23 @@ function OutlineTab({ intl }) {
|
||||
/** show post enrolment survey to only B2C learners */
|
||||
const learnerType = isEnterpriseUser() ? 'enterprise_learner' : 'b2c_learner';
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const currentParams = new URLSearchParams(location.search);
|
||||
const startCourse = currentParams.get('start_course');
|
||||
if (startCourse === '1') {
|
||||
sendTrackEvent('welcome.email.clicked.startcourse', {});
|
||||
|
||||
// Deleting the course_start query param as it only needs to be set once
|
||||
// whenever passed in query params.
|
||||
currentParams.delete('start_course');
|
||||
history.replace({
|
||||
search: currentParams.toString(),
|
||||
});
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-learner-type={learnerType} className="row w-100 mx-0 my-3 justify-content-between">
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -355,6 +344,26 @@ 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('welcome.email.clicked.setgoal', {});
|
||||
});
|
||||
|
||||
it('emit start course event via query param', async () => {
|
||||
sendTrackEvent.mockClear();
|
||||
await fetchAndRender('http://localhost/?start_course=1');
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('welcome.email.clicked.startcourse', {});
|
||||
});
|
||||
|
||||
describe('weekly learning goal is not set', () => {
|
||||
beforeEach(async () => {
|
||||
setTabData({
|
||||
@@ -568,7 +577,7 @@ describe('Outline Tab', () => {
|
||||
const instructorToolbar = await screen.getByTestId('instructor-toolbar');
|
||||
expect(instructorToolbar).toBeInTheDocument();
|
||||
expect(screen.getByText('This learner no longer has access to this course. Their access expired on', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2020')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2020', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render banner when not masquerading', async () => {
|
||||
@@ -659,7 +668,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 +695,6 @@ describe('Outline Tab', () => {
|
||||
cert_data: {
|
||||
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
|
||||
cert_web_view_url: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
@@ -756,7 +763,6 @@ describe('Outline Tab', () => {
|
||||
cert_data: {
|
||||
cert_status: CERT_STATUS_TYPE.REQUESTING,
|
||||
cert_web_view_url: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
@@ -790,50 +796,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 +807,6 @@ describe('Outline Tab', () => {
|
||||
cert_data: {
|
||||
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
|
||||
cert_web_view_url: null,
|
||||
download_url: null,
|
||||
},
|
||||
}, {
|
||||
date_blocks: [
|
||||
@@ -932,7 +894,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 +919,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 +935,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);
|
||||
@@ -1084,6 +1017,22 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
|
||||
it('displays expiration warning', async () => {
|
||||
const expirationDate = new Date();
|
||||
// This message will render if the expiration date is within 28 days; set the date 10 days in future
|
||||
expirationDate.setTime(expirationDate.getTime() + 864800000);
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, {
|
||||
onboarding_status: 'verified',
|
||||
onboarding_link: 'test',
|
||||
expiration_date: expirationDate.toString(),
|
||||
onboarding_release_date: onboardingReleaseDate.toISOString(),
|
||||
});
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByText('Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays expiration warning for other course', async () => {
|
||||
const expirationDate = new Date();
|
||||
// This message will render if the expiration date is within 28 days; set the date 10 days in future
|
||||
expirationDate.setTime(expirationDate.getTime() + 864800000);
|
||||
@@ -1095,7 +1044,23 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByText('Your onboarding profile has been approved in another course. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays expired', async () => {
|
||||
const expirationDate = new Date();
|
||||
// This message appears after expiration, set the date 10 days in the past
|
||||
expirationDate.setTime(expirationDate.getTime() - 864800000);
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, {
|
||||
onboarding_status: 'verified',
|
||||
onboarding_link: 'test',
|
||||
expiration_date: expirationDate.toString(),
|
||||
onboarding_release_date: onboardingReleaseDate.toISOString(),
|
||||
});
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByText('Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedDate,
|
||||
FormattedMessage,
|
||||
FormattedRelative,
|
||||
FormattedRelativeTime,
|
||||
FormattedTime,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
@@ -21,7 +21,7 @@ function CourseEndAlert({ payload }) {
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
const timeRemaining = (
|
||||
<FormattedRelative
|
||||
<FormattedRelativeTime
|
||||
key="timeRemaining"
|
||||
value={endDate}
|
||||
{...timezoneFormatArgs}
|
||||
|
||||
@@ -231,6 +231,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Expiring Soon',
|
||||
description: 'A label to indicate that proctortrack onboarding exam will expire soon',
|
||||
},
|
||||
expiredProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.expired',
|
||||
defaultMessage: 'Expired',
|
||||
description: 'A label to indicate that proctortrack onboarding exam has expired',
|
||||
},
|
||||
proctoringCurrentStatus: {
|
||||
id: 'learning.proctoringPanel.status',
|
||||
defaultMessage: 'Current Onboarding Status:',
|
||||
@@ -278,9 +283,14 @@ const messages = defineMessages({
|
||||
},
|
||||
expiringSoonProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.expiringSoon',
|
||||
defaultMessage: 'Your onboarding profile has been approved in another course. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.',
|
||||
defaultMessage: 'Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.',
|
||||
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (expiring soon)',
|
||||
},
|
||||
expiredProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.expired',
|
||||
defaultMessage: 'Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.',
|
||||
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (expired)',
|
||||
},
|
||||
proctoringPanelGeneralInfo: {
|
||||
id: 'learning.proctoringPanel.generalInfo',
|
||||
defaultMessage: 'You must complete the onboarding process prior to taking any proctored exam. ',
|
||||
|
||||
@@ -35,6 +35,7 @@ function ProctoringInfoPanel({ intl }) {
|
||||
error: 'error',
|
||||
otherCourseApproved: 'otherCourseApproved',
|
||||
expiringSoon: 'expiringSoon',
|
||||
expired: 'expired',
|
||||
};
|
||||
|
||||
function getReadableStatusClass(examStatus) {
|
||||
@@ -54,9 +55,14 @@ function ProctoringInfoPanel({ intl }) {
|
||||
return readableClass;
|
||||
}
|
||||
|
||||
function isNotYetSubmitted(examStatus) {
|
||||
const NO_SHOW_STATES = ['submitted', 'second_review_required', 'verified'];
|
||||
return !NO_SHOW_STATES.includes(examStatus);
|
||||
function isCurrentlySubmitted(examStatus) {
|
||||
const SUBMITTED_STATES = ['submitted', 'second_review_required'];
|
||||
return SUBMITTED_STATES.includes(examStatus);
|
||||
}
|
||||
|
||||
function isSubmissionRequired(examStatus) {
|
||||
const OK_STATES = [readableStatuses.submitted, readableStatuses.verified];
|
||||
return !OK_STATES.includes(examStatus);
|
||||
}
|
||||
|
||||
function isNotYetReleased(examReleaseDate) {
|
||||
@@ -77,11 +83,19 @@ function ProctoringInfoPanel({ intl }) {
|
||||
return borderClass;
|
||||
}
|
||||
|
||||
function isExpiringSoon(dateString) {
|
||||
// Returns true if the expiration date is within 28 days
|
||||
function isExpired(dateString) {
|
||||
// Returns true if the expiration date has passed
|
||||
const today = new Date();
|
||||
const expirationDateObject = new Date(dateString);
|
||||
return today > expirationDateObject.getTime() - 2419200000;
|
||||
return today >= expirationDateObject.getTime();
|
||||
}
|
||||
|
||||
function isExpiringSoon(dateString) {
|
||||
// Returns true if the expiration date is within 28 days
|
||||
const twentyeightDays = 28 * 24 * 60 * 60 * 1000;
|
||||
const today = new Date();
|
||||
const expirationDateObject = new Date(dateString);
|
||||
return today > expirationDateObject.getTime() - twentyeightDays;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -96,7 +110,9 @@ function ProctoringInfoPanel({ intl }) {
|
||||
setStatus(response.onboarding_status);
|
||||
setLink(response.onboarding_link);
|
||||
const expirationDate = response.expiration_date;
|
||||
if (expirationDate && isExpiringSoon(expirationDate)) {
|
||||
if (expirationDate && isExpired(expirationDate)) {
|
||||
setReadableStatus(getReadableStatusClass('expired'));
|
||||
} else if (expirationDate && isExpiringSoon(expirationDate)) {
|
||||
setReadableStatus(getReadableStatusClass('expiringSoon'));
|
||||
} else {
|
||||
setReadableStatus(getReadableStatusClass(response.onboarding_status));
|
||||
@@ -175,17 +191,17 @@ function ProctoringInfoPanel({ intl }) {
|
||||
{![readableStatuses.verified, readableStatuses.otherCourseApproved].includes(readableStatus) && (
|
||||
<>
|
||||
<p>
|
||||
{isNotYetSubmitted(status) && (
|
||||
{!isCurrentlySubmitted(status) && (
|
||||
intl.formatMessage(messages.proctoringPanelGeneralInfo)
|
||||
)}
|
||||
{!isNotYetSubmitted(status) && (
|
||||
{isCurrentlySubmitted(status) && (
|
||||
intl.formatMessage(messages.proctoringPanelGeneralInfoSubmitted)
|
||||
)}
|
||||
</p>
|
||||
<p>{intl.formatMessage(messages.proctoringPanelGeneralTime)}</p>
|
||||
</>
|
||||
)}
|
||||
{isNotYetSubmitted(status) && (
|
||||
{isSubmissionRequired(readableStatus) && (
|
||||
onboardingExamButton
|
||||
)}
|
||||
<Button variant="outline-primary" block href="https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams">
|
||||
|
||||
@@ -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);
|
||||
@@ -47,6 +50,9 @@ function WeeklyLearningGoalCard({
|
||||
num_days: days,
|
||||
reminder_selected: selectReminders,
|
||||
});
|
||||
if (triggeredFromEmail) {
|
||||
sendTrackEvent('welcome.email.clicked.setgoal', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +71,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,66 @@ 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',
|
||||
}),
|
||||
{ exact: false },
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Credit Information', () => {
|
||||
@@ -1283,7 +1303,7 @@ describe('Progress Tab', () => {
|
||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||
expect(screen.getByTestId('instructor-toolbar')).toBeInTheDocument();
|
||||
expect(screen.getByText('This learner no longer has access to this course. Their access expired on', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2020')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2020', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
it('does not render banner when not masquerading', async () => {
|
||||
setMetadata({ is_enrolled: true, original_user_is_staff: true });
|
||||
@@ -1296,7 +1316,7 @@ describe('Progress Tab', () => {
|
||||
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||
expect(screen.queryByText('This learner no longer has access to this course. Their access expired on', { exact: false })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('1/1/2020')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('1/1/2020', { exact: false })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1312,7 +1332,7 @@ describe('Progress Tab', () => {
|
||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||
expect(screen.getByTestId('instructor-toolbar')).toBeInTheDocument();
|
||||
expect(screen.getByText('This learner does not yet have access to this course. The course starts on', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2999')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2999', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
it('does not render banner when not masquerading', async () => {
|
||||
setMetadata({
|
||||
@@ -1324,7 +1344,7 @@ describe('Progress Tab', () => {
|
||||
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||
expect(screen.queryByText('This learner does not yet have access to this course. The course starts on', { exact: false })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('1/1/2999')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('1/1/2999', { exact: false })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
import CompleteDonutSegment from './CompleteDonutSegment';
|
||||
@@ -26,6 +28,8 @@ function CompletionDonutChart({ intl }) {
|
||||
const lockedPercentage = lockedCount ? Number(((lockedCount / numTotalUnits) * 100).toFixed(0)) : 0;
|
||||
const incompletePercentage = 100 - completePercentage - lockedPercentage;
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg role="img" width="50%" height="100%" viewBox="0 0 42 42" className="donut" style={{ maxWidth: '178px' }} aria-hidden="true">
|
||||
@@ -35,7 +39,7 @@ function CompletionDonutChart({ intl }) {
|
||||
<circle className="donut-hole" fill="#fff" cx="21" cy="21" r="15.91549430918954" />
|
||||
<g className="donut-chart-text">
|
||||
<text x="50%" y="50%" className="donut-chart-number">
|
||||
{completePercentage}%
|
||||
{completePercentage}{isLocaleRtl && '\u200f'}%
|
||||
</text>
|
||||
<text x="50%" y="50%" className="donut-chart-label">
|
||||
{intl.formatMessage(messages.donutLabel)}
|
||||
|
||||
@@ -41,7 +41,7 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
||||
overlay={(
|
||||
<Popover id={`${isPassing ? 'passing' : 'non-passing'}-grade-tooltip`} aria-hidden="true" className={tooltipClassName}>
|
||||
<Popover.Content data-testid="currentGradeTooltipContent" className={isPassing ? 'text-white' : 'text-dark-700'}>
|
||||
{currentGrade.toFixed(0)}%
|
||||
{currentGrade.toFixed(0)}{isLocaleRtl ? '\u200f' : ''}%
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import CurrentGradeTooltip from './CurrentGradeTooltip';
|
||||
import PassingGradeTooltip from './PassingGradeTooltip';
|
||||
@@ -26,14 +28,16 @@ function GradeBar({ intl, passingGrade }) {
|
||||
|
||||
const lockedTooltipClassName = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
||||
|
||||
const adjustedRtlStyle = (percentOffest) => (isRtl(getLocale()) ? { transform: `translateX(${100 - percentOffest}%)` } : {});
|
||||
|
||||
return (
|
||||
<div className="col-12 col-sm-6 align-self-center p-0">
|
||||
<div className="sr-only">{intl.formatMessage(messages.courseGradeBarAltText, { currentGrade, passingGrade })}</div>
|
||||
<svg width="100%" height="100px" className="grade-bar" aria-hidden="true">
|
||||
<g style={{ transform: 'translateY(2.61em)' }}>
|
||||
<rect className="grade-bar__base" width="100%" />
|
||||
<rect className="grade-bar--passing" width={`${passingGrade}%`} />
|
||||
<rect className={`grade-bar--current-${isPassing ? 'passing' : 'non-passing'}`} width={`${currentGrade}%`} />
|
||||
<rect className="grade-bar--passing" width={`${passingGrade}%`} style={adjustedRtlStyle(passingGrade)} />
|
||||
<rect className={`grade-bar--current-${isPassing ? 'passing' : 'non-passing'}`} width={`${currentGrade}%`} style={adjustedRtlStyle(currentGrade)} />
|
||||
|
||||
{/* Start divider */}
|
||||
<rect className="grade-bar__divider" />
|
||||
|
||||
@@ -25,7 +25,7 @@ function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
||||
overlay={(
|
||||
<Popover id="minimum-grade-tooltip" className={`bg-primary-500 ${tooltipClassName}`} aria-hidden="true">
|
||||
<Popover.Content className="text-white">
|
||||
{passingGrade}%
|
||||
{passingGrade}{isLocaleRtl && '\u200f'}%
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
@@ -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';
|
||||
@@ -20,6 +19,7 @@ function DetailedGrades({ intl }) {
|
||||
} = useSelector(state => state.courseHome);
|
||||
const {
|
||||
org,
|
||||
tabs,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
const {
|
||||
gradesFeatureIsFullyLocked,
|
||||
@@ -37,11 +37,14 @@ function DetailedGrades({ intl }) {
|
||||
});
|
||||
};
|
||||
|
||||
const outlineLink = (
|
||||
const overviewTab = tabs.find(tab => tab.slug === 'outline');
|
||||
const overviewTabUrl = overviewTab && overviewTab.url;
|
||||
|
||||
const outlineLink = overviewTabUrl && (
|
||||
<Hyperlink
|
||||
variant="muted"
|
||||
isInline
|
||||
destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`}
|
||||
destination={overviewTabUrl}
|
||||
onClick={logOutlineLinkClick}
|
||||
tabIndex={gradesFeatureIsFullyLocked ? '-1' : '0'}
|
||||
>
|
||||
@@ -64,14 +67,16 @@ function DetailedGrades({ intl }) {
|
||||
{!hasSectionScores && (
|
||||
<p className="small">{intl.formatMessage(messages.detailedGradesEmpty)}</p>
|
||||
)}
|
||||
<p className="x-small m-0">
|
||||
<FormattedMessage
|
||||
id="progress.ungradedAlert"
|
||||
defaultMessage="For progress on ungraded aspects of the course, view your {outlineLink}."
|
||||
description="Text that precede link that redirect to course outline page"
|
||||
values={{ outlineLink }}
|
||||
/>
|
||||
</p>
|
||||
{overviewTabUrl && (
|
||||
<p className="x-small m-0">
|
||||
<FormattedMessage
|
||||
id="progress.ungradedAlert"
|
||||
defaultMessage="For progress on ungraded aspects of the course, view your {outlineLink}."
|
||||
description="Text that precede link that redirect to course outline page"
|
||||
values={{ outlineLink }}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
@@ -17,6 +19,7 @@ function DetailedGradesTable({ intl }) {
|
||||
sectionScores,
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
return (
|
||||
sectionScores.map((chapter) => {
|
||||
const subsectionScores = chapter.subsections.filter(
|
||||
@@ -32,7 +35,7 @@ function DetailedGradesTable({ intl }) {
|
||||
|
||||
const detailedGradesData = subsectionScores.map((subsection) => ({
|
||||
subsectionTitle: <SubsectionTitleCell subsection={subsection} />,
|
||||
score: <span className={subsection.learnerHasAccess ? '' : 'greyed-out'}>{subsection.numPointsEarned}/{subsection.numPointsPossible}</span>,
|
||||
score: <span className={subsection.learnerHasAccess ? '' : 'greyed-out'}>{subsection.numPointsEarned}{isLocaleRtl ? '\\' : '/'}{subsection.numPointsPossible}</span>,
|
||||
}));
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,18 +2,21 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function ProblemScoreDrawer({ intl, problemScores, subsection }) {
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
return (
|
||||
<span className="row w-100 m-0 x-small ml-4 pt-2 pl-1 text-gray-700 flex-nowrap">
|
||||
<span id="problem-score-label" className="col-auto p-0">{intl.formatMessage(messages.problemScoreLabel)}</span>
|
||||
<div className={classNames('col', 'p-0', { 'greyed-out': !subsection.learnerHasAccess })}>
|
||||
<ul className="list-unstyled row w-100 m-0" aria-labelledby="problem-score-label">
|
||||
{problemScores.map(problemScore => (
|
||||
<li className="ml-3">{problemScore.earned}/{problemScore.possible}</li>
|
||||
<li className="ml-3">{problemScore.earned}{isLocaleRtl ? '\\' : '/'}{problemScore.possible}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
@@ -66,13 +68,15 @@ function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
||||
|
||||
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type);
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
return {
|
||||
type: {
|
||||
footnoteId, footnoteMarker, type: assignment.type, locked,
|
||||
},
|
||||
weight: { weight: `${(assignment.weight * 100).toFixed(0)}%`, locked },
|
||||
grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}%`, locked },
|
||||
weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}%`, locked },
|
||||
weight: { weight: `${(assignment.weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
@@ -22,11 +24,13 @@ function GradeSummaryTableFooter({ intl }) {
|
||||
const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100';
|
||||
const totalGrade = (percent * 100).toFixed(0);
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
return (
|
||||
<DataTable.TableFooter className={`border-top border-primary ${bgColor}`}>
|
||||
<div className="row w-100 m-0">
|
||||
<div id="weighted-grade-summary" className="col-8 p-0 small">{intl.formatMessage(messages.weightedGradeSummary)}</div>
|
||||
<div data-testid="gradeSummaryFooterTotalWeightedGrade" aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}%</div>
|
||||
<div data-testid="gradeSummaryFooterTotalWeightedGrade" aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}{isLocaleRtl && '\u200f'}%</div>
|
||||
</div>
|
||||
</DataTable.TableFooter>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
@@ -16,6 +15,7 @@ function RelatedLinks({ intl }) {
|
||||
} = useSelector(state => state.courseHome);
|
||||
const {
|
||||
org,
|
||||
tabs,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
@@ -28,22 +28,31 @@ function RelatedLinks({ intl }) {
|
||||
});
|
||||
};
|
||||
|
||||
const overviewTab = tabs.find(tab => tab.slug === 'outline');
|
||||
const overviewTabUrl = overviewTab && overviewTab.url;
|
||||
const datesTab = tabs.find(tab => tab.slug === 'dates');
|
||||
const datesTabUrl = datesTab && datesTab.url;
|
||||
|
||||
return (
|
||||
<section className="mb-4 x-small">
|
||||
<h3 className="h4">{intl.formatMessage(messages.relatedLinks)}</h3>
|
||||
<ul className="pl-4">
|
||||
{datesTabUrl && (
|
||||
<li>
|
||||
<Hyperlink destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/dates`} onClick={() => logLinkClicked('dates')}>
|
||||
<Hyperlink destination={datesTabUrl} onClick={() => logLinkClicked('dates')}>
|
||||
{intl.formatMessage(messages.datesCardLink)}
|
||||
</Hyperlink>
|
||||
<p>{intl.formatMessage(messages.datesCardDescription)}</p>
|
||||
</li>
|
||||
)}
|
||||
{overviewTabUrl && (
|
||||
<li>
|
||||
<Hyperlink destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`} onClick={() => logLinkClicked('course_outline')}>
|
||||
<Hyperlink destination={overviewTabUrl} onClick={() => logLinkClicked('course_outline')}>
|
||||
{intl.formatMessage(messages.outlineCardLink)}
|
||||
</Hyperlink>
|
||||
<p>{intl.formatMessage(messages.outlineCardDescription)}</p>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -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,25 @@ 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 access denied page for a incorrect_active_enterprise error code', async () => {
|
||||
const { courseMetadata } = setUpWithDeniedStatus('incorrect_active_enterprise');
|
||||
await loadContainer();
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/course/${courseMetadata.id}/access-denied`);
|
||||
});
|
||||
|
||||
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 +519,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,19 @@ 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}`);
|
||||
}}
|
||||
/>
|
||||
<PageRoute
|
||||
path={`${path}/home/:courseId`}
|
||||
render={({ match }) => {
|
||||
global.location.assign(`/course/${match.params.courseId}/home`);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
|
||||
52
src/courseware/CoursewareRedirectLandingPage.test.jsx
Normal file
52
src/courseware/CoursewareRedirectLandingPage.test.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { render, initializeMockApp } from '../setupTest';
|
||||
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 () => {
|
||||
await initializeMockApp();
|
||||
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');
|
||||
});
|
||||
|
||||
it('Redirects to correct consent URL', () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/redirect/home/course-v1:edX+DemoX+Demo_Course'],
|
||||
});
|
||||
|
||||
render(
|
||||
<Router history={history}>
|
||||
<CoursewareRedirectLandingPage />
|
||||
</Router>,
|
||||
);
|
||||
|
||||
expect(redirectUrl).toHaveBeenCalledWith('/course/course-v1:edX+DemoX+Demo_Course/home');
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { screen, render } from '@testing-library/react';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useModel, useModels } from '../../generic/model-store';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
|
||||
@@ -106,14 +107,16 @@ describe('CourseBreadcrumbs', () => {
|
||||
],
|
||||
]);
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<CourseBreadcrumbs
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
|
||||
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
|
||||
isStaff
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
<IntlProvider>
|
||||
<BrowserRouter>
|
||||
<CourseBreadcrumbs
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
|
||||
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
|
||||
isStaff
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
</IntlProvider>,
|
||||
);
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
expect(screen.queryAllByRole('link')).toHaveLength(1);
|
||||
|
||||
@@ -57,14 +57,16 @@ function WeeklyGoalCelebrationModal({
|
||||
className="mr-2"
|
||||
style={{ height: '21px', width: '22px' }}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id="learning.celebration.setGoal"
|
||||
defaultMessage="Setting a goal can help you {strongText} in your course."
|
||||
description="It explain the advantages of setting goal"
|
||||
values={{
|
||||
strongText: (<strong>achieve higher performance</strong>),
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="learning.celebration.setGoal"
|
||||
defaultMessage="Setting a goal can help you {strongText} in your course."
|
||||
description="It explain the advantages of setting goal"
|
||||
values={{
|
||||
strongText: (<strong>achieve higher performance</strong>),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</StandardModal>
|
||||
|
||||
@@ -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,65 @@ 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',
|
||||
}),
|
||||
{ exact: false },
|
||||
)).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;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('Sequence Content', () => {
|
||||
courseId: courseware.courseId,
|
||||
sequenceId: courseware.sequenceId,
|
||||
unitId: models.sequences[courseware.sequenceId].unitIds[0],
|
||||
unitLoadedHandler: () => {},
|
||||
unitLoadedHandler: () => { },
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'learn.contentLock.complete.prerequisite': {
|
||||
id: 'learn.contentLock.complete.prerequisite',
|
||||
defaultMessage: "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
||||
defaultMessage: "You must complete the prerequisite: ''{prereqSectionName}'' to access this content.",
|
||||
description: 'Message shown to indicate which prerequisite the student must complete prior to accessing the locked content. {prereqSectionName} is the name of the prerequisite.',
|
||||
},
|
||||
'learn.contentLock.goToSection': {
|
||||
|
||||
@@ -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,10 +16,10 @@ 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}`;
|
||||
const discussionsUrl = `${getConfig().DISCUSSIONS_MFE_BASE_URL}/${courseId}/category/${unitId}`;
|
||||
return (
|
||||
<SidebarBase
|
||||
title={intl.formatMessage(messages.discussionsTitle)}
|
||||
@@ -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}/category/${unitId}?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;
|
||||
|
||||
63
src/generic/CourseAccessErrorPage.jsx
Normal file
63
src/generic/CourseAccessErrorPage.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Redirect } from 'react-router';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import useActiveEnterpriseAlert from '../alerts/active-enteprise-alert';
|
||||
import { AlertList } from './user-messages';
|
||||
import { fetchDiscussionTab } from '../course-home/data/thunks';
|
||||
import { LOADED, LOADING } from '../course-home/data/slice';
|
||||
import PageLoading from './PageLoading';
|
||||
import messages from '../tab-page/messages';
|
||||
|
||||
function CourseAccessErrorPage({ intl }) {
|
||||
const { courseId } = useParams();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const activeEnterpriseAlert = useActiveEnterpriseAlert(courseId);
|
||||
useEffect(() => {
|
||||
dispatch(fetchDiscussionTab(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
const {
|
||||
courseStatus,
|
||||
} = useSelector(state => state.courseHome);
|
||||
|
||||
if (courseStatus === LOADING) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<PageLoading
|
||||
srMessage={intl.formatMessage(messages.loading)}
|
||||
/>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (courseStatus === LOADED) {
|
||||
return (<Redirect to={`/redirect/home/${courseId}`} />);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main id="main-content" className="container my-5 text-center" data-testid="access-denied-main">
|
||||
<AlertList
|
||||
topic="outline"
|
||||
className="mx-5 mt-3"
|
||||
customAlerts={{
|
||||
...activeEnterpriseAlert,
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CourseAccessErrorPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseAccessErrorPage);
|
||||
58
src/generic/CourseAccessErrorPage.test.jsx
Normal file
58
src/generic/CourseAccessErrorPage.test.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { Route } from 'react-router';
|
||||
import { initializeTestStore, render, screen } from '../setupTest';
|
||||
import CourseAccessErrorPage from './CourseAccessErrorPage';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
let mockCourseStatus;
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
useSelector: () => ({ courseStatus: mockCourseStatus }),
|
||||
}));
|
||||
jest.mock('./PageLoading', () => () => <div data-testid="page-loading" />);
|
||||
|
||||
describe('CourseAccessErrorPage', () => {
|
||||
let courseId;
|
||||
let accessDeniedUrl;
|
||||
beforeEach(async () => {
|
||||
const store = await initializeTestStore({ excludeFetchSequence: true });
|
||||
courseId = store.getState().courseware.courseId;
|
||||
accessDeniedUrl = `/course/${courseId}/access-denied`;
|
||||
history.push(accessDeniedUrl);
|
||||
});
|
||||
|
||||
it('Displays loading in start on page rendering', () => {
|
||||
mockCourseStatus = 'loading';
|
||||
render(
|
||||
<Route path="/course/:courseId/access-denied">
|
||||
<CourseAccessErrorPage />
|
||||
</Route>,
|
||||
);
|
||||
expect(screen.getByTestId('page-loading')).toBeInTheDocument();
|
||||
expect(history.location.pathname).toBe(accessDeniedUrl);
|
||||
});
|
||||
|
||||
it('Redirect user to homepage if user has access', () => {
|
||||
mockCourseStatus = 'loaded';
|
||||
render(
|
||||
<Route path="/course/:courseId/access-denied">
|
||||
<CourseAccessErrorPage />
|
||||
</Route>,
|
||||
);
|
||||
expect(history.location.pathname).toBe('/redirect/home/course-v1:edX+DemoX+Demo_Course');
|
||||
});
|
||||
|
||||
it('For access denied it should render access denied page', () => {
|
||||
mockCourseStatus = 'denied';
|
||||
|
||||
render(
|
||||
<Route path="/course/:courseId/access-denied">
|
||||
<CourseAccessErrorPage />
|
||||
</Route>,
|
||||
);
|
||||
expect(screen.getByTestId('access-denied-main')).toBeInTheDocument();
|
||||
expect(history.location.pathname).toBe(accessDeniedUrl);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,444 +1,447 @@
|
||||
{
|
||||
"learning.accessExpiration.deadline": "قم بالترقية قبل {date} للحصول على صلاحية دخول غير محدود طالما أنه موجود على الموقع.",
|
||||
"learning.accessExpiration.header": "تنتهي صلاحية حضرور الدورة كمستمع في {date}",
|
||||
"learning.accessExpiration.body": "ستفقد كل صلاحيات الدخول إلى هذا المساق، بما في ذلك مسار تقدمك في {date}.",
|
||||
"instructorToolbar.pageBanner.courseHasExpired": "لا يمتلك الطالب/ة صلاحية الدخول الى المقرر التعليمي, انتهت الصلاحية منذ {date}.",
|
||||
"learning.accessExpiration.upgradeNow": "ترقية الآن",
|
||||
"learning.outline.alert.start.short": "يبدأ المساق في غضون {timeRemaining} في {courseStartDate}.",
|
||||
"learning.outline.alert.end.long": "سينتهي المساق خلال {timeRemaining} في {courseEndDate}.",
|
||||
"learning.outline.alert.end.calendar": "لا تنسى إضافة تذكير في التقويم!",
|
||||
"instructorToolbar.pageBanner.courseHasNotStarted": "لا يمكن لالطالب الوصول لالمساق حتى الان. يبدأ المساق في {date}.",
|
||||
"learning.enrollment.alert": "يجب أن تكون مسجلا في المساق لمشاهدة المحتوى.",
|
||||
"learning.staff.enrollment.alert": "أنت تستعرض هذا المساق كفرد من فريق طاقم المساق، ولم تلتحق بعد.",
|
||||
"learning.enrollment.enrollNow.Inline": "التحق الآن",
|
||||
"learning.enrollment.enrollNow.Sentence": "التحق الآن",
|
||||
"learning.enrollment.success": "تم التحاقك في هذا المساق بنجاح!",
|
||||
"account-activation.alert.button": "استمر الى {siteName}",
|
||||
"account-activation.alert.message": "تم ارسال بريد الكتروني الى {boldEmail} مع رابط لتفعيل الحساب. اذا لم تستطيع ان تجده يرجى فحص البريد في مجلد البريد المزعج او الغير مرغوب به ال (spam) \n {sendEmailTag}.",
|
||||
"account-activation.resend.link": "ارسل البريد مرة اخرى ",
|
||||
"learning.logistration.alert": "للاطلاع على محتوى المساق {signIn} أو {register}.",
|
||||
"account-activation.alert.title": "فعل حساب لكي تستطيع الدخول مرة أخرى",
|
||||
"learn.sequence.entranceExamTextNotPassing": "للوصول إلى مواد الدورة التدريبية ، يجب أن تحصل على درجة {entranceExamMinimumScorePct}٪ أو أعلى في هذا الاختبار. نتيجتك الحالية هي {entranceExamCurrentScore}٪.",
|
||||
"learn.sequence.entranceExamTextPassed": "نتيجتك هي {entranceExamCurrentScore}٪. لقد اجتزت امتحان القبول.",
|
||||
"learning.accessExpiration.deadline": "قم بالترقية قبل {date} للاستفادة من دخول غير محدود للمساق طالما هو موجود على الموقع.",
|
||||
"learning.accessExpiration.header": "تنتهي صلاحية دخول المساق كمستمع في {date}",
|
||||
"learning.accessExpiration.body": "ستفقد كل صلاجيات الدخول إلى هذا المساق مع أي تقدم حققته. و ذلك اعتبارًا من {date}.",
|
||||
"instructorToolbar.pageBanner.courseHasExpired": "لم يعد هذا المتعلّم مخولاً بالدخول لهذا المساق. لقد انتهت صلاحية بتاريخ {date}.",
|
||||
"learning.accessExpiration.upgradeNow": "الترقية الآن",
|
||||
"learning.activeEnterprise.alert": "{changeActiveEnterprise}.",
|
||||
"learning.activeEnterprise.change.alert": "تغيير المؤسسة الآن",
|
||||
"learning.outline.alert.start.short": "يبدأ المساق في {timeRemaining} بتاريخ {courseStartDate}.",
|
||||
"learning.outline.alert.end.long": "ينتهي المساق في {timeRemaining} في {courseEndDate}.",
|
||||
"learning.outline.alert.end.calendar": "لا تنسَ إضافة تذكير في التقويم!",
|
||||
"instructorToolbar.pageBanner.courseHasNotStarted": "لا يمكن للطالب بعدُ الدخول للمساق. يبدأ المساق في {date}.",
|
||||
"learning.enrollment.alert": "يجب أن تكون مسجلا في المساق لمشاهدة محتواه.",
|
||||
"learning.staff.enrollment.alert": "أنت تستعرض هذا المساق كعضو في الطاقم، و لست مسجلا فيه.",
|
||||
"learning.enrollment.enrollNow.Inline": "سجل الآن",
|
||||
"learning.enrollment.enrollNow.Sentence": "سجل الآن.",
|
||||
"learning.enrollment.success": "تم تسجيلك في هذا المساق بنجاح!",
|
||||
"account-activation.alert.button": "المواصلة إلى {siteName}",
|
||||
"account-activation.alert.message": "تم ارسال بريد الكتروني الى {boldEmail} به رابط لتفعيل حسابك. لم تجده؟ تفقد إذًا مجلد الرسائل غير المرغوب فيها وإلا \nف{sendEmailTag}.",
|
||||
"account-activation.resend.link": "أرسل البريد مرة اخرى ",
|
||||
"learning.logistration.alert": "لرؤية محتوى المساق، {signIn} أو {register}.",
|
||||
"account-activation.alert.title": "فعّل حسابك كي تستطيع الدخول مجددًا",
|
||||
"learn.sequence.entranceExamTextNotPassing": "للوصول إلى مواد المساق، يجب أن تحصل في هذا الاختبار على درجة {entranceExamMinimumScorePct}% أو أعلى. درجتك الحالية هي {entranceExamCurrentScore}%.",
|
||||
"learn.sequence.entranceExamTextPassed": "درجتك هي {entranceExamCurrentScore}%. لقد اجتزت امتحان القبول.",
|
||||
"learning.dates.badge.completed": "مُكتمل",
|
||||
"learning.dates.badge.dueNext": "موعد الاستحقاق التالي",
|
||||
"learning.dates.badge.pastDue": "مضى تاريخ الاستحقاق",
|
||||
"learning.dates.title": "تواريخ مهمّة",
|
||||
"learning.dates.badge.dueNext": "الأجَل قادم",
|
||||
"learning.dates.badge.pastDue": "انقضى الأجَل",
|
||||
"learning.dates.title": "التواريخ المهمة",
|
||||
"learning.dates.badge.today": "اليوم",
|
||||
"learning.dates.badge.unreleased": "لم يتم الإصدار بعد",
|
||||
"learning.dates.badge.verifiedOnly": "موثق فقط",
|
||||
"learning.goals.unsubscribe.contact": "الاتصال بفريق الدعم",
|
||||
"learning.goals.unsubscribe.description": "لن تستقبل اشعارات على بريدك الالكتروني لتذكيرك باهدافك ل {courseTitle}.",
|
||||
"learning.dates.badge.unreleased": "لم يصدر بعد",
|
||||
"learning.dates.badge.verifiedOnly": "حصري للمعتمدين",
|
||||
"learning.goals.unsubscribe.contact": "اتصل بالدعم",
|
||||
"learning.goals.unsubscribe.description": "لن يصلك مستقبلا أي تذكير بهدفك لـ {courseTitle} عبر البريد الإلكتروني.",
|
||||
"learning.goals.unsubscribe.errorHeader": "حصل خطأ ما",
|
||||
"learning.goals.unsubscribe.goToDashboard": "الذهاب إلى لوحة المعلومات",
|
||||
"learning.goals.unsubscribe.header": "قمت بالغاء اشتراكك في شعارات التذكيرية لاهداف",
|
||||
"learning.goals.unsubscribe.loading": "يجري الغاء الاشتراك...",
|
||||
"learning.goals.unsubscribe.errorDescription": "لم نستطع الغاء اشتراكك في اشعارات التذكيرية باهدافك على بريدك الالكتروني. رجاءا حاول مرة اخرى او {contactSupport} للمساعدة.",
|
||||
"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": "أنت غير مؤهل بعد للحصول على شهادة",
|
||||
"learning.goals.unsubscribe.header": "لقد ألغيت اشتراكك في رسائل التذكير بالهدف",
|
||||
"learning.goals.unsubscribe.loading": "إلغاء الاشتراك جارٍ...",
|
||||
"learning.goals.unsubscribe.errorDescription": "لم نستطع الغاء اشتراكك في رسائل التذكير بالهدف. رجاءً حاول مجددًا في وقت لاحق أو {contactSupport} للمساعدة.",
|
||||
"learning.outline.alert.cert.earnedNotAvailable": "ينتهي هذا المساق في {courseEndDateFormatted}. تمت جدولة ظهور الدرجات النهائية و أي شهادات مكتسبة ابتداءً من {certificateAvailableDate}.",
|
||||
"cert.alert.earned.unavailable.header.v2": "ستظهر درجتك و حالة شهادتك قريبًا.",
|
||||
"cert.alert.earned.ready.header": "تهانينا! شهادتك جاهزة.",
|
||||
"cert.alert.notPassing.header": "لست مؤهلا بعد للحصول على شهادة",
|
||||
"cert.alert.notPassing.button": "عرض الدرجات",
|
||||
"learning.outline.alert.end.short": "ينتهي هذا المساق في غضون {timeRemaining}في {courseEndTime}.",
|
||||
"alert.enroll": "للوصول إلى محتوى الدورة الكاملة.",
|
||||
"learning.privateCourse.signInOrRegister": "{signIn} أو {register} ثم التحق بهذا المساق.",
|
||||
"learning.outline.alert.scheduled-content.heading": "المزيد من المحتوى قريبا!",
|
||||
"learning.outline.alert.scheduled-content.body": "سيتم إصدار المزيد من المحتوى في هذه الدورة التدريبية في تاريخ لاحق. ابقى على اطلاع بتحديثات البريد الإلكتروني أو تحقق في تحديثات هذه الدورة التدريبية لاحقاَ.",
|
||||
"learning.outline.alert.scheduled-content.button": "عرض الجدول الزمني للدورة",
|
||||
"learning.outline.alert.end.short": "ينتهي هذا المساق في غضون {timeRemaining} في {courseEndTime}.",
|
||||
"alert.enroll": "للوصول إلى كامل المساق.",
|
||||
"learning.privateCourse.signInOrRegister": "{signIn} أو {register} أولاً، ثم سجل نفسك في المساق.",
|
||||
"learning.outline.alert.scheduled-content.heading": "مزيد من المحتوى قادم قريبا!",
|
||||
"learning.outline.alert.scheduled-content.body": "سيصدر لهذا المساق مزيد المحتوى في تاريخ لاحق. ترقب التحديثات عبر البريد الإلكتروني أو تفقّد هذا المساق لاحقًا من أجل التحديثات.",
|
||||
"learning.outline.alert.scheduled-content.button": "عرض مواعيد المساق",
|
||||
"learning.outline.dates.all": "عرض جميع تواريخ المساق",
|
||||
"learning.outline.goalButton.casual.text": "يوم واحد في الأسبوع",
|
||||
"learning.outline.goalButton.screenReader.text": "غير رسمي",
|
||||
"learning.outline.certificateAlt": "عينة الشهادة",
|
||||
"learning.outline.collapseAll": "اغلاق الكل",
|
||||
"learning.outline.completedAssignment": "اكتمل",
|
||||
"learning.outline.certificateAlt": "نموذج عن الشهادة",
|
||||
"learning.outline.collapseAll": "طي الكل",
|
||||
"learning.outline.completedAssignment": "مكتمل",
|
||||
"learning.outline.completedSection": "قسم مكتمل",
|
||||
"learning.outline.dates": "تواريخ مهمّة",
|
||||
"learning.outline.editGoal": "تحرير الهدف",
|
||||
"learning.outline.expandAll": "توسيع الكل",
|
||||
"learning.outline.dates": "التواريخ المهمة",
|
||||
"learning.outline.editGoal": "تعديل الهدف",
|
||||
"learning.outline.expandAll": "تكبير الكل",
|
||||
"learning.outline.goal": "الهدف",
|
||||
"learning.outline.goalReminderDetail": "إذا لاحظنا أنك لم تحقق هدفك تمامًا ، فسنرسل إليك تذكيرًا بالبريد الإلكتروني.",
|
||||
"learning.outline.goalReminderDetail": "إن لاحظنا أنك لم تحقق هدفك تمامًا، فسنرسل إليك تذكيرًا بالبريد الإلكتروني.",
|
||||
"learning.outline.goalUnsure": "لست متأكدا بعد",
|
||||
"learning.outline.handouts": "نشرات المساق",
|
||||
"learning.outline.incompleteAssignment": " بيانات غير مستكملة",
|
||||
"learning.outline.handouts": "مطبوعات المساق",
|
||||
"learning.outline.incompleteAssignment": "غير مكتمل",
|
||||
"learning.outline.incompleteSection": "قسم غير مكتمل",
|
||||
"learning.outline.goalButton.intense.text": "5 أيام في الأسبوع",
|
||||
"learning.outline.goalButton.intense.title": "شديد",
|
||||
"learning.outline.learnMore": "اعرف المزيد",
|
||||
"learning.outline.learnMore": "معرفة المزيد",
|
||||
"learning.outline.altText.openSection": "فتح",
|
||||
"learning.proctoringPanel.header": "تحتوي هذه الدورة على امتحانات مراقبة",
|
||||
"learning.proctoringPanel.header": "يحتوي هذا المساق امتحانات مراقبة",
|
||||
"learning.outline.goalButton.regular.text": "3 أيام في الأسبوع",
|
||||
"learning.outline.goalButton.regular.title": "عادي",
|
||||
"learning.outline.resumeBlurb": "تابع من حيث توقفت",
|
||||
"learning.outline.resume": "استئناف الدورة",
|
||||
"learning.outline.setGoal": "للبدء اضبط هدفًا للمساق عن طريق تحديد الخيار أدناه والذي يعطي الوصف الأمثل لخطتك التعليمية",
|
||||
"learning.outline.setGoalReminder": "تفعيل خاصية تذكير الهدف",
|
||||
"learning.outline.goalButton.casual.title": "حدد أسلوب هدف التعلم.",
|
||||
"learning.outline.setWeeklyGoal": "حدد هدف التعلم الأسبوعي",
|
||||
"learning.outline.setWeeklyGoalDetail": "إن تحديد هدف يحفزك على إنهاء الدورة. يمكنك دائما تغييره في وقت لاحق.",
|
||||
"learning.outline.start": "ابدأ المساق",
|
||||
"learning.outline.startBlurb": "ابدأ المساق اليوم",
|
||||
"learning.outline.resumeBlurb": "استأنف من حيث توقفت",
|
||||
"learning.outline.resume": "استئناف المساق",
|
||||
"learning.outline.setGoal": "للبدء، اضبط هدفًا للمساق بتحديد أكثر خيار أدناه يصف خطتك التعلمية",
|
||||
"learning.outline.setGoalReminder": "حدد ما يذكرك بالهدف",
|
||||
"learning.outline.goalButton.casual.title": "حدد أسلوبًا للهدف التعلمي.",
|
||||
"learning.outline.setWeeklyGoal": "حدد هدفًا تعلميًا أسبوعيًا",
|
||||
"learning.outline.setWeeklyGoalDetail": "إن تحديد هدف يحفزك على إنهاء المساق. سيبقى بإمكانك تغييره لاحقا.",
|
||||
"learning.outline.start": "بدأ المساق",
|
||||
"learning.outline.startBlurb": "ابدأ مساقك اليوم",
|
||||
"learning.outline.tools": "أدوات المساق",
|
||||
"learning.outline.upgradeButton": "ترقية ({symbol}{price})",
|
||||
"learning.outline.upgradeTitle": "اسعَ للحصول على شهادة معتمدة",
|
||||
"learning.outline.welcomeMessage": "رسالة ترحيب",
|
||||
"learning.outline.welcomeMessageShowMoreButton": "إظهار المزيد",
|
||||
"learning.outline.welcomeMessageShowLessButton": "إظهار أقل",
|
||||
"learning.outline.goalWelcome": "مرحبًا بك في",
|
||||
"learning.proctoringPanel.status.notStarted": "لم تبدأ بعد",
|
||||
"learning.proctoringPanel.status.started": "بدأ في تاريخ",
|
||||
"learning.proctoringPanel.status.submitted": "تم الإرسال ",
|
||||
"learning.proctoringPanel.status.verified": "تمّ التحقّق",
|
||||
"learning.outline.welcomeMessage": "الكلمة الترحيبية",
|
||||
"learning.outline.welcomeMessageShowMoreButton": "إظهار المزيد",
|
||||
"learning.outline.welcomeMessageShowLessButton": "إظهار القليل",
|
||||
"learning.outline.goalWelcome": "أهلاً بك في",
|
||||
"learning.proctoringPanel.status.notStarted": "لم يبدأ بعد",
|
||||
"learning.proctoringPanel.status.started": "بدأ في",
|
||||
"learning.proctoringPanel.status.submitted": "مُرسَل",
|
||||
"learning.proctoringPanel.status.verified": "معتمد",
|
||||
"learning.proctoringPanel.status.rejected": "مرفوض",
|
||||
"learning.proctoringPanel.status.error": "خطأ",
|
||||
"learning.proctoringPanel.status.otherCourseApproved": "معتمد من مساق أخرى",
|
||||
"learning.proctoringPanel.status.expiringSoon": "تنتهي قريبا",
|
||||
"learning.proctoringPanel.status": "حالة التهيئة الحالية:",
|
||||
"learning.proctoringPanel.message.notStarted": "لم تبدأ امتحان التهيئة الخاص بك.",
|
||||
"learning.proctoringPanel.message.started": "لقد بدأت اختبار التهيئة الخاص بك.",
|
||||
"learning.proctoringPanel.message.submitted": "لقد قدمت امتحان التهيئة الخاص بك.",
|
||||
"learning.proctoringPanel.message.verified": "تمت الموافقة على امتحان التهيئة الخاص بك في هذه الدورة التدريبية.",
|
||||
"learning.proctoringPanel.message.rejected": "تم رفض امتحان التهيئة الخاص بك. يرجى إعادة المحاولة امتحان التهيئة.",
|
||||
"learning.proctoringPanel.message.error": "حدث خطأ أثناء امتحان التهيئة الخاص بك. يرجى إعادة المحاولة امتحان التهيئة.",
|
||||
"learning.proctoringPanel.message.otherCourseApproved": "تمت الموافقة على امتحان التهيئة الخاص بك من دورة أخرى.",
|
||||
"learning.proctoringPanel.detail.otherCourseApproved": "إذا تم تغيير جهازك ، نوصيك بإكمال امتحان التهيئة لهذه الدورة التدريبية للتأكد من أن إعدادتك لا تزال نفي بمتطلبات المراقبة.",
|
||||
"learning.proctoringPanel.message.expiringSoon": "تمت الموافقة على ملف التعريف الخاص بك لامتحان التهيئة من دورة أخرى. ومع ذلك ، تنتهي صلاحية امتحان التهيئة الخاصة بك قريبًا. يرجى اجراء امتحان التهيئة مرة أخرى للتأكد من أنك ستتمكن من الاستمرار في إجراء الاختبارات المراقبة.",
|
||||
"learning.proctoringPanel.generalInfo": "يجب عليك إكمال امتحان التهيئة قبل إجراء أي اختبار مراقب.",
|
||||
"learning.proctoringPanel.generalInfoSubmitted": "ملفك الشخصي المقدم قيد المراجعة.",
|
||||
"learning.proctoringPanel.generalTime": "يمكن أن تستغرق مراجعة الملف الشخصي لامتحان التهيئة على الأقل يومين عمل.",
|
||||
"learning.proctoringPanel.onboardingButton": "ابدأ امتحان التهيئة",
|
||||
"learning.proctoringPanel.onboardingPracticeButton": "عرض امتحان التهيئة",
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "يفتح امتحان التهيئة في : {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "مراجعة التعليمات ومتطلبات النظام",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "امتحان التهيئة تجاوز تاريخ الاستحقاق",
|
||||
"learning.outline.sequence-due": "{description} في{assignmentDue}",
|
||||
"progress.certificateStatus.unverifiedBody": "لإنشاء شهادة ، يجب عليك إكمال عملية التحقق من الهوية. {idVerificationSupportLink}.",
|
||||
"progress.certificateStatus.downloadableBody": "اعرض إنجازاتك على لينكد ان أو على سيرتك الذاتية اليوم. يمكنك تنزيل شهادتك الآن والوصول إليها في أي وقت من لوحة التحكم والملف الشخصي.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "This course ends on {endDate}. Final grades and any earned certificates are\n scheduled to be available after {certAvailabilityDate}.",
|
||||
"learning.proctoringPanel.status.otherCourseApproved": "معتمد من مساق آخر",
|
||||
"learning.proctoringPanel.status.expiringSoon": "تنتهي صلاحيته قريبا",
|
||||
"learning.proctoringPanel.status.expired": "Expired",
|
||||
"learning.proctoringPanel.status": "وضع الامتحان التحضيري حاليا:",
|
||||
"learning.proctoringPanel.message.notStarted": "لم تبدأ امتحانك التحضيري.",
|
||||
"learning.proctoringPanel.message.started": "لقد بدأت امتحانك التحضيري.",
|
||||
"learning.proctoringPanel.message.submitted": "لقد سلمت امتحانك التحضيري.",
|
||||
"learning.proctoringPanel.message.verified": "تم اعتماد امتحانك التحضيري في هذا المساق.",
|
||||
"learning.proctoringPanel.message.rejected": "تم رفض امتحانك التحضيري. رجاءً حاول إجراء الامتحان مجددًا.",
|
||||
"learning.proctoringPanel.message.error": "حدث خطأ أثناء امتحان التهيئة الخاص بك. رجاءً حاول إجراء الامتحان مجددًا.",
|
||||
"learning.proctoringPanel.message.otherCourseApproved": "تم اعتماد امتحانك التحضيري من مساق آخر.",
|
||||
"learning.proctoringPanel.detail.otherCourseApproved": "إن تغير جهازك، فإننا نصحك بإكمال الامتحان التحضيري لهذا المساق، و ذلك للتأكد من أن إعداداتك لا تزال تستوفي متطلبات المراقبة.",
|
||||
"learning.proctoringPanel.message.expiringSoon": "Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.",
|
||||
"learning.proctoringPanel.message.expired": "Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.",
|
||||
"learning.proctoringPanel.generalInfo": "عليك إتمام إجراءات التحضير قبل إجراء أي امتحان مراقب.",
|
||||
"learning.proctoringPanel.generalInfoSubmitted": "ملفك الشخصي المرسَل قيد المراجعة.",
|
||||
"learning.proctoringPanel.generalTime": "قد تستغرق مراجعة الملف الشخصي للامتحان التحضيري يومي عمل على الأقل.",
|
||||
"learning.proctoringPanel.onboardingButton": "إكمال الامتحان التحضيري",
|
||||
"learning.proctoringPanel.onboardingPracticeButton": "عرض الامتحان التحضيري",
|
||||
"learning.proctoringPanel.onboardingButtonNotOpen": "يفتح الامتحان التحضيري في: {releaseDate}",
|
||||
"learning.proctoringPanel.reviewRequirementsButton": "مراجعة التعليمات و متطلبات النظام",
|
||||
"learning.proctoringPanel.onboardingButtonPastDue": "انقضى أجَل الامتحان التحضيري",
|
||||
"learning.outline.sequence-due": "{description} للتسليم قبل {assignmentDue}",
|
||||
"progress.certificateStatus.unverifiedBody": "لتوليد شهادة، يجب عليك إكمال التحقق من هويتك. {idVerificationSupportLink}.",
|
||||
"progress.certificateStatus.downloadableBody": "اعرض إنجازاتك اليوم على لينكد إن أو ضمن سيرتك الذاتية. يمكنك تحميل شهادتك الآن و الوصول إليها في أي وقت من لوحة معلوماتك و ملفك الشخصي.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate": "تمت جدولة ظهور الدرجات النهائية و أي شهادات مكتسبة ابتداءً من {endDate}.",
|
||||
"progress.certificateStatus.notPassingHeader": "حالة الشهادة",
|
||||
"progress.certificateStatus.notPassingBody": "من أجل التأهل للحصول على شهادة ، يجب أن تكون حاصلاً على درجة النجاح.",
|
||||
"progress.certificateStatus.inProgressHeader": "المزيد من المحتوى قريبا!",
|
||||
"progress.certificateStatus.inProgressBody": "يبدو أن هناك المزيد من المحتوى في هذه الدورة والذي سيتم إصداره في المستقبل. ابقى على اطلاع بتحديثات عن طربق البريد الإلكتروني أو تحقق مرة أخرى من صفحة الدورة لمعرفة وقت توفر المحتوى الجديد.",
|
||||
"progress.certificateStatus.notPassingBody": "كي تكون أهلا لنيل شهادة، يجب أن تحصل على درجة نجاح.",
|
||||
"progress.certificateStatus.inProgressHeader": "مزيد من المحتوى قادم قريبا!",
|
||||
"progress.certificateStatus.inProgressBody": "يبدو أن في هذا المساق مزيدا من المحتوى سيصدر مستقبلا. ستقبل. ترقب التحديثات عبر البريد الإلكتروني أو تفقّد هذا المساق لاحقًا لمعرفة موعد صدور هذا المحتوى.",
|
||||
"progress.certificateStatus.requestableHeader": "حالة الشهادة",
|
||||
"progress.certificateStatus.requestableBody": "تهانينا ، انت مؤهل للحصول على شهادة! من أجل الوصول إلى شهادتك ، اطلبها أدناه.",
|
||||
"progress.certificateStatus.requestableButton": "طلب شهادة",
|
||||
"progress.certificateStatus.requestableBody": "تهانينا، انت مؤهل للحصول على شهادة! للوصول إلى شهادتك، اطلبها أدناه.",
|
||||
"progress.certificateStatus.requestableButton": "طلب الشهادة",
|
||||
"progress.certificateStatus.unverifiedHeader": "حالة الشهادة",
|
||||
"progress.certificateStatus.unverifiedButton": "التحقق من الهوية",
|
||||
"progress.certificateStatus.courseCelebration.verificationPending": "عملية التحقق من المعرّف الخاص بك معلّقة وستتوفر شهادتك بمجرد الموافقة.",
|
||||
"progress.certificateStatus.downloadableHeader": "!شهادتك جاهزة",
|
||||
"progress.certificateStatus.downloadableButton": "تحميل شهاداتي",
|
||||
"progress.certificateStatus.viewableButton": "عرض شهاداتي",
|
||||
"progress.certificateStatus.courseCelebration.verificationPending": "طلب التحقق من هويتك قيد الانتظار، و ستتوفر شهادتك فور الموافقة علىه.",
|
||||
"progress.certificateStatus.downloadableHeader": "شهادتك جاهزة!",
|
||||
"progress.certificateStatus.viewableButton": "عرض شهادتي",
|
||||
"progress.certificateStatus.notAvailableHeader": "حالة الشهادة",
|
||||
"progress.certificateBody.notAvailable.endDate": "تمت جدولة ظهور الدرجات النهائية و أي شهادات مكتسبة ابتداءً من {endDate}.",
|
||||
"progress.certificateStatus.upgradeHeader": "احصل على شهادة.",
|
||||
"progress.certificateStatus.upgradeBody": "أنت في مسجل في المساق كمستمع ولست مؤهلاً للحصول على شهادة. من أجل الحصول على شهادة ، قم بترقية تسجيلك في المسافق اليوم.",
|
||||
"progress.certificateStatus.upgradeBody": "أنت في مسجل في المساق كمستمع ولست مؤهلاً للحصول على شهادة. من أجل الحصول على شهادة، قم بترقية تسجيلك في المساق اليوم.",
|
||||
"progress.certificateStatus.upgradeButton": "الترقية الآن",
|
||||
"progress.certificateStatus.unverifiedHomeHeader.v2": "Verify your identity to qualify for a certificate.",
|
||||
"progress.certificateStatus.unverifiedHomeButton": "تحقق من هويتي",
|
||||
"progress.certificateStatus.unverifiedHomeBody": "من أجل إنشاء شهادة لهذه الدورة ، يجب عليك إكمال عملية التحقق من الهوية.",
|
||||
"progress.certificateStatus.unverifiedHomeHeader.v2": "تحقق من هويتك حتى تتأهل للحصول على شهادة.",
|
||||
"progress.certificateStatus.unverifiedHomeButton": "التحقق من هويتي",
|
||||
"progress.certificateStatus.unverifiedHomeBody": "من أجل إنشاء شهادة لهذا المساق، يجب عليك إكمال عملية التحقق من الهوية.",
|
||||
"progress.completion.donut.label": "مكتمل",
|
||||
"progress.completion.body": "يمثل هذا مقدار محتوى الدورة الذي أكملته. لاحظ أن بعض المحتوى قد لا يكون متوفر حتى الان.",
|
||||
"progress.completion.body": "يمثل هذا مقدار ما أكملته من محتوى المساق. لاحظ أن بعض المحتوى قد لا يكون صادرًا بعد.",
|
||||
"progress.completion.tooltip.locked": "المحتوى الذي أكملته.",
|
||||
"progress.completion.header": "اكتمال الدورة",
|
||||
"progress.completion.tooltip": "المحتوى الذي يمكنك الوصول إليه ولم تكتمله بعد.",
|
||||
"progress.completion.tooltip.complete": "المحتوى المقفل والمتاح فقط لأولئك الذين قامو بترقية تسجلهم بالمساق.",
|
||||
"progress.completion.donut.percentComplete": "لقد أكملت {percent}٪ من محتوى هذا المساق.",
|
||||
"progress.completion.donut.percentIncomplete": "لم تكمل {percent}٪ من محتوى هذا المساق, بالنسبة للمحتوى الذي يمكنك الوصول إليه.",
|
||||
"progress.completion.donut.percentLocked": "{percent}٪ من المحتوى في هذه الدورة مقفول ومتاح فقط لمن قاموا بترقية تسجلهم بالمساق.",
|
||||
"progress.creditInformation.creditNotEligible": "لم تعد مؤهلاً للحصول على اعتماد في هذه الدورة التدريبية. اعرف المزيد عن {creditLink}.",
|
||||
"progress.creditInformation.creditEligible": "لقد استوفيت متطلبات الحصول على اعتماد في هذه الدورة. انتقل إلى {dashboardLink} لشراء اعتماد الدورة. أو تعرف على المزيد حول {creditLink}.",
|
||||
"progress.creditInformation.creditPartialEligible": "أنت لم تفي بمتطلبات اعتماد المساق. اعرف المزيد عن {creditLink}.",
|
||||
"progress.completion.header": "إكمال المساق",
|
||||
"progress.completion.tooltip": "المحتوى المتاح لك و الذي لم تكمله بعد.",
|
||||
"progress.completion.tooltip.complete": "المحتوى المقفل و المتاح فقط لمن يقوم بالترقية",
|
||||
"progress.completion.donut.percentComplete": "لقد أكملت {percent}% من محتوى هذا المساق.",
|
||||
"progress.completion.donut.percentIncomplete": "لم تكمل {percent}% من محتوى هذا المساق، بالنسبة للمحتوى الذي يمكنك الوصول إليه.",
|
||||
"progress.completion.donut.percentLocked": "{percent}% من محتوى هذا المساق مقفل و متاح فقط لمن يقوم بالترقية.",
|
||||
"progress.creditInformation.creditNotEligible": "لم تعد مؤهلاً للحصول على اعتماد في هذا المساق. اعرف المزيد عن {creditLink}.",
|
||||
"progress.creditInformation.creditEligible": "لقد استوفيت متطلبات الحصول على اعتماد في هذا المساق. اذهب إلى {dashboardLink} لشراء اعتماد المساق، أو اعرف المزيد عن {creditLink}.",
|
||||
"progress.creditInformation.creditPartialEligible": "لم تستوف بعد شروط الحصول على اعتماد. اعرف المزيد عن {creditLink}.",
|
||||
"progress.creditInformation.completed": "مُكتمل",
|
||||
"progress.creditInformation.courseCredit": "اعتماد المساق",
|
||||
"progress.creditInformation.minimumGrade": "الحد الأدنى لدرجة الاعتماد ({minGrade}٪)",
|
||||
"progress.creditInformation.requirementsHeader": "متطلبات الحصول على اعتماد للمساق",
|
||||
"progress.creditInformation.minimumGrade": "أدنى درجة مؤهلة للحصول على اعتماد ({minGrade}%)",
|
||||
"progress.creditInformation.requirementsHeader": "متطلبات الحصول على اعتماد المساق",
|
||||
"progress.creditInformation.upcoming": "قريبًا",
|
||||
"progress.creditInformation.verificationFailed": "فشل التحقق",
|
||||
"progress.creditInformation.verificationSubmitted": "تم تقديم التحقق",
|
||||
"progress.ungradedAlert": "للتقدمك في المحتوى الذي لم يتم تقديره من المساق ، قم بعرض {outlineLink}.",
|
||||
"progress.footnotes.droppableAssignments": "أدنى {numDroppable، plural, one {# {assignmentType} تجاهله} other {# {assignmentType} تجاهلهم} من النتائج.",
|
||||
"progress.creditInformation.verificationSubmitted": "تم إرسال طلب التحقق",
|
||||
"progress.ungradedAlert": "لمعرفة تقدمك في محتويات المساق غير المنقّطة، اطلع على {outlineLink}ك.",
|
||||
"progress.footnotes.droppableAssignments": "لا تٌحتسب أدنى {numDroppable, plural,\n one {درجة}\n two {درجتين}\n few {# درجات}\n many {# درجة}\n other {# درحة}\n} {assignmentType}.",
|
||||
"progress.assignmentType": "نوع الواجب",
|
||||
"progress.footnotes.backToContent": "العودة إلى المحتوى",
|
||||
"progress.courseGrade.body": "يمثل هذا مكان درجتك الحالية مقابل الدرجة المطلوبة لاجتياز هذه الدورة التدريبية.",
|
||||
"progress.courseGrade.gradeBar.altText": "درجتك الحالية هي {currentGrade}٪. مطلوب الحصول على درجة {passingGrade}٪ للنجاح في هذه الدورة التدريبية.",
|
||||
"progress.courseGrade.footer.generic.passing": "أنت ناجح حاليًا في هذه الدورة التدريبية",
|
||||
"progress.courseGrade.footer.nonPassing": "مطلوب درجة مرجحة تبلغ {passingGrade}٪ للنجاح في هذه الدورة",
|
||||
"progress.courseGrade.footer.passing": "أنت تنجح حاليًا في اجتياز هذه الدورة بتقدير {letterGrade} ({minGrade} - {maxGrade}٪)",
|
||||
"progress.courseGrade.body": "يمثل هذا درجتك الموزونة الحالية مقارنة بالدرجة المطلوبة للنجاح في هذا المساق.",
|
||||
"progress.courseGrade.gradeBar.altText": "درجتك الحالية هي {currentGrade}% مطلوب الحصول على درجة موزونة تبلغ {passingGrade}% للنجاح في هذا المساق.",
|
||||
"progress.courseGrade.footer.generic.passing": "أنت ناجح حاليًا في هذا المساق.",
|
||||
"progress.courseGrade.footer.nonPassing": "مطلوب الحصول على درجة موزونة تبلغ {passingGrade}% للنجاح في هذا المساق.",
|
||||
"progress.courseGrade.footer.passing": "أنت ناجح حاليًا في هذا المساق بتقدير {letterGrade} ({minGrade} - {maxGrade}%)",
|
||||
"progress.courseGrade.preview.headerLocked": "ميزة مقفلة",
|
||||
"progress.courseGrade.preview.headerLimited": "ميزة محدودة",
|
||||
"progress.courseGrade.preview.header.ariaHidden": "معاينة ال",
|
||||
"progress.courseGrade.preview.body.unlockCertificate": "قم بترقية تسجيلك لعرض الدرجات والعمل للحصول على الشهادة.",
|
||||
"progress.courseGrade.partialpreview.body.unlockCertificate": "قم بترقية تسجيلك للعمل من أجل الحصول على شهادة.",
|
||||
"progress.courseGrade.preview.body.upgradeDeadlinePassed": "انقضى الموعد النهائي للترقية في هذه الدورة.",
|
||||
"progress.courseGrade.preview.header.ariaHidden": "معاينة ",
|
||||
"progress.courseGrade.preview.body.unlockCertificate": "ارفع القيود لعرض الدرجات و العمل للحصول على شهادة.",
|
||||
"progress.courseGrade.partialpreview.body.unlockCertificate": "ارفع القيود من أجل العمل للحصول على شهادة.",
|
||||
"progress.courseGrade.preview.body.upgradeDeadlinePassed": "انقضى أجَل الترقية في هذا المساق",
|
||||
"progress.courseGrade.preview.button.upgrade": "الترقية الآن",
|
||||
"progress.courseGrade.gradeRange.tooltip": "نطاقات الدرجات لهذه الدورة:",
|
||||
"progress.courseOutline": "مخطّط المساق",
|
||||
"progress.courseGrade.gradeRange.tooltip": "مجالات الدرجات لهذا المساق:",
|
||||
"progress.courseOutline": "مخطّط مساق",
|
||||
"progress.courseGrade.label.currentGrade": "درجتك الحالية",
|
||||
"progress.detailedGrades": "الدرجات التفصيلية",
|
||||
"progress.detailedGrades.emptyTable": "ليس لديك أي مسائل بحاجة للتقييم.",
|
||||
"progress.footnotes.title": "ملاحظات لملخص الدرجات",
|
||||
"progress.detailedGrades.emptyTable": "ليست لديك حاليا أي درجات لمسائل منقّطة.",
|
||||
"progress.footnotes.title": "ملاحظات بخصوص ملخص الدرجات",
|
||||
"progress.gradeSummary.grade": "الدرجة",
|
||||
"progress.courseGrade.grades": "درجات",
|
||||
"progress.courseGrade.gradesAndCredit": "الدرجات والساعات المعتمدة",
|
||||
"progress.courseGrade.gradeRange.Tooltip": "عن نطاق الدرجات",
|
||||
"progress.gradeSummary": "ملخص الدرجة",
|
||||
"progress.gradeSummary.limitedAccessExplanation": "لديك وصول محدود إلى الواجبات المقدرة كجزء من تسجيلك ( كمستمع ) في هذه الدورة التدريبية.",
|
||||
"progress.gradeSummary.tooltip.alt": "عن ملخص الدرجات",
|
||||
"progress.gradeSummary.tooltip.body": "يتم تحديد وزن كل واجب من المقرر الدراسي الخاص, بك من قبل مدرسك. (حاصل الضرب ل تقيمك بواجب معين ب وزن الواجب نفسه) ينتج عن ذلك درجتك الموزنة. درجتك الموزنة هي ما يتم استخدامه لتحديد ما إذا كنت ستنجح في الدورة التدريبية.",
|
||||
"progress.noAcessToAssignmentType": "ليس لديك حق الوصول إلى الواجبات من النوع {assignmentType}",
|
||||
"progress.noAcessToSubsection": "ليس لديك حق الوصول إلى جزء {displayName}",
|
||||
"progress.courseGrade.grades": "الدرجات",
|
||||
"progress.courseGrade.gradesAndCredit": "الدرجات و الاعتماد",
|
||||
"progress.courseGrade.gradeRange.Tooltip": "تلميح عن مجال الدرجة",
|
||||
"progress.gradeSummary": "ملخص الدرجات",
|
||||
"progress.gradeSummary.limitedAccessExplanation": "وصولك للواجبات المنقّطة محدود، لكونك مسجلا صمن مسار الاستماع في هذا المساق.",
|
||||
"progress.gradeSummary.tooltip.alt": "تلميح عن ملخص الدرجات",
|
||||
"progress.gradeSummary.tooltip.body": "أوزان الواحبات في مساقك محددة من طرف الأستاذ. يتم حساب الدرجة الموزونة بضرب درجتك في وزن ذلك الواجب. درجتك الموزنة هي ما يُستخدم لتحديد ما إن كنت ناجحًا في المساق.",
|
||||
"progress.noAcessToAssignmentType": " لا يمكنك الوصول إلى الواجبات من نوع \"{assignmentType}\"",
|
||||
"progress.noAcessToSubsection": "لا يمكنك الوصول إلى القسم الفرعي \"{displayName}\"",
|
||||
"progress.courseGrade.label.passingGrade": "درجة النجاح",
|
||||
"progress.detailedGrades.problemScore.label": "درجات الواجبات:",
|
||||
"progress.detailedGrades.problemScore.toggleButton": "اظهار التقييم لكل مسألة ب {subsectionTitle}",
|
||||
"progress.detailedGrades.overridden": "تم تجاوز درجة القسم.",
|
||||
"progress.score": "النتيجة",
|
||||
"progress.weight": "القيمة",
|
||||
"progress.weightedGrade": "درجتك الموزنة",
|
||||
"progress.weightedGradeSummary": "ملخص درجتك الموزنة الحالية",
|
||||
"progress.detailedGrades.problemScore.label": "درجات المسائل:",
|
||||
"progress.detailedGrades.problemScore.toggleButton": "إظهار / إخفاء درجة كل مسألة في {subsectionTitle}",
|
||||
"progress.detailedGrades.overridden": "تم استبدال درجة هذا القسم.",
|
||||
"progress.score": "الدرجة",
|
||||
"progress.weight": "الوزن",
|
||||
"progress.weightedGrade": "الدرجة الموزونة",
|
||||
"progress.weightedGradeSummary": "ملخص الدرجات الموزونة الحالية",
|
||||
"progress.header": "تقدمك",
|
||||
"progress.header.targetUser": "التقدم في المساق لـ {username}",
|
||||
"progress.link.studio": "استعراض عملية التقييم في استوديو",
|
||||
"progress.relatedLinks.datesCard.description": "عرض الجدول الزمني لتواريخ المستحقة والتعيينات القادمة ل واجبات الدورة.",
|
||||
"progress.relatedLinks.datesCard.link": "تواريخ الاستحقاق",
|
||||
"progress.relatedLinks.outlineCard.description": "نظرة عامة لمحتوى دورتك.",
|
||||
"progress.header.targetUser": "تقدم {username} في المساق",
|
||||
"progress.link.studio": "استعراض التقييم في الاستوديو",
|
||||
"progress.relatedLinks.datesCard.description": "عرض لجدول زمني يتضمن الآجال و الواجبات القادمة لمساقك.",
|
||||
"progress.relatedLinks.datesCard.link": "التواريخ",
|
||||
"progress.relatedLinks.outlineCard.description": "نظرة عامة لمحتوى مساقك.",
|
||||
"progress.relatedLinks.outlineCard.link": "مخطّط المساق",
|
||||
"progress.relatedLinks": "روابط ذات صلة",
|
||||
"datesBanner.suggestedSchedule": "لقد وضعنا جدولاً زمنيًا مقترحًا لمساعدتك على وضع خطة تعليم. لكن لا تقلق - فهي خطة مرنة حتى تتمكن من التعلم بالسرعة التي تناسبك.",
|
||||
"datesBanner.upgradeToCompleteGradedBanner.header": "قم بالترقية لالغاء القفل.",
|
||||
"datesBanner.upgradeToCompleteGradedBanner.body": "أنت ملتحق كمستمع في هذه الدورة التدريبية ، مما يعني أنك غير قادر على المشاركة في الواجبات المقدرة. لإكمال الواجبات المقدرة كجزء من هذه الدورة التدريبية ، يمكنك الترقية اليوم.",
|
||||
"datesBanner.upgradeToCompleteGradedBanner.button": "ترقية الآن",
|
||||
"datesBanner.upgradeToResetBanner.body": "للبقاء على المسار الصحيح، يمكن تحديث هذا الجدول وتحويل المهام السابقة المستحقة إلى مهام تاريخ استحقاقها في المستقبل. لا داعي للقلق - لن تفقد أي تقدم أحرزته عند تغيير تواريخ الاستحقاق.",
|
||||
"datesBanner.upgradeToResetBanner.button": "قم بالترقية لتغيير التواريخ المستحقة",
|
||||
"datesBanner.resetDatesBanner.header": "يبدو أنه قد فاتتك بعض المواعيد النهائية المهمة بناءً على جدولنا الزمني المقترح.",
|
||||
"datesBanner.resetDatesBanner.body": "للبقاء على المسار الصحيح، يمكن تحديث هذا الجدول وتحويل المهام السابقة المستحقة إلى المستقبل. لا داعي للقلق - لن تفقد أي تقدم أحرزته عند تغيير تواريخ الاستحقاق.",
|
||||
"datesBanner.resetDatesBanner.button": "تغيير التواريخ المستحقة",
|
||||
"datesBanner.suggestedSchedule": "لقد بنينا جدولا زمنيا مقترحًا لمساعدتك في البقاء على السكة. لكن لا تقلق، فهو مرن و يمكّنك من التعلّم وفق وتيرتك الخاصة.",
|
||||
"datesBanner.upgradeToCompleteGradedBanner.header": "قم بالترقية لرفع القيود",
|
||||
"datesBanner.upgradeToCompleteGradedBanner.body": "أنت مستمع في هذا المساق، ما يعني أنك غير قادر على المشاركة في الواجبات المنقّطة. لإكمال الواجبات المنقّطة كجزء من هذا المساق، يمكنك الترقية اليوم.",
|
||||
"datesBanner.upgradeToCompleteGradedBanner.button": "الترقية الآن",
|
||||
"datesBanner.upgradeToResetBanner.body": "للبقاء على السكة، يمكنك تحديث هذا الجدول و تغيير آجال الواجبات المنقضية إلى آجال مستقبلية. لا داعي للقلق، لن تفقد أي تقدم أحرزته عند تغيير الآجال.",
|
||||
"datesBanner.upgradeToResetBanner.button": "الترقية لتغيير الآجال",
|
||||
"datesBanner.resetDatesBanner.header": "يبدو أنك قد تخطيت بعض الآجال المهمة حسب جدولنا الزمني المقترح.",
|
||||
"datesBanner.resetDatesBanner.body": "للبقاء على السكة، يمكنك تحديث هذا الجدول و تغيير آجال الواجبات المنقضية إلى آجال مستقبلية. لا داعي للقلق، لن تفقد أي تقدم أحرزته عند تغيير الآجال.",
|
||||
"datesBanner.resetDatesBanner.button": "تغيير الآجال",
|
||||
"learn.navigation.course.tabs.label": "مواد المساق",
|
||||
"unit.bookmark.button.add.bookmark": "إضافة الصفحة للمفضلة",
|
||||
"unit.bookmark.button.remove.bookmark": "تم وضع علامة مرجعية",
|
||||
"unit.bookmark.button.add.bookmark": "التأشير على هذه الصفحة",
|
||||
"unit.bookmark.button.remove.bookmark": "تم التأشير على الصفحة",
|
||||
"learning.celebration.completed": "لقد أكملت للتو القسم الأول من مساقك.",
|
||||
"learning.celebration.congrats": "تهانينا!",
|
||||
"learning.celebration.earned": "لقد استحققته.",
|
||||
"learning.celebration.earned": "لقد حققته!",
|
||||
"learning.celebration.emailSubject": "أنا في طريقي لإتمام {title} عن بعد في {platform}!",
|
||||
"learning.celebration.forward": "استمر ",
|
||||
"learning.celebration.forward": "واصل",
|
||||
"learning.celebration.goalMet": "لقد حققت هدفك!",
|
||||
"learning.celebration.keepItUp": "أبقه على ما انت عليه",
|
||||
"learning.celebration.share": "لحظة من فضلك للاحتفاء بك ومشاركتك فرحة تقدمك",
|
||||
"learning.celebration.social": "أنا في طريقي لإتمام {title} عن بعد في {platform}. ما الذي تقضي الوقت في تعلمه؟",
|
||||
"learning.celebration.goalCongrats": "تهانينا ، لقد حققت هدفك التعليمي وهو {nTimes} في الأسبوع.",
|
||||
"learning.celebration.setGoal": "يمكن أن يساعدك تحديد هدف {strongText} في مقررك الدراسي.",
|
||||
"calculator.instructions.button.label": "إرشادات الحاسبة",
|
||||
"calculator.instructions": "لمزيد من التفاصيل، تفضل بالاطلاع على {expressions_link}.",
|
||||
"learning.celebration.keepItUp": "واصل على هذا المنوال",
|
||||
"learning.celebration.share": "خذ لحظة للاحتفاء بتقدّمك و مشاركته.",
|
||||
"learning.celebration.social": "أنا في طريقي لإتمام مساق \"{title}\" عن بعد مع {platform}. ما الذي تقضي الوقت في تعلمه؟",
|
||||
"learning.celebration.goalCongrats": "تهانينا، فقد حققت هدفك التعلّمي و هو {nTimes} أسبوعيا.",
|
||||
"learning.celebration.setGoal": "إن تحديد هدف {strongText} قد يساعدك في مساقك.",
|
||||
"calculator.instructions.button.label": "إرشادات الآلة الحاسبة",
|
||||
"calculator.instructions": "لمزيد من المعلومات، اطّلع على {expressions_link}.",
|
||||
"calculator.instructions.support.title": "مركز المساعدة",
|
||||
"calculator.instructions.useful.tips": "تلميحات مفيدة:",
|
||||
"calculator.hint1": "يُرجى استخدام القوسين () لإيضاح التعابير. ويمكنك استخدام قوسين داخل قوسين آخرين. ",
|
||||
"calculator.instructions.useful.tips": "نصائح مفيدة:",
|
||||
"calculator.hint1": "استخدم القوسين () لجعل التعابير واضحة. يمكنك استخدام الأقواس بعضها داخل بعض. ",
|
||||
"calculator.hint2": "لا تستخدم مسافات في التعابير.",
|
||||
"calculator.hint3": "بالنسبة للثوابت، حدِّد عملية الضرب بوضوح (مثلًا: 5*c). ",
|
||||
"calculator.hint4": "بالنسبة لللاحقات، أَدخِل الرقم والملحق من دون مسافة بينهما (مثلًا: 5c). ",
|
||||
"calculator.hint5": "بالنسبة للوظائف، أدخِل اسم الوظيفة، يتبعه التعبير بين قوسين. ",
|
||||
"calculator.instruction.table.to.use.heading": "للاستخدام",
|
||||
"calculator.hint3": "بالنسبة للثوابت، أبرز عملية الضرب بوضوح (مثلًا: 5*c).",
|
||||
"calculator.hint4": "بالنسبة للواحق، اكتب الرقم فاللاحقة دون مسافة (مثلًا: 5c).",
|
||||
"calculator.hint5": "بالنسبة للدوال، اكتب اسم الدالة، ثم التعبير بين قوسين.",
|
||||
"calculator.instruction.table.to.use.heading": "لاستخدام",
|
||||
"calculator.instruction.table.type.heading": "النوع",
|
||||
"calculator.instruction.table.examples.heading": "أمثلة",
|
||||
"calculator.instruction.table.to.use.numbers": "أرقام",
|
||||
"calculator.instruction.table.to.use.numbers.type1": "أعداد صحيحة",
|
||||
"calculator.instruction.table.to.use.numbers": "الأرقام",
|
||||
"calculator.instruction.table.to.use.numbers.type1": "أعداد الصحيحة",
|
||||
"calculator.instruction.table.to.use.numbers.type2": "كسور",
|
||||
"calculator.instruction.table.to.use.numbers.type3": "أعداد عشرية",
|
||||
"calculator.instruction.table.to.use.operators": "المعامِلات الحسابية",
|
||||
"calculator.instruction.table.to.use.operators.type1": "(زائد، ناقص، ضرب، قسمة)",
|
||||
"calculator.instruction.table.to.use.numbers.type3": "أعداد العشرية",
|
||||
"calculator.instruction.table.to.use.operators": "العمليات الحسابية",
|
||||
"calculator.instruction.table.to.use.operators.type1": "(جمع، طرح، ضرب و قسمة)",
|
||||
"calculator.instruction.table.to.use.operators.type2": "(الرفع إلى قوة)",
|
||||
"calculator.instruction.table.to.use.operators.type3": "(مقاومات متوازية)",
|
||||
"calculator.instruction.table.to.use.constants": "الثوابت ",
|
||||
"calculator.instruction.table.to.use.affixes": "اللاحقات",
|
||||
"calculator.instruction.table.to.use.affixes.type": "علامة النسبة المئوية (٪)",
|
||||
"calculator.instruction.table.to.use.basic.functions": "الاقترانات الأساسية",
|
||||
"calculator.instruction.table.to.use.trig.functions": "الاقترانات المثلثية",
|
||||
"calculator.instruction.table.to.use.affixes": "اللواحق",
|
||||
"calculator.instruction.table.to.use.affixes.type": "علامة النسبة المئوية (%)",
|
||||
"calculator.instruction.table.to.use.basic.functions": "الدوال الأساسية",
|
||||
"calculator.instruction.table.to.use.trig.functions": "الدوال المثلثية",
|
||||
"calculator.instruction.table.to.use.scientific.notation": "الكتابة العلميّة للأعداد",
|
||||
"calculator.instruction.table.to.use.scientific.notation.type1": "{exponentSyntax} والأس",
|
||||
"calculator.instruction.table.to.use.scientific.notation.type2": "{notationSyntax} الترميز",
|
||||
"calculator.instruction.table.to.use.scientific.notation.type3": "{notationSyntax}والأس",
|
||||
"calculator.instruction.table.to.use.scientific.notation.type1": "{exponentSyntax} و الأس",
|
||||
"calculator.instruction.table.to.use.scientific.notation.type2": "كتابة {notationSyntax}",
|
||||
"calculator.instruction.table.to.use.scientific.notation.type3": "{notationSyntax} و الأس",
|
||||
"calculator.button.label": "الآلة الحاسبة ",
|
||||
"calculator.input.field.label": "الإدخال في الآلة الحاسبة",
|
||||
"calculator.submit.button.label": "احسب",
|
||||
"calculator.result.field.label": "نتيجة الحاسبة",
|
||||
"calculator.input.field.label": "أدخل في الآلة الحاسبة",
|
||||
"calculator.submit.button.label": "حساب",
|
||||
"calculator.result.field.label": "نتيجة الآلة الحاسبة",
|
||||
"calculator.result.field.placeholder": "النتيجة",
|
||||
"notes.button.show": "إظهار الملاحظات",
|
||||
"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}%!",
|
||||
"courseCelebration.recommendations.heading": "استمر في بناء مهاراتك مع هذه الدورات!",
|
||||
"courseExit.catalogSearchSuggestion": "هل انت متطلع للتعلّم أكثر؟ {searchOurCatalogLink} عن مزيد من المساقات والبرامج.",
|
||||
"courseCelebration.certificateBody.available": "اعرض إنجازاتك على لينكد إن أو سيرتك الذاتية اليوم.\nيمكنك تنزيل الشهادة الآن والوصول إليها في أي وقت من\n{dashboardLink} و {profileLink}.",
|
||||
"courseCelebration.certificateBody.notAvailable.endDate.v2": "ينتهي هذا المساق في {endDate}. تمت جدولة ظهور الدرجات النهائية و أي شهادات مكتسبة ابتداءً من {certAvailableDate}.",
|
||||
"courseCelebration.certificateBody.unverified": "لتوليد شهادة، يجب عليك إتمام التحقق من الهوية.\n{idVerificationSupportLink} الآن.",
|
||||
"courseCelebration.certificateBody.upgradable": "لم يفت الأوان بعد للترقية. مقابل {price}، سترفع القيود عن كل الواجبات المنقّطة في هذا المساق. عند الانتهاء، ستحصل على شهادة معتمدة، وهي من الوثائق القيّمة التي تنفع لتوسيع آفاقك في التوظيف و تقدمك في حياتك المهنية، أو لإبراز شهادتك في طلبات الالتحاق بالمدارس.",
|
||||
"courseCelebration.upgradeDiscountCodePrompt": "استخدم الرمز {code} عند الشراء للاستفادة من خصم {percent}%!",
|
||||
"courseCelebration.recommendations.heading": "واصل بناء مهاراتك مع هذه المساقات!",
|
||||
"courseCelebration.recommendations.label": "المساق",
|
||||
"courseCelebration.recommendations.formatting.list_join": "{style, select, punctuation {, } conjunction { {sp}and } other { }}",
|
||||
"courseCelebration.recommendations.browse_catalog": "اكتشف المزيد من الدورات",
|
||||
"courseCelebration.recommendations.loading_recommendations": "جاري تحميل التوصيات",
|
||||
"courseCelebration.recommendations.card.schools.label": "المؤسسات التعليمية والشركاء",
|
||||
"courseCelebration.dashboardInfo": "يمكنك الوصول إلى هذا المساق ومواده على لوحة معلوماتك {dashboardLink}.",
|
||||
"courseExit.programs.applyForCredit": "تقدم بطلب للحصول على ائتمان",
|
||||
"courseCelebration.recommendations.formatting.list_join": "{style, select, punctuation {، } conjunction { {sp}و } other { }}",
|
||||
"courseCelebration.recommendations.browse_catalog": "اكتشف المزيد من المساقات",
|
||||
"courseCelebration.recommendations.loading_recommendations": "تحميل المقترحات جارٍ",
|
||||
"courseCelebration.recommendations.card.schools.label": "المدارس و الشركاء",
|
||||
"courseCelebration.dashboardInfo": "يمكنك الوصول إلى هذا المساق و مواده من {dashboardLink}.",
|
||||
"courseExit.programs.applyForCredit": "تقدّم بطلب للحصول على اعتماد",
|
||||
"courseCelebration.certificateHeader.downloadable": "!شهادتك جاهزة",
|
||||
"courseCelebration.certificateHeader.notAvailable": "Your grade and certificate status will be available soon.",
|
||||
"courseCelebration.certificateBody.notAvailable.accessCertificate": "إذا كنت قد حصلت على درجة النجاح ، فسيتم إصدار شهادتك تلقائيًا.",
|
||||
"courseCelebration.certificateHeader.notAvailable": "ستتوفر حالة درجتك وشهادتك قريبًا.",
|
||||
"courseCelebration.certificateBody.notAvailable.accessCertificate": "إن كنت قد حصلت على درجة نجاح، فإن شهادتك ستصدر تلقائيا.",
|
||||
"courseCelebration.certificateHeader.unverified": "يجب إكمال عملية التحقق للحصول على شهادتك",
|
||||
"courseCelebration.certificateHeader.requestable": "تهانينا، لقد تأهلت للحصول على شهادة!",
|
||||
"courseCelebration.certificateHeader.upgradable": "قم بالترقية للحصول على شهادة معتمدة",
|
||||
"courseCelebration.certificateHeader.upgradable": "الترقية الآن للحصول على شهادة معتمدة",
|
||||
"courseCelebration.certificateImage": "عينة الشهادة",
|
||||
"courseCelebration.completedCourseHeader": "لقد أكملت دورتك.",
|
||||
"courseCelebration.completedCourseHeader": "لقد أكملت مساقك.",
|
||||
"courseCelebration.congratulationsHeader": "تهانينا!",
|
||||
"courseCelebration.congratulationsImage": "يرفع أربعة أشخاص أيديهم احتفاءً",
|
||||
"courseExit.courseInProgressDescription": "يبدو أن هناك المزيد من المحتوى في هذه الدورة والذي سيتم إصداره في المستقبل. ابقى على اطلاع بتحديثات البريد الإلكتروني أو تحقق مرة أخرى من الدورة التدريبية الخاصة بك لمعرفة وقت توفر هذا المحتوى.",
|
||||
"courseExit.courseInProgressHeader": "المزيد من المحتوى قريبا!",
|
||||
"courseExit.courseInProgressDescription": "يبدو أن في هذا المساق مزيدا من المحتوى سيصدر مستقبلا. ستقبل. ترقب التحديثات عبر البريد الإلكتروني أو تفقّد هذا المساق لاحقًا لمعرفة موعد صدور هذا المحتوى.",
|
||||
"courseExit.courseInProgressHeader": "مزيد من المحتوى قادم قريبا!",
|
||||
"courseExit.dashboardLink": "لوحة المعلومات",
|
||||
"courseCelebration.downloadButton": "تحميل شهاداتي",
|
||||
"courseExit.endOfCourseDescription": "لسوء الحظ، لست مؤهلًا الآن للحصول على شهادة. تحتاج إلى تحقيق درجة الاجتياز تؤهلك للحصول على شهادة.",
|
||||
"courseExit.endOfCourseDescription": "مع الأسف، لست مؤهلًا حاليا للحصول على شهادة. أنت بحاجة لتحصيل درجة نجاح كي تتأهل للحصول على شهادة.",
|
||||
"courseExit.endOfCourseHeader": "لقد أتممت المساق!",
|
||||
"courseExit.endOfCourseTitle": "نهاية المساق",
|
||||
"courseExit.idVerificationSupportLink": "اعرف المزيد عن عملية التحقق من الهوية",
|
||||
"courseCelebration.linkedinAddToProfileButton": "إضافة إلى ملف شخصي على لينكد إن",
|
||||
"courseExit.programs.microBachelors.learnMore": "تعرف على المزيد حول كيفية تقديم بيانات اعتماد البكالوريوس المصغر للحصول على ائتمان.",
|
||||
"courseExit.programs.microMasters.learnMore": "تعرف على المزيد حول عملية تطبيق شهادات MicroMasters على درجة الماجستير.",
|
||||
"courseExit.programs.microMasters.mastersMessage": "إذا كنت مهتماً باستخدام شهادة MicroMasters الخاصة بك في برنامج Master، فيمكنك البدء اليوم!",
|
||||
"courseExit.programs.microBachelors.learnMore": "اعرف المزيد عن كيفية طلب اعتماد مقابل مؤهل MicroBachelors الذي حصلت عليه.",
|
||||
"courseExit.programs.microMasters.learnMore": "اعرف المزيد عن عملية تطبيق شهادات MicroMasters على شهادة الماستر / الماجستير.",
|
||||
"courseExit.programs.microMasters.mastersMessage": "إن كنت مهتماً باستخدام شهادة MicroMasters التي حصلت عليها للتسجيل في برنامج Master، فيمكنك البدء في ذلك اليوم!",
|
||||
"learn.sequence.navigation.complete.button": "أكمل المساق",
|
||||
"courseExit.nextButton.endOfCourse": "التالي (نهاية المساق)",
|
||||
"courseExit.profileLink": "الملف الشخصي",
|
||||
"courseExit.programs.lastCourse": "لقد أتممت المساق الأخير في {title}!",
|
||||
"courseCelebration.requestCertificateBodyText": "لتصفح شهادتك ارفع الطلب أدناه.",
|
||||
"courseCelebration.requestCertificateButton": "طلب شهادة",
|
||||
"courseExit.searchOurCatalogLink": "ابحث في الدليل",
|
||||
"courseCelebration.requestCertificateBodyText": "للوصول إلى شهادتك، اطلبها أدناه.",
|
||||
"courseCelebration.requestCertificateButton": "طلب الشهادة",
|
||||
"courseExit.searchOurCatalogLink": "ابحث في دليلنا",
|
||||
"courseCelebration.shareMessage": "شارك نجاحك على وسائل التواصل الاجتماعي أو البريد الإلكتروني.",
|
||||
"courseExit.social.shareCompletionMessage": "أتممت للتو {title} في {platform}!",
|
||||
"courseExit.upgradeButton": "ترقية الآن",
|
||||
"courseExit.social.shareCompletionMessage": "لقد أتممت للتو \"{title}\" مع {platform}!",
|
||||
"courseExit.upgradeButton": "الترقية الآن",
|
||||
"courseExit.upgradeLink": "الترقية الآن",
|
||||
"courseCelebration.verificationPending": "عملية التحقق من المعرّف الخاص بك معلّقة وستتوفر شهادتك بمجرد الموافقة.",
|
||||
"courseExit.verifiedCertificateSupportLink": "تعرف أكثر على الشهادات المعتمدة",
|
||||
"courseCelebration.verifyIdentityButton": "تحقق من الهوية الآن",
|
||||
"courseCelebration.verificationPending": "عملية التحقق من هويتك في انتظار المراجعة وستتوفر شهادتك بمجرد الموافقة.",
|
||||
"courseExit.verifiedCertificateSupportLink": "معرفة المزيد عن الشهادات المعتمدة",
|
||||
"courseCelebration.verifyIdentityButton": "التحقق من الهوية الآن",
|
||||
"courseCelebration.viewCertificateButton": "عرض شهاداتي",
|
||||
"courseExit.viewCourseScheduleButton": "عرض الجدول الزمني للدورة",
|
||||
"courseExit.viewCoursesButton": "عرض مساقاتي الملتحق بها",
|
||||
"courseExit.viewCourseScheduleButton": "عرض الجدول الزمني للمساق",
|
||||
"courseExit.viewCoursesButton": "عرض مساقاتي",
|
||||
"courseExit.viewGradesButton": "عرض الدرجات",
|
||||
"courseExit.programCompletion.dashboardMessage": "لعرض حالة الشهادة، تحقق من قسم البرامج في {programLink}.",
|
||||
"courseExit.upgradeFootnote": "يتوفر الوصول إلى هذا المساق ومواده على لوحة المعلومات حتى {expirationDate}. لزيادة فترة إتاحة الوصول، {ترقيتها Link}.",
|
||||
"courseExit.upgradeFootnote": "سيبقى الوصول إلى هذا المساق و مواده على لوحة المعلومات ممكنا إلى غاية {expirationDate}. لتمديد فترة الوصول، قم ب{upgradeLink}.",
|
||||
"learn.course.license.allRightsReserved.text": "جميع الحقوق محفوظة",
|
||||
"learn.course.license.creativeCommons.terms.preamble": "محتوى مرخّص كمشاع إبداعي وفق الشروط التالية:",
|
||||
"learn.course.license.creativeCommons.terms.by": "الإسناد",
|
||||
"learn.course.license.creativeCommons.terms.nc": "استخدام غير تجاري",
|
||||
"learn.course.license.creativeCommons.terms.nd": "عدم الاقتباس",
|
||||
"learn.course.license.creativeCommons.terms.sa": "مشاركة بالتساوي",
|
||||
"learn.course.license.creativeCommons.terms.zero": "لا توجد عناصر",
|
||||
"learn.course.license.creativeCommons.terms.by": "الإسناد (Attribution)",
|
||||
"learn.course.license.creativeCommons.terms.nc": "غير تجاري (Non-Commercial)",
|
||||
"learn.course.license.creativeCommons.terms.nd": "دون اشتقاقات (No Derivatives)",
|
||||
"learn.course.license.creativeCommons.terms.sa": " المشاركة بالمثل (Share Alike)",
|
||||
"learn.course.license.creativeCommons.terms.zero": "دون شروط",
|
||||
"learn.course.license.creativeCommons.text": "بعض الحقوق محفوظة",
|
||||
"learn.breadcrumb.navigation.course.home": "المساق ",
|
||||
"notification.tray.container": "Notification tray",
|
||||
"notification.open.button": "Show notification tray",
|
||||
"notification.close.button": "Close notification tray",
|
||||
"responsive.close.notification": "العودة إلى الدورة",
|
||||
"notification.tray.title": "إشعارات",
|
||||
"notification.tray.no.message": "ليس لديك إشعارات جديدة في هذا الوقت.",
|
||||
"learn.contentLock.content.locked": "محتوى مغلق",
|
||||
"learn.contentLock.complete.prerequisite": "يجب استيفاء المتطلبات الأساسية: '{priceqSectionName}' للوصول إلى هذا المحتوى.",
|
||||
"learn.contentLock.goToSection": "انتقل إلى قسم المتطلبات الأساسية",
|
||||
"learn.hiddenAfterDue.gradeAvailable": "إذا كنت قد أكملت هذا الواجب ، فسيكون تقديرك متاحًا على {progressPage}.",
|
||||
"learn.hiddenAfterDue.header": "انقضى الموعد النهائي لهذا الواجب.",
|
||||
"learn.hiddenAfterDue.description": "Because the due date has passed, this assignment is no longer available.",
|
||||
"learn.hiddenAfterDue.progressPage": "صفحة تقدمك",
|
||||
"learn.honorCode.content": "Honesty and academic integrity are important to {siteName} and the institutions providing courses and programs on the {siteName} site. By clicking “I agree” below, I confirm that I have read, understand, and will abide by the {link} for the {siteName} Site.",
|
||||
"learn.honorCode.name": "ميثاق الشرف الأكاديمي",
|
||||
"notification.tray.container": "دُرج الإشعارات",
|
||||
"notification.open.button": "إظهار دٌرج الإشعارات",
|
||||
"notification.close.button": "إغلاق دٌرج الإشعارات",
|
||||
"responsive.close.notification": "العودة إلى المساق",
|
||||
"notification.tray.title": "الإشعارات",
|
||||
"notification.tray.no.message": "ليست لديك إشعارات جديدة في الوقت الراهن.",
|
||||
"learn.contentLock.content.locked": "المحتوى مقفل",
|
||||
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: ''{prereqSectionName}'' to access this content.",
|
||||
"learn.contentLock.goToSection": "انتقل إلى القسم المتطلّب.",
|
||||
"learn.hiddenAfterDue.gradeAvailable": "إن كنت قد أكملت هذا الواجب، فإن درجتك ستظهر في {progressPage}.",
|
||||
"learn.hiddenAfterDue.header": "انقضى أجَل هذا الواجب.",
|
||||
"learn.hiddenAfterDue.description": "نظرًا لانقضاء الأجَل، فإن هذا الواجب لم يعد متاحًا.",
|
||||
"learn.hiddenAfterDue.progressPage": "صفحة التقدم",
|
||||
"learn.honorCode.content": "الصدق و النزاهة الأكاديمية أمران مهمان بالنسبة إلى {siteName} و المؤسسات التي تقدم المساقات و البرامج على موقع {siteName}. بالنقر على “أنا موافق“ أدناه، أؤكد أني قد قرأت و فهمت و سألتزم ب{link} في موقع {siteName}.",
|
||||
"learn.honorCode.name": "ميثاق الشرف",
|
||||
"learn.honorCode.cancel": "إلغاء",
|
||||
"learn.honorCode.agree": "أنا موافق",
|
||||
"learn.lockPaywall.title": "الواجبات المقدرة غير متاحة لك",
|
||||
"learn.lockPaywall.content": "قم بالترقية للوصول إلى الميزات المقفلة مثل هذه الميزة وتحقيق أقصى استفادة من الدورة التدريبية الخاصة بك.",
|
||||
"learn.lockPaywall.content.pastExpiration": "انقضى الموعد النهائي للترقية لهذه الدورة. للترقية ، قم بالتسجيل في الجلسة التالية المتاحة.",
|
||||
"learn.lockPaywall.courseDetails": "عرض تفاصيل الدورة",
|
||||
"learn.lockPaywall.title": "الواجبات المنقّطة مقفلة",
|
||||
"learn.lockPaywall.content": "قم بالترقية للوصول إلى الميزات المقفلة كهذه الميزة و تحقيق أقصى استفادة من مساقك.",
|
||||
"learn.lockPaywall.content.pastExpiration": "لقد انقضى أجَل الترقية لهذا المساق.، سجل نفسك في الدورة التالية المتاحة.",
|
||||
"learn.lockPaywall.courseDetails": "عرض تفاصيل المساق",
|
||||
"learn.lockPaywall.example.alt": "عينة الشهادة",
|
||||
"learn.lockPaywall.list.intro": "عندما تقوم بالترقية ، فإنك:",
|
||||
"learn.header.h2.placeholder": "Level 2 headings may be created by course providers in the future.",
|
||||
"learn.lockPaywall.list.intro": "عندما تقوم بالترقية، فإنك:",
|
||||
"learn.header.h2.placeholder": "قد يتم إنشاء عناوين المستوى 2 من طرف مزودي المساق مستقبلا.",
|
||||
"learn.course.load.failure": "حدث خطأ أثناء تحميل هذا المساق.",
|
||||
"learn.loading.honor.codk": "جارٍ تحميل ميثاق الشرف ...",
|
||||
"learn.loading.content.lock": "جارٍ تحميل رسائل المحتوى المغلق...",
|
||||
"learn.loading.learning.sequence": "جارِ تحميل سلسلة التعلّم...",
|
||||
"learn.loading.honor.codk": "جارٍ تحميل نص ميثاق الشرف...",
|
||||
"learn.loading.content.lock": "جارٍ تحميل رسالة المحتوى المغلق...",
|
||||
"learn.loading.learning.sequence": "جارٍ تحميل السلسلة التعليمية...",
|
||||
"learn.sequence.no.content": "لا يوجد محتوى هنا.",
|
||||
"learn.sequence.navigation.next.button": "التالي",
|
||||
"learn.sequence.navigation.next.up.button": "التالي: {title}",
|
||||
"learn.sequence.navigation.previous.button": "السابق",
|
||||
"learn.course.sequence.navigation.mobile.menu": "{current} من أصل {total}",
|
||||
"discussions.sidebar.title": "نقاشات",
|
||||
"discussions.sidebar.open.button": "Show discussions tray",
|
||||
"learn.redirect.interstitial.message": "إعادة توجيه...",
|
||||
"learn.course.sequence.navigation.mobile.menu": "{current} من {total}",
|
||||
"discussions.sidebar.title": "المناقشات",
|
||||
"discussions.sidebar.open.button": "إظهار دٌرج المناقشات",
|
||||
"learn.redirect.interstitial.message": "إعادة التوجيه جارية...",
|
||||
"learn.loading.error": "خطأ: {error}",
|
||||
"learning.celebration.emailBody": "ما الذي تقضي وقتك في تعلّمه؟",
|
||||
"learning.social.shareEmail": "شارك تقدمك عبر البريد الإلكتروني.",
|
||||
"learning.social.shareService": "شارك تقدمك على {service}.",
|
||||
"general.altText.close": "إغلاق ",
|
||||
"learning.logistration.register": "تسجيل",
|
||||
"learning.logistration.login": "تسجيل الدخول",
|
||||
"learning.logistration.register": "افتح حسابًا جديدًا",
|
||||
"learning.logistration.login": "سجل دخولك",
|
||||
"general.signIn.sentenceCase": "تسجيل الدخول",
|
||||
"learn.course.tabs.navigation.overflow.menu": "المزيد...",
|
||||
"learning.offer.screenReaderPrices": "السعر الأصلي: {originalPrice}, سعر الخصم: {discountedPrice}",
|
||||
"learning.offer.screenReaderPrices": "السعر الأصلي: {originalPrice}، السعر المخفَّض: {discountedPrice}",
|
||||
"learning.upgradeButton.screenReaderInlinePrices": "السعر الأصلي: {originalPrice}",
|
||||
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||
"learning.upgradeNowButton.buttonText": "Upgrade now for {pricing}",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "including any progress",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "benefits of upgrading",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss": "ستفقد كل إمكانية الوصول إلى هذه الدورة التدريبية ، {includingAnyProgress} ، في {date}.",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert": "Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}.",
|
||||
"learning.generic.upgradeNotification.pastExpiration.content": "The upgrade deadline for this course passed. To upgrade, enroll in the next available session.",
|
||||
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, \n one {day}\n other {days}} left",
|
||||
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} left",
|
||||
"learning.generic.upgradeNotification.expirationMinutes": "أقل من 1 ساعة متبقية",
|
||||
"learning.generic.upgradeNotification.expiration": "ستنتهي صلاحية الوصول إلى الدورة التدريبية في {date}",
|
||||
"learning.generic.upgradeNotification.pastExpiration.banner": "Upgrade deadline passed on {date}",
|
||||
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% First-Time Learner Discount",
|
||||
"learning.generic.upgradeNotification.accessExpiration": "قم بترقية التحاقك بالدورة اليوم",
|
||||
"learning.generic.upgradeNotification.accessExpirationUrgent": "Course Access Expiration",
|
||||
"learning.generic.upgradeNotification.accessExpirationPast": "Course Access Expiration",
|
||||
"learning.upgradeButton.buttonText": "الترقية مقابل {pricing}",
|
||||
"learning.upgradeNowButton.buttonText": "الترقية الآن مقابل {pricing}",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "بما في ذلك أي تقدم",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "فوائد الترقية",
|
||||
"learning.generic.upgradeNotification.expirationAccessLoss": "ستفقد كل إمكانية الوصول إلى هذا المساق، {includingAnyProgress}، في {date}.",
|
||||
"learning.generic.upgradeNotification.expirationVerifiedCert": "ترقية مساقك تتيح لك السعي للحصول شهادة معتمدة، و ترفع القيود عن العديد من الميزات. اعرف المزيد عن {benefitsOfUpgrading}.",
|
||||
"learning.generic.upgradeNotification.pastExpiration.content": "لقد انقضى أجَل الترقية لهذا المساق. للترقية، سجل نفسك في الدورة التالية المتاحة.",
|
||||
"learning.generic.upgradeNotification.expirationDays": "{dayCount, plural, \n one {بقي يوم واحد}\n two {بقي يومان}\n few {بقيت # أيام}\n many {بقي # يومًا}\n other {بقي # يوم}\n}",
|
||||
"learning.generic.upgradeNotification.expirationHours": "بقيت {hourCount, plural, \n one {ساعة و احدة}\n two {ساعتان}\n few {# ساعات}\n many {# ساعة}\n}",
|
||||
"learning.generic.upgradeNotification.expirationMinutes": "بقيت أقل من ساعة",
|
||||
"learning.generic.upgradeNotification.expiration": "ستنتهي صلاحية الوصول إلى المساق في {date}",
|
||||
"learning.generic.upgradeNotification.pastExpiration.banner": "انقضى أجَل الترقية في {date}",
|
||||
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "خصم {percentage}% للمتعلم أول مرة",
|
||||
"learning.generic.upgradeNotification.accessExpiration": "قم بترقية مساقك اليوم",
|
||||
"learning.generic.upgradeNotification.accessExpirationUrgent": " نهاية صلاحية الوصول إلى المساق",
|
||||
"learning.generic.upgradeNotification.accessExpirationPast": "نهاية صلاحية الوصول إلى المساق",
|
||||
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "اسعَ للحصول على شهادة معتمدة",
|
||||
"learning.generic.upgradeNotification.code": "استخدم الكود {code} عند الدفع",
|
||||
"learning.generic.upsell.verifiedCertBullet.verifiedCert": "verified certificate",
|
||||
"learning.generic.upsell.verifiedCertBullet": "Earn a {verifiedCertLink} of completion to showcase on your resumé",
|
||||
"learning.generic.upsell.unlockGradedBullet.gradedAssignments": "graded assignments",
|
||||
"learning.generic.upsell.unlockGradedBullet": "Unlock your access to all course activities, including {gradedAssignmentsInBoldText}",
|
||||
"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",
|
||||
"masquerade-widget.userName.error.generic": "حدث خطأ؛ يرجى المحاولة مرة أخرى.",
|
||||
"learning.generic.upgradeNotification.code": "استخدم الكود {code} عند الشراء",
|
||||
"learning.generic.upsell.verifiedCertBullet.verifiedCert": "شهادة معتمدة",
|
||||
"learning.generic.upsell.verifiedCertBullet": "تحصل على {verifiedCertLink} لإتمام المساق لتعرضها في سيرتك الذاتية.",
|
||||
"learning.generic.upsell.unlockGradedBullet.gradedAssignments": "الواجبات المنقّطة",
|
||||
"learning.generic.upsell.unlockGradedBullet": "ترفع قيودك عن جميع أنشطة المساق، بما في ذلك {gradedAssignmentsInBoldText}",
|
||||
"learning.generic.upsell.fullAccessBullet.fullAccess": "تستفيد من كل",
|
||||
"learning.generic.upsell.fullAccessBullet": "{fullAccessInBoldText} محتوى و مواد المساق، حتى بعد انتهائه",
|
||||
"learning.generic.upsell.supportMissionBullet.mission": "رسالتنا",
|
||||
"learning.generic.upsell.supportMissionBullet": "تدعم {missionInBoldText} في {siteName}",
|
||||
"masquerade-widget.userName.error.generic": "حدث خطأ ما. رجاءً حاول مجددًا",
|
||||
"masquerade-widget.userName.input.placeholder": "اسم المستخدم أو البريد الإلكتروني",
|
||||
"masquerade-widget.userName.input.label": "عرف كهذا المستخدم",
|
||||
"tours.abandonTour.launchTourCheckpoint.body": "Feeling lost? Launch the tour any time for some quick tips to get the most out of the experience.",
|
||||
"tours.sequenceNavigationCheckpoint.body": "The top bar within your course allows you to easily jump to different sections and shows you what’s coming up.",
|
||||
"tours.existingUserTour.launchTourCheckpoint.body": "We’ve recently added a few new features to the course experience. Want some help looking around? Take a tour to learn more.",
|
||||
"masquerade-widget.userName.input.label": "تقمص دور هذا المستخدم",
|
||||
"tours.abandonTour.launchTourCheckpoint.body": "تشعر بالضياع؟ ابدأ الجولة في أي وقت للحصول على بعض النصائح السريعة لتحقيق أقصى استفادة من التجربة.",
|
||||
"tours.sequenceNavigationCheckpoint.body": "يتيح لك الشريط العلوي في مساقك الانتقال بسهولة إلى أقسام مختلفة ويظهر لك ما هو قادم.",
|
||||
"tours.existingUserTour.launchTourCheckpoint.body": "لقد أضفنا مؤخرًا بعض الميزات الجديدة إلى تجربة المساق. هل أتريد بعض المساعدة في البحث حولك؟ قم بجولة لمعرفة المزيد.",
|
||||
"tours.button.dismiss": "تجاهل",
|
||||
"tours.button.next": "التالي",
|
||||
"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.button.okay": "حسنًا",
|
||||
"tours.button.beginTour": "بدأ الجولة",
|
||||
"tours.button.launchTour": "إطلاق الجولة",
|
||||
"tours.newUserModal.body": "لنذهب في جولة سريعة على {siteName} تمكنك من تحقيق أقصى استفادة من مساقك.",
|
||||
"tours.newUserModal.title.welcome": "مرحبًا بك في",
|
||||
"tours.button.skipForNow": "تخطي في الوقت الراهن",
|
||||
"tours.datesCheckpoint.body": "Important dates can help you stay on track.",
|
||||
"tours.datesCheckpoint.title": "Keep on top of key dates",
|
||||
"tours.outlineCheckpoint.body": "You can explore sections of the course using the outline below.",
|
||||
"tours.outlineCheckpoint.title": "التحق في الدورة!",
|
||||
"tours.tabNavigationCheckpoint.body": "يمكن استخدام علامات التبويب هذه للوصول إلى مواد الدورة التدريبية الأخرى ، مثل تقدمك والمنهج الدراسي وما إلى ذلك.",
|
||||
"tours.tabNavigationCheckpoint.title": "موارد الدورة الإضافية",
|
||||
"tours.upgradeCheckpoint.body": "Work towards a certificate and gain full access to course materials. Upgrade now!",
|
||||
"tours.upgradeCheckpoint.title": "قم بترفية دورتك",
|
||||
"tours.weeklyGoalsCheckpoint.body": "يؤدي تحديد خظة التعلم إلى زيادة احتمالية إتمام دورتك التدريبية.",
|
||||
"tours.weeklyGoalsCheckpoint.title": "حدد خظة التعلم",
|
||||
"tours.newUserModal.title": "{welcome} دورة {siteName}!",
|
||||
"tours.button.skipForNow": "التخطي مؤقتا",
|
||||
"tours.datesCheckpoint.body": "قد تكون التواريخ المهمة عونا لك للبقاء على السكة.",
|
||||
"tours.datesCheckpoint.title": "ابق مٌطّلعًا على أهم التواريخ",
|
||||
"tours.outlineCheckpoint.body": "يمكنك استكشاف أقسام المساق من خلال المخطط أدناه.",
|
||||
"tours.outlineCheckpoint.title": "إبدأ المساق!",
|
||||
"tours.tabNavigationCheckpoint.body": "يمكن استخدام هذه التبويبات للوصول إلى بقية مواد المساق الأخرى كتقدمك، و المنهاج، و غير ذلك.",
|
||||
"tours.tabNavigationCheckpoint.title": "موارد المساق الإضافية",
|
||||
"tours.upgradeCheckpoint.body": "اعمل للحصول على شهادة و استفد من كافة مواد المساق. قم بالترقية الآن!",
|
||||
"tours.upgradeCheckpoint.title": "رفع القيود عن مساقك",
|
||||
"tours.weeklyGoalsCheckpoint.body": "إن تحديد هدف يجعلك أقرب ﻹتمام المساق.",
|
||||
"tours.weeklyGoalsCheckpoint.title": "حدد هدفًا للمساق",
|
||||
"tours.newUserModal.title": "{welcome} مساق {siteName}!",
|
||||
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
|
||||
"learning.effortEstimation.activities": "{activityCount, plural, zero {# أنشطة} one {# نشاط} two {# أنشطة} few {# أنشطة} many {# أنشطة} other {# أنشطة}}",
|
||||
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, zero {# دقيقة} one {# دقيقة} two {# دقيقة} few {# دقيقة} many {# دقيقة} other {# دقائق}}",
|
||||
"learning.effortEstimation.minutesFull": "{minuteCount, plural, zero {# الدقائق} one {# اللحظة} two {# الدقائق} few {# الدقائق} many {# الدقائق} other {# دقائق}}",
|
||||
"learning.effortEstimation.activities": "{activityCount, plural, zero {لا أنشطة} one {نشاط واحد} two {نشاطان} few {# أنشطة} many {# نشاطًا} other {# نشاط}}",
|
||||
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, zero {#د} one {#د} two {#د} few {#د} many {#د} other {#د}}",
|
||||
"learning.effortEstimation.minutesFull": "{minuteCount, plural, zero {# دقيقة} one {# دقيقة واحدة} two {# دقيقتان} few {# دقائق} many {# دقيقة} other {# دقيقة}}",
|
||||
"learning.streakCelebration.congratulations": "تهانينا!",
|
||||
"learning.streakCelebration.body": "استمر في التقدم ، أنت بوضع ممتاز!",
|
||||
"learning.streakCelebration.button": "أبق على ما انت عليه !",
|
||||
"learning.streakCelebration.buttonSrOnly": "إغلاق الوسائط والمتابعة",
|
||||
"learning.streakCelebration.buttonAA759": "أكمل المساق",
|
||||
"learning.streakCelebration.header": "ايام متتالية",
|
||||
"learning.streakCelebration.factoidABoldedSection": "تزيد احتمالية اجتيازهم الدورة التدريبية بمقدار 20 ضعفًا",
|
||||
"learning.streakCelebration.factoidBBoldedSection": "أكمل بمعدل 5 أضعاف محتوى الدورة التدريبية",
|
||||
"learning.streakCelebration.streakDiscountMessage": "لقد قمت بتفعيل خصم {percent}٪ بمكنك استخدامه عند ترقية هذه الدورة التدريبية لفترة محدودة فقط.",
|
||||
"learning.streakcelebration.factoida": "المستخدمون الذين يتعلمون {streak_length} يومًا متتاليًا {bolded_section} أكثر من أولئك الذين لا يتعلمون.",
|
||||
"learning.streakcelebration.factoidb": "المستخدمون الذين يتعلمون {streak_length} يومًا متتاليًا {bolded_section} مقابل أولئك الذين لا يفعلون ذلك.",
|
||||
"learning.streakCelebration.body": "واصل التقدم، حافظ على حماسك!",
|
||||
"learning.streakCelebration.button": "واصل على هذا المنوال",
|
||||
"learning.streakCelebration.buttonSrOnly": "إغلاق النافذة و المتابعة",
|
||||
"learning.streakCelebration.buttonAA759": "المواصلة مع المساق",
|
||||
"learning.streakCelebration.header": "أيام متتالية",
|
||||
"learning.streakCelebration.factoidABoldedSection": "يزيد احتمال نجاحهم في مساقهم بـ 20 ضعفًا",
|
||||
"learning.streakCelebration.factoidBBoldedSection": "يكملون في المتوسط أكثر من 5 أضعاف محتوى المساق الذي يكمله",
|
||||
"learning.streakCelebration.streakDiscountMessage": "لقد حصلت على خصم {percent}% لترقية هذا المساق، و هو صالح لفترة محدودة فقط.",
|
||||
"learning.streakcelebration.factoida": "إن المستخدمين الذين يتعلمون ل{streak_length, plural,\n two {يومين}\n few {# أيام}\n many {# يومًا}\n other {# يوم}\n} على التوالي {bolded_section} عن الذين لا يفعلون مثلهم.",
|
||||
"learning.streakcelebration.factoidb": "إن المستخدمين الذين يتعلمون ل{streak_length, plural,\n two {يومين}\n few {# أيام}\n many {# يومًا}\n other {# يوم}\n} على التوالي {bolded_section} من لا يفعلون مثلهم.",
|
||||
"learning.streakCelebration.streakCelebrationCouponEndDateMessage": "ينتهي في {date}.",
|
||||
"learning.loading.failure": "حدث خطأ أثناء تحميل هذا المساق.",
|
||||
"learning.loading": "يتم الآن تحميل صفحة المساق..."
|
||||
"learning.loading.failure": "حدث خطأ ما أثناء تحميل هذا المساق.",
|
||||
"learning.loading": "جارٍ تحميل صفحة المساق..."
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
"learning.accessExpiration.body": "Pierdes todo acceso a este curso, incluyendo tu progreso, el {fecha}.",
|
||||
"instructorToolbar.pageBanner.courseHasExpired": "El estudiante ya no cuenta con acceso para este curso. Su acceso expiró en {fecha}.",
|
||||
"learning.accessExpiration.upgradeNow": "Actualizar Ahora",
|
||||
"learning.activeEnterprise.alert": "{changeActiveEnterprise}.",
|
||||
"learning.activeEnterprise.change.alert": "cambiar de empresa ahora",
|
||||
"learning.outline.alert.start.short": "El curso comienza en {timeRemaining} a la/s {courseStartTime}.",
|
||||
"learning.outline.alert.end.long": "Este curso terminará en {timeRemaining} el {courseEndDate}.",
|
||||
"learning.outline.alert.end.calendar": "No olvides añadir un recordatorio en el calendario.",
|
||||
@@ -18,8 +20,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 +36,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.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.",
|
||||
"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",
|
||||
@@ -92,6 +94,7 @@
|
||||
"learning.proctoringPanel.status.error": "Error",
|
||||
"learning.proctoringPanel.status.otherCourseApproved": "Aprobado en otro curso",
|
||||
"learning.proctoringPanel.status.expiringSoon": "Expira pronto",
|
||||
"learning.proctoringPanel.status.expired": "Expired",
|
||||
"learning.proctoringPanel.status": "Estado actual de la integración:",
|
||||
"learning.proctoringPanel.message.notStarted": "No has comenzado tu examen de integración.",
|
||||
"learning.proctoringPanel.message.started": "Has comenzado tu examen de integración.",
|
||||
@@ -101,7 +104,8 @@
|
||||
"learning.proctoringPanel.message.error": "Se ha producido un error durante tu examen de integración. Vuelve a intentar la integración.",
|
||||
"learning.proctoringPanel.message.otherCourseApproved": "Tu examen de ingreso ha sido aprobado en otro curso.",
|
||||
"learning.proctoringPanel.detail.otherCourseApproved": "Si tu dispositivo ha cambiado, te recomendamos que realices el examen de ingreso de este curso para asegurarte de que tu configuración sigue cumpliendo los requisitos para el examen supervisado.",
|
||||
"learning.proctoringPanel.message.expiringSoon": "Tu perfil de ingreso ha sido aprobado en otro curso. Sin embargo, tu estado de ingreso expirará pronto. Vuelve a completar el proceso de ingreso para asegurarte de que puedas seguir realizando los exámenes supervisados.",
|
||||
"learning.proctoringPanel.message.expiringSoon": "Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.",
|
||||
"learning.proctoringPanel.message.expired": "Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.",
|
||||
"learning.proctoringPanel.generalInfo": "Debes completar el proceso de integración antes de realizar cualquier examen supervisado. ",
|
||||
"learning.proctoringPanel.generalInfoSubmitted": "Tu perfil enviado está en revisión.",
|
||||
"learning.proctoringPanel.generalTime": "La revisión del perfil de ingreso puede tardar más de 2 días laborables.",
|
||||
@@ -113,7 +117,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": "This course ends on {endDate}. Final grades and any earned certificates are\n scheduled to be available after {certAvailabilityDate}.",
|
||||
"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 +129,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.v2": "Verify your identity to qualify for a certificate.",
|
||||
"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 +250,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,7 +266,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": "This course ends on {endDate}. Final grades and any earned certificates are\n scheduled to be available after {certAvailableDate}.",
|
||||
"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.",
|
||||
@@ -275,7 +279,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": "Your grade and certificate status will be available soon.",
|
||||
"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!",
|
||||
@@ -287,7 +291,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",
|
||||
@@ -332,7 +335,7 @@
|
||||
"notification.tray.title": "Notificaciones",
|
||||
"notification.tray.no.message": "No tienes notificaciones nuevas en este momento.",
|
||||
"learn.contentLock.content.locked": "Contenido Bloqueado",
|
||||
"learn.contentLock.complete.prerequisite": "Debe completar el prerrequisito: '{prereqSectionName}'para acceder a este contenido.",
|
||||
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: ''{prereqSectionName}'' to access this content.",
|
||||
"learn.contentLock.goToSection": "Ir a la Sección de Prerrequisitos",
|
||||
"learn.hiddenAfterDue.gradeAvailable": "Si has completado esta tarea, tu calificación estará disponible en {progressPage}.",
|
||||
"learn.hiddenAfterDue.header": "La fecha límite para esta tarea ha pasado.",
|
||||
@@ -344,8 +347,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. ",
|
||||
@@ -358,8 +361,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?",
|
||||
@@ -378,16 +381,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",
|
||||
@@ -397,7 +400,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",
|
||||
@@ -409,8 +412,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",
|
||||
@@ -422,11 +425,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í",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"learning.accessExpiration.body": "Vous perdez tout accès à ce cours, y compris votre progression, le {date}.",
|
||||
"instructorToolbar.pageBanner.courseHasExpired": "Cet apprenant n'a plus accès à ce cours. Leur accès a expiré le {date}.",
|
||||
"learning.accessExpiration.upgradeNow": "Mettre à jour dès maintenant",
|
||||
"learning.activeEnterprise.alert": " {changeActiveEnterprise}.",
|
||||
"learning.activeEnterprise.change.alert": "change enterprise now",
|
||||
"learning.outline.alert.start.short": "Le cours commence dans {timeRemaining} à {courseStartTime}.",
|
||||
"learning.outline.alert.end.long": "Ce cours se termine dans {timeRemaining}, le {courseEndDate}.",
|
||||
"learning.outline.alert.end.calendar": "N'oubliez pas d'ajouter un rappel dans le calendrier!",
|
||||
@@ -34,8 +36,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.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.",
|
||||
"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",
|
||||
@@ -92,6 +94,7 @@
|
||||
"learning.proctoringPanel.status.error": "Erreur",
|
||||
"learning.proctoringPanel.status.otherCourseApproved": "Approuvé dans un autre cours",
|
||||
"learning.proctoringPanel.status.expiringSoon": "Expire bientôt",
|
||||
"learning.proctoringPanel.status.expired": "Expired",
|
||||
"learning.proctoringPanel.status": "Statut actuel d'intégration :",
|
||||
"learning.proctoringPanel.message.notStarted": "Vous n'avez pas commencé votre examen d'intégration.",
|
||||
"learning.proctoringPanel.message.started": "Vous avez commencé votre examen d'intégration.",
|
||||
@@ -101,7 +104,8 @@
|
||||
"learning.proctoringPanel.message.error": "Une erreur s'est produite lors de votre examen d'intégration. Veuillez réessayer l'intégration.",
|
||||
"learning.proctoringPanel.message.otherCourseApproved": "Votre examen d'intégration a été approuvé dans un autre cours.",
|
||||
"learning.proctoringPanel.detail.otherCourseApproved": "Si votre appareil a changé, nous vous recommandons de passer l'examen d'intégration de ce cours afin de vous assurer que votre configuration répond toujours aux exigences de surveillance.",
|
||||
"learning.proctoringPanel.message.expiringSoon": "Votre profil d'intégration a été approuvé dans un autre cours. Cependant, votre statut d'intégration expire bientôt. Veuillez compléter à nouveau l'intégration afin que vous soyez en mesure de continuer à passer des examens surveillés.",
|
||||
"learning.proctoringPanel.message.expiringSoon": "Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.",
|
||||
"learning.proctoringPanel.message.expired": "Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.",
|
||||
"learning.proctoringPanel.generalInfo": "Vous devez terminer le processus d'intégration avant de passer un examen surveillé.",
|
||||
"learning.proctoringPanel.generalInfoSubmitted": "Votre profil soumis est en cours de révision.",
|
||||
"learning.proctoringPanel.generalTime": "L'examen du profil d'intégration peut prendre plus de 2 jours ouvrables.",
|
||||
@@ -113,7 +117,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": "This course ends on {endDate}. Final grades and any earned certificates are\n scheduled to be available after {certAvailabilityDate}.",
|
||||
"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 +129,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.v2": "Verify your identity to qualify for a certificate.",
|
||||
"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,7 +266,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": "This course ends on {endDate}. Final grades and any earned certificates are\n scheduled to be available after {certAvailableDate}.",
|
||||
"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!",
|
||||
@@ -275,7 +279,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": "Your grade and certificate status will be available soon.",
|
||||
"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 !",
|
||||
@@ -287,7 +291,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",
|
||||
@@ -332,7 +335,7 @@
|
||||
"notification.tray.title": "Notifications",
|
||||
"notification.tray.no.message": "Vous n'avez aucune nouvelle notification pour le moment.",
|
||||
"learn.contentLock.content.locked": "Contenu vérouillé",
|
||||
"learn.contentLock.complete.prerequisite": "Vous devez compléter le prérequis: '{prereqSectionName}' pour accéder à ce contenu.",
|
||||
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: ''{prereqSectionName}'' to access this content.",
|
||||
"learn.contentLock.goToSection": "Aller à la section des prérequis",
|
||||
"learn.hiddenAfterDue.gradeAvailable": "Si vous avez complété ce travail, votre note est disponible sur {progressPage}.",
|
||||
"learn.hiddenAfterDue.header": "La date d'échéance de ce devoir est passée.",
|
||||
@@ -397,7 +400,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",
|
||||
@@ -409,7 +412,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.",
|
||||
@@ -424,9 +427,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",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"learning.accessExpiration.body": "You lose all access to this course, including your progress, on {date}.",
|
||||
"instructorToolbar.pageBanner.courseHasExpired": "This learner no longer has access to this course. Their access expired on {date}.",
|
||||
"learning.accessExpiration.upgradeNow": "Upgrade now",
|
||||
"learning.activeEnterprise.alert": " {changeActiveEnterprise}.",
|
||||
"learning.activeEnterprise.change.alert": "change enterprise now",
|
||||
"learning.outline.alert.start.short": "Course starts {timeRemaining} at {courseStartTime}.",
|
||||
"learning.outline.alert.end.long": "This course is ending {timeRemaining} on {courseEndDate}.",
|
||||
"learning.outline.alert.end.calendar": "Don’t forget to add a calendar reminder!",
|
||||
@@ -92,6 +94,7 @@
|
||||
"learning.proctoringPanel.status.error": "Error",
|
||||
"learning.proctoringPanel.status.otherCourseApproved": "Approved in Another Course",
|
||||
"learning.proctoringPanel.status.expiringSoon": "Expiring Soon",
|
||||
"learning.proctoringPanel.status.expired": "Expired",
|
||||
"learning.proctoringPanel.status": "Current Onboarding Status:",
|
||||
"learning.proctoringPanel.message.notStarted": "You have not started your onboarding exam.",
|
||||
"learning.proctoringPanel.message.started": "You have started your onboarding exam.",
|
||||
@@ -101,7 +104,8 @@
|
||||
"learning.proctoringPanel.message.error": "An error has occurred during your onboarding exam. Please retry onboarding.",
|
||||
"learning.proctoringPanel.message.otherCourseApproved": "Your onboarding exam has been approved in another course.",
|
||||
"learning.proctoringPanel.detail.otherCourseApproved": "If your device has changed, we recommend that you complete this course's onboarding exam in order to ensure that your setup still meets the requirements for proctoring.",
|
||||
"learning.proctoringPanel.message.expiringSoon": "Your onboarding profile has been approved in another course. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.",
|
||||
"learning.proctoringPanel.message.expiringSoon": "Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.",
|
||||
"learning.proctoringPanel.message.expired": "Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.",
|
||||
"learning.proctoringPanel.generalInfo": "You must complete the onboarding process prior to taking any proctored exam. ",
|
||||
"learning.proctoringPanel.generalInfoSubmitted": "Your submitted profile is in review.",
|
||||
"learning.proctoringPanel.generalTime": "Onboarding profile review can take 2+ business days.",
|
||||
@@ -113,7 +117,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 ends on {endDate}. Final grades and any earned certificates are\n scheduled to be available after {certAvailabilityDate}.",
|
||||
"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,9 +129,9 @@
|
||||
"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",
|
||||
@@ -287,7 +291,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",
|
||||
@@ -332,7 +335,7 @@
|
||||
"notification.tray.title": "Notifications",
|
||||
"notification.tray.no.message": "You have no new notifications at this time.",
|
||||
"learn.contentLock.content.locked": "Content Locked",
|
||||
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
||||
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: ''{prereqSectionName}'' to access this content.",
|
||||
"learn.contentLock.goToSection": "Go To Prerequisite Section",
|
||||
"learn.hiddenAfterDue.gradeAvailable": "If you have completed this assignment, your grade is available on the {progressPage}.",
|
||||
"learn.hiddenAfterDue.header": "The due date for this assignment has passed.",
|
||||
@@ -397,7 +400,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",
|
||||
@@ -409,7 +412,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.",
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'regenerator-runtime/runtime';
|
||||
import {
|
||||
APP_INIT_ERROR, APP_READY, subscribe, initialize,
|
||||
mergeConfig,
|
||||
getConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import { AppProvider, ErrorPage, PageRoute } from '@edx/frontend-platform/react';
|
||||
import React from 'react';
|
||||
@@ -12,6 +13,9 @@ 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 { Helmet } from 'react-helmet';
|
||||
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,26 +35,42 @@ 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';
|
||||
import CourseAccessErrorPage from './generic/CourseAccessErrorPage';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={initializeStore()}>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<PathFixesProvider>
|
||||
<NoticesProvider>
|
||||
<UserMessagesProvider>
|
||||
<Switch>
|
||||
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
|
||||
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
|
||||
<PageRoute path="/course/:courseId/access-denied" component={CourseAccessErrorPage} />
|
||||
<PageRoute path="/course/:courseId/home">
|
||||
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
|
||||
<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/',
|
||||
|
||||
@@ -376,6 +376,7 @@
|
||||
@import "course-home/progress-tab/course-completion/CompletionDonutChart.scss";
|
||||
@import "course-home/progress-tab/grades/course-grade/GradeBar.scss";
|
||||
@import "courseware/course/course-exit/CourseRecommendations";
|
||||
@import "product-tours/newUserCourseHomeTour/NewUserCourseHomeTourModal.scss";
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
@import "experiments/mm-p2p/index.scss";
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -19,6 +19,7 @@ function NewUserCourseHomeTourModal({
|
||||
<MarketingModal
|
||||
isOpen={isOpen}
|
||||
title="New user course home prompt"
|
||||
className="new-user-tour-dialog"
|
||||
heroIsDark
|
||||
hasCloseButton={false}
|
||||
heroNode={(
|
||||
@@ -58,7 +59,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[dir="rtl"] .new-user-tour-dialog .pgn__modal-hero .pgn__modal-hero-bg {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
@@ -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,20 @@ 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 'incorrect_active_enterprise':
|
||||
url = `/course/${courseId}/access-denied`;
|
||||
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}`;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -96,8 +96,8 @@ describe('Loaded Tab Page', () => {
|
||||
await renderModal();
|
||||
|
||||
const endDateText = `Ends ${new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toLocaleDateString({ timeZone: 'UTC' })}.`;
|
||||
expect(screen.getByText('You’ve unlocked a 14% off discount when you upgrade this course for a limited time only.')).toBeInTheDocument();
|
||||
expect(screen.getByText(endDateText)).toBeInTheDocument();
|
||||
expect(screen.getByText('You’ve unlocked a 14% off discount when you upgrade this course for a limited time only.', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText(endDateText, { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('Continue with course')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Keep it up')).not.toBeInTheDocument();
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.course.streak_discount_enabled', {
|
||||
|
||||
@@ -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