Compare commits
241 Commits
open-relea
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d1854dd29 | ||
|
|
3e8f2ba4e7 | ||
|
|
4af279edeb | ||
|
|
5148ff06d7 | ||
|
|
d0544bc4e2 | ||
|
|
ceed6cb287 | ||
|
|
004315fdc4 | ||
|
|
8baaa1d50c | ||
|
|
0bd9483cdb | ||
|
|
13eea81fe7 | ||
|
|
1d0ab113e7 | ||
|
|
e34382aa11 | ||
|
|
bf127d5292 | ||
|
|
522ebb0003 | ||
|
|
630e843816 | ||
|
|
9c5ac6ac5b | ||
|
|
f43ac7bcc3 | ||
|
|
b282bc05df | ||
|
|
d987aed861 | ||
|
|
9ece337504 | ||
|
|
d3235af879 | ||
|
|
d0a8778015 | ||
|
|
f8381e7900 | ||
|
|
1d5484ff1d | ||
|
|
52692dc662 | ||
|
|
f91af211f6 | ||
|
|
7318fb3ef7 | ||
|
|
7233f08d3d | ||
|
|
d6d229f1c3 | ||
|
|
47b9a436a6 | ||
|
|
e556d5b74c | ||
|
|
694d95a816 | ||
|
|
e83813da8e | ||
|
|
a54a1b8c3c | ||
|
|
d3188efbcc | ||
|
|
33f737579a | ||
|
|
870263001e | ||
|
|
af50d5a6ed | ||
|
|
7fccf7794c | ||
|
|
c760bc479b | ||
|
|
d5140a6bf0 | ||
|
|
9bf5d01c41 | ||
|
|
f3334085d7 | ||
|
|
4840fff44b | ||
|
|
579bd0365b | ||
|
|
2b4a9661a5 | ||
|
|
a6e4e28e58 | ||
|
|
e6f7588ccd | ||
|
|
db29e314c3 | ||
|
|
e9121f9261 | ||
|
|
9cbc2276d6 | ||
|
|
4c8aa7c80c | ||
|
|
68926334a1 | ||
|
|
56a73eee15 | ||
|
|
bf95916063 | ||
|
|
48270c35dd | ||
|
|
33d7d669d9 | ||
|
|
a75c89cd14 | ||
|
|
06902d8ae8 | ||
|
|
e4134641e6 | ||
|
|
77fcc83efd | ||
|
|
b0505352be | ||
|
|
ddbc2124ef | ||
|
|
462e75f6a6 | ||
|
|
bc4c8c2dec | ||
|
|
ecd5164806 | ||
|
|
44d952bef7 | ||
|
|
7eddc918bb | ||
|
|
f28528e813 | ||
|
|
ab3f5fd7bc | ||
|
|
73eaf61261 | ||
|
|
db9663b664 | ||
|
|
7edac93752 | ||
|
|
d1dede568e | ||
|
|
31b02d777f | ||
|
|
67bb54a028 | ||
|
|
847d4e5ce6 | ||
|
|
b89cdb4a69 | ||
|
|
a1d0afff6c | ||
|
|
1714f285b0 | ||
|
|
03cda5326a | ||
|
|
a71152b008 | ||
|
|
d14c2a9ffd | ||
|
|
b6c29df0a0 | ||
|
|
2ce833341b | ||
|
|
ff57a6b217 | ||
|
|
dc6ee749be | ||
|
|
236fb57023 | ||
|
|
d3d2f75c12 | ||
|
|
8e9306d35a | ||
|
|
b1ee8a3713 | ||
|
|
73406fbb31 | ||
|
|
f4ae1c51ff | ||
|
|
7ef3892027 | ||
|
|
1484bc50f7 | ||
|
|
6b197aad27 | ||
|
|
1412bfe209 | ||
|
|
e8d3bd7c24 | ||
|
|
511091055b | ||
|
|
24c9437e91 | ||
|
|
fb6f110732 | ||
|
|
1656b73a31 | ||
|
|
81671ad328 | ||
|
|
4cc716b20c | ||
|
|
756fbbac83 | ||
|
|
903fe28ff6 | ||
|
|
14c662dc53 | ||
|
|
af432eab27 | ||
|
|
dde640df33 | ||
|
|
b827db800d | ||
|
|
5b7f76b43d | ||
|
|
cf4bea3604 | ||
|
|
85e6e9266d | ||
|
|
360af1f0e9 | ||
|
|
26f4a90976 | ||
|
|
0d45c78ace | ||
|
|
c18214dc41 | ||
|
|
54611c1b4d | ||
|
|
7ca4b71ff7 | ||
|
|
63a7ff83cf | ||
|
|
8ecaa018da | ||
|
|
64ca156095 | ||
|
|
c06f2c37ab | ||
|
|
d5a092b220 | ||
|
|
81b621195e | ||
|
|
226c4cc1d7 | ||
|
|
7f6a59b701 | ||
|
|
7ea0bd175b | ||
|
|
dae1d63e23 | ||
|
|
6ab0deb7b7 | ||
|
|
e5f04d92b9 | ||
|
|
f39a50e7dc | ||
|
|
72724bcafb | ||
|
|
964abbe0c3 | ||
|
|
96d20e20e6 | ||
|
|
a56fd7d0e1 | ||
|
|
679caa61f3 | ||
|
|
420060967b | ||
|
|
91d3762513 | ||
|
|
2e3ed087d1 | ||
|
|
d76d4db097 | ||
|
|
04b314d157 | ||
|
|
1db4848d1a | ||
|
|
8f294781d2 | ||
|
|
d6908abb13 | ||
|
|
96d3d0da7e | ||
|
|
14cc32fcf6 | ||
|
|
0ac127e4c9 | ||
|
|
06e5fb5a44 | ||
|
|
2235737490 | ||
|
|
fca32ae872 | ||
|
|
ae04e5b366 | ||
|
|
db3f1b9cb0 | ||
|
|
ec360bc545 | ||
|
|
6c5220b4d7 | ||
|
|
f433118a8d | ||
|
|
e798331855 | ||
|
|
adb5796ff6 | ||
|
|
9958638a86 | ||
|
|
b8e844eba6 | ||
|
|
da633ffbd9 | ||
|
|
46889c2aba | ||
|
|
3cbbb0272b | ||
|
|
911c7658f5 | ||
|
|
b54d1e467e | ||
|
|
e34d18d727 | ||
|
|
6949e5708f | ||
|
|
eef6b1efe2 | ||
|
|
9dc45e192d | ||
|
|
bd9c97c269 | ||
|
|
c70fb138f0 | ||
|
|
8823cfaa0a | ||
|
|
7865fadec2 | ||
|
|
5be1620f1d | ||
|
|
d5a6a59d07 | ||
|
|
826f1382dd | ||
|
|
5e5fdeba44 | ||
|
|
01369eb00d | ||
|
|
4bb4bb7a88 | ||
|
|
1d154f46c1 | ||
|
|
420db8133f | ||
|
|
1ffc93dc6d | ||
|
|
346e15abd4 | ||
|
|
4726c23bc3 | ||
|
|
cbbb417894 | ||
|
|
c57f28ad40 | ||
|
|
310fb84517 | ||
|
|
623f6946e5 | ||
|
|
cf124877e8 | ||
|
|
0456ad9318 | ||
|
|
7be87b0f83 | ||
|
|
cbe5b28762 | ||
|
|
4a80532b8d | ||
|
|
e505f78cfb | ||
|
|
3811f5f9d5 | ||
|
|
8a20b908c7 | ||
|
|
8a6fa937ea | ||
|
|
dafdcad2b4 | ||
|
|
cd56ffaf9d | ||
|
|
c11cb85d78 | ||
|
|
b09bcbd3ae | ||
|
|
4a925f9c11 | ||
|
|
f5b6243c61 | ||
|
|
98c670afe7 | ||
|
|
038b05ba6c | ||
|
|
020e7fb42c | ||
|
|
ead98538b9 | ||
|
|
90ef6ace5c | ||
|
|
e0196f2a2a | ||
|
|
b7befcff7e | ||
|
|
642031bf87 | ||
|
|
f778f27647 | ||
|
|
b3bce8713c | ||
|
|
dacb30c73e | ||
|
|
81a4deeec0 | ||
|
|
9a1b05a1a4 | ||
|
|
b9e1fb0d2b | ||
|
|
ebd0f8816c | ||
|
|
d749429361 | ||
|
|
19b8df35ae | ||
|
|
e468d2087b | ||
|
|
42e0ac86d7 | ||
|
|
ea5cf37fd8 | ||
|
|
e4cdec7389 | ||
|
|
8aafc6b8bd | ||
|
|
913c8e4086 | ||
|
|
c221770213 | ||
|
|
4fe40c264f | ||
|
|
c20c7677a3 | ||
|
|
2ff8c3949e | ||
|
|
4a5c43d365 | ||
|
|
4da37f369b | ||
|
|
0effb32318 | ||
|
|
6813872dd3 | ||
|
|
e337a367d1 | ||
|
|
65343470e1 | ||
|
|
e69114a839 | ||
|
|
2d63a14c2e | ||
|
|
2d1f893a40 | ||
|
|
64f92deeb1 | ||
|
|
d47433ee83 |
5
.env
5
.env
@@ -12,10 +12,12 @@ CREDIT_HELP_LINK_URL=''
|
|||||||
CSRF_TOKEN_API_PATH=''
|
CSRF_TOKEN_API_PATH=''
|
||||||
DISCOVERY_API_BASE_URL=''
|
DISCOVERY_API_BASE_URL=''
|
||||||
DISCUSSIONS_MFE_BASE_URL=''
|
DISCUSSIONS_MFE_BASE_URL=''
|
||||||
|
DISCOUNT_CODE_INFO_URL=''
|
||||||
ECOMMERCE_BASE_URL=''
|
ECOMMERCE_BASE_URL=''
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
||||||
|
ENTERPRISE_LEARNER_PORTAL_URL=''
|
||||||
EXAMS_BASE_URL=''
|
EXAMS_BASE_URL=''
|
||||||
FAVICON_URL=''
|
FAVICON_URL=''
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
@@ -48,3 +50,6 @@ TWITTER_HASHTAG=''
|
|||||||
TWITTER_URL=''
|
TWITTER_URL=''
|
||||||
USER_INFO_COOKIE_NAME=''
|
USER_INFO_COOKIE_NAME=''
|
||||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||||
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||||
|
# Fallback in local style files
|
||||||
|
PARAGON_THEME_URLS={}
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ APP_ID='learning'
|
|||||||
BASE_URL='http://localhost:2000'
|
BASE_URL='http://localhost:2000'
|
||||||
CONTACT_URL='http://localhost:18000/contact'
|
CONTACT_URL='http://localhost:18000/contact'
|
||||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||||
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
|
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
|
||||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
|
DISCOUNT_CODE_INFO_URL=''
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||||
|
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734'
|
||||||
EXAMS_BASE_URL=''
|
EXAMS_BASE_URL=''
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
@@ -50,3 +52,6 @@ SESSION_COOKIE_DOMAIN='localhost'
|
|||||||
CHAT_RESPONSE_URL='http://localhost:18000/api/learning_assistant/v1/course_id'
|
CHAT_RESPONSE_URL='http://localhost:18000/api/learning_assistant/v1/course_id'
|
||||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||||
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||||
|
# Fallback in local style files
|
||||||
|
PARAGON_THEME_URLS={}
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ APP_ID='learning'
|
|||||||
BASE_URL='http://localhost:2000'
|
BASE_URL='http://localhost:2000'
|
||||||
CONTACT_URL='http://localhost:18000/contact'
|
CONTACT_URL='http://localhost:18000/contact'
|
||||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||||
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
|
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
|
||||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
|
DISCOUNT_CODE_INFO_URL=''
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||||
|
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734'
|
||||||
EXAMS_BASE_URL='http://localhost:18740'
|
EXAMS_BASE_URL='http://localhost:18740'
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
@@ -47,3 +49,6 @@ TWITTER_HASHTAG='myedxjourney'
|
|||||||
TWITTER_URL='https://twitter.com/edXOnline'
|
TWITTER_URL='https://twitter.com/edXOnline'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||||
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||||
|
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:Enterprise'
|
||||||
|
FEATURE_ENABLE_CHAT_V2_ENDPOINT='false'
|
||||||
|
|||||||
18
.github/workflows/add-issue-to-btr-project.yml
vendored
Normal file
18
.github/workflows/add-issue-to-btr-project.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Run the workflow that adds new tickets that are labelled "release testing"
|
||||||
|
# to the org-wide BTR project board
|
||||||
|
|
||||||
|
name: Add release testing issues to the BTR project board
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled]
|
||||||
|
# This workflow is triggered when an issue is labeled with 'release testing'.
|
||||||
|
# It adds the issue to the BTR project and applies the 'needs triage' label
|
||||||
|
# if it doesn't already have it.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
handle-release-testing:
|
||||||
|
uses: openedx/.github/.github/workflows/add-issue-to-btr-project.yml@master
|
||||||
|
secrets:
|
||||||
|
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||||
|
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||||
21
.github/workflows/validate.yml
vendored
21
.github/workflows/validate.yml
vendored
@@ -9,35 +9,28 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node: [18, 20]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version-file: '.nvmrc'
|
||||||
- run: make validate.ci
|
- run: make validate.ci
|
||||||
- name: Archive code coverage results
|
- name: Archive code coverage results
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: code-coverage-report-${{ matrix.node }}
|
name: code-coverage-report
|
||||||
# When we're only using Node 20, replace the line above with the following:
|
|
||||||
# name: code-coverage-report
|
|
||||||
path: coverage/*.*
|
path: coverage/*.*
|
||||||
coverage:
|
coverage:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: tests
|
needs: tests
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v5
|
||||||
- name: Download code coverage results
|
- name: Download code coverage results
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: code-coverage-report-20
|
pattern: code-coverage-report
|
||||||
# When we're only using Node 20, replace the line above with the following:
|
|
||||||
# name: code-coverage-report
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
1
.husky/_/.gitignore
vendored
1
.husky/_/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
*
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
if [ -z "$husky_skip_init" ]; then
|
|
||||||
debug () {
|
|
||||||
if [ "$HUSKY_DEBUG" = "1" ]; then
|
|
||||||
echo "husky (debug) - $1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly hook_name="$(basename "$0")"
|
|
||||||
debug "starting $hook_name..."
|
|
||||||
|
|
||||||
if [ "$HUSKY" = "0" ]; then
|
|
||||||
debug "HUSKY env variable is set to 0, skipping hook"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f ~/.huskyrc ]; then
|
|
||||||
debug "sourcing ~/.huskyrc"
|
|
||||||
. ~/.huskyrc
|
|
||||||
fi
|
|
||||||
|
|
||||||
export readonly husky_skip_init=1
|
|
||||||
sh -e "$0" "$@"
|
|
||||||
exitCode="$?"
|
|
||||||
|
|
||||||
if [ $exitCode != 0 ]; then
|
|
||||||
echo "husky - $hook_name hook exited with code $exitCode (error)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
fi
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npm run lint
|
|
||||||
@@ -41,9 +41,8 @@ Cloning and Setup
|
|||||||
|
|
||||||
git clone https://github.com/openedx/frontend-app-learning.git
|
git clone https://github.com/openedx/frontend-app-learning.git
|
||||||
|
|
||||||
2. Use node v20.x.
|
2. Use the version of Node specified in ``.nvmrc``.
|
||||||
|
|
||||||
The current version of the micro-frontend build scripts supports node 18.
|
|
||||||
Using other major versions of node *may* work, but this is unsupported. For
|
Using other major versions of node *may* work, but this is unsupported. For
|
||||||
convenience, this repository includes an ``.nvmrc`` file to help in setting the
|
convenience, this repository includes an ``.nvmrc`` file to help in setting the
|
||||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||||
@@ -131,7 +130,7 @@ Deployment
|
|||||||
|
|
||||||
The Learning MFE is similar to all the other Open edX MFEs. Read the Open
|
The Learning MFE is similar to all the other Open edX MFEs. Read the Open
|
||||||
edX Developer Guide's section on
|
edX Developer Guide's section on
|
||||||
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
|
`MFE applications <https://openedx.github.io/frontend-platform/>`_.
|
||||||
|
|
||||||
Plugins
|
Plugins
|
||||||
=======
|
=======
|
||||||
@@ -145,7 +144,7 @@ Environment Variables
|
|||||||
This MFE is configured via environment variables supplied at build time.
|
This MFE is configured via environment variables supplied at build time.
|
||||||
All micro-frontends have a shared set of required environment variables,
|
All micro-frontends have a shared set of required environment variables,
|
||||||
as documented in the Open edX Developer Guide under
|
as documented in the Open edX Developer Guide under
|
||||||
`Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`_.
|
`Required Environment Variables <https://openedx.github.io/frontend-platform/>`_.
|
||||||
|
|
||||||
The learning micro-frontend also supports the following additional variables:
|
The learning micro-frontend also supports the following additional variables:
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ metadata:
|
|||||||
icon: "Web"
|
icon: "Web"
|
||||||
annotations:
|
annotations:
|
||||||
openedx.org/arch-interest-groups: ""
|
openedx.org/arch-interest-groups: ""
|
||||||
|
openedx.org/release: "master"
|
||||||
spec:
|
spec:
|
||||||
owner: group:committers-frontend-app-learning
|
owner: group:committers-frontend-app-learning
|
||||||
type: 'website'
|
type: 'website'
|
||||||
|
|||||||
10
openedx.yaml
10
openedx.yaml
@@ -1,10 +0,0 @@
|
|||||||
# This file describes this Open edX repo, as described in OEP-2:
|
|
||||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
|
|
||||||
|
|
||||||
oeps: {}
|
|
||||||
owner: edx/platform-core-tnl
|
|
||||||
openedx-release:
|
|
||||||
# The openedx-release key is described in OEP-10:
|
|
||||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html
|
|
||||||
# The FAQ might also be helpful: https://openedx.atlassian.net/wiki/spaces/COMM/pages/1331268879/Open+edX+Release+FAQ
|
|
||||||
ref: master
|
|
||||||
13753
package-lock.json
generated
13753
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
53
package.json
53
package.json
@@ -15,12 +15,11 @@
|
|||||||
"i18n_extract": "fedx-scripts formatjs extract",
|
"i18n_extract": "fedx-scripts formatjs extract",
|
||||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||||
"prepare": "husky install",
|
|
||||||
"postinstall": "patch-package",
|
|
||||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
|
||||||
"start": "fedx-scripts webpack-dev-server --progress",
|
"start": "fedx-scripts webpack-dev-server --progress",
|
||||||
|
"start:with-theme": "paragon install-theme && npm start && npm install",
|
||||||
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||||
"test": "fedx-scripts jest --coverage --passWithNoTests",
|
"test": "NODE_ENV=test fedx-scripts jest --coverage --passWithNoTests",
|
||||||
|
"test:watch": "fedx-scripts jest --watch --passWithNoTests",
|
||||||
"types": "tsc --noEmit"
|
"types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"author": "edX",
|
"author": "edX",
|
||||||
@@ -34,43 +33,39 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||||
"@edx/browserslist-config": "1.2.0",
|
"@edx/browserslist-config": "1.5.1",
|
||||||
"@edx/frontend-component-header": "^5.8.0",
|
"@edx/frontend-component-footer": "^14.6.0",
|
||||||
"@edx/frontend-lib-learning-assistant": "^2.2.4",
|
"@edx/frontend-component-header": "^8.0.0",
|
||||||
"@edx/frontend-lib-special-exams": "^3.1.3",
|
"@edx/frontend-lib-learning-assistant": "^2.24.0",
|
||||||
"@edx/frontend-platform": "^8.0.0",
|
"@edx/frontend-lib-special-exams": "^4.0.0",
|
||||||
"@edx/openedx-atlas": "^0.6.0",
|
"@edx/frontend-platform": "^8.4.0",
|
||||||
"@edx/react-unit-test-utils": "3.0.0",
|
"@edx/openedx-atlas": "^0.7.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||||
"@openedx/frontend-build": "14.1.2",
|
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||||
"@openedx/frontend-plugin-framework": "^1.2.1",
|
"@openedx/paragon": "^23.4.5",
|
||||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
|
||||||
"@openedx/paragon": "^22.3.0",
|
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
"@reduxjs/toolkit": "1.8.1",
|
"@reduxjs/toolkit": "1.9.7",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"classnames": "2.5.1",
|
"classnames": "2.5.1",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^12.0.0",
|
||||||
"husky": "7.0.4",
|
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash.camelcase": "4.3.0",
|
"lodash.camelcase": "4.3.0",
|
||||||
"patch-package": "^8.0.0",
|
|
||||||
"postcss-loader": "^8.1.1",
|
"postcss-loader": "^8.1.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"react": "17.0.2",
|
"react": "^18.3.1",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "^18.3.1",
|
||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-redux": "7.2.9",
|
"react-redux": "7.2.9",
|
||||||
"react-router": "6.15.0",
|
"react-router": "6.15.0",
|
||||||
"react-router-dom": "6.15.0",
|
"react-router-dom": "6.15.0",
|
||||||
"react-share": "4.4.1",
|
"react-share": "4.4.1",
|
||||||
"redux": "4.1.2",
|
"redux": "4.2.1",
|
||||||
"reselect": "4.1.8",
|
"reselect": "4.1.8",
|
||||||
"sass": "^1.79.3",
|
"sass": "^1.79.3",
|
||||||
"sass-loader": "^16.0.2",
|
"sass-loader": "^16.0.2",
|
||||||
@@ -78,16 +73,14 @@
|
|||||||
"truncate-html": "1.0.4"
|
"truncate-html": "1.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/reactifex": "2.2.0",
|
"@openedx/frontend-build": "^14.6.2",
|
||||||
"@pact-foundation/pact": "^13.0.0",
|
"@pact-foundation/pact": "^13.0.0",
|
||||||
"@testing-library/jest-dom": "5.17.0",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/user-event": "14.6.1",
|
||||||
"@testing-library/user-event": "13.5.0",
|
"axios-mock-adapter": "2.1.0",
|
||||||
"axios-mock-adapter": "2.0.0",
|
|
||||||
"bundlewatch": "^0.4.0",
|
"bundlewatch": "^0.4.0",
|
||||||
"eslint-import-resolver-webpack": "^0.13.9",
|
"eslint-import-resolver-webpack": "^0.13.9",
|
||||||
"jest": "^29.7.0",
|
|
||||||
"jest-console-group-reporter": "^1.1.1",
|
"jest-console-group-reporter": "^1.1.1",
|
||||||
"jest-when": "^3.6.0",
|
"jest-when": "^3.6.0",
|
||||||
"rosie": "2.1.1"
|
"rosie": "2.1.1"
|
||||||
@@ -96,7 +89,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "dist/*.js",
|
"path": "dist/*.js",
|
||||||
"maxSize": "1300kB"
|
"maxSize": "1450kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
|
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
diff --git a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
|
||||||
index 2879dd9..9efd0fc 100644
|
|
||||||
--- a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
|
||||||
+++ b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
|
||||||
@@ -12,6 +12,7 @@ const NewRelicSourceMapPlugin = require('@edx/new-relic-source-map-webpack-plugi
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
||||||
const path = require('path');
|
|
||||||
+const fs = require('fs');
|
|
||||||
const PostCssAutoprefixerPlugin = require('autoprefixer');
|
|
||||||
const PostCssRTLCSS = require('postcss-rtlcss');
|
|
||||||
const PostCssCustomMediaCSS = require('postcss-custom-media');
|
|
||||||
@@ -23,6 +24,23 @@ const HtmlWebpackNewRelicPlugin = require('../lib/plugins/html-webpack-new-relic
|
|
||||||
const commonConfig = require('./webpack.common.config');
|
|
||||||
const presets = require('../lib/presets');
|
|
||||||
|
|
||||||
+/**
|
|
||||||
+ * This condition confirms whether the configuration for the MFE has switched to a JS-based configuration
|
|
||||||
+ * as previously implemented in frontend-build and frontend-platform. If the environment variable JS_CONFIG_FILEPATH
|
|
||||||
+ * exists, then an env.config.js(x) file will be copied from the location referenced by the environment variable to the
|
|
||||||
+ * root directory. Its env variables can be accessed with getConfig().
|
|
||||||
+ *
|
|
||||||
+ * https://github.com/openedx/frontend-build/blob/master/docs/0002-js-environment-config.md
|
|
||||||
+ * https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
|
|
||||||
+ */
|
|
||||||
+
|
|
||||||
+const envConfigPath = process.env.JS_CONFIG_FILEPATH;
|
|
||||||
+
|
|
||||||
+if (envConfigPath) {
|
|
||||||
+ const envConfigFilename = envConfigPath.slice(envConfigPath.indexOf('env.config'));
|
|
||||||
+ fs.copyFileSync(envConfigPath, envConfigFilename);
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
// Add process env vars. Currently used only for setting the PUBLIC_PATH.
|
|
||||||
dotenv.config({
|
|
||||||
path: path.resolve(process.cwd(), '.env'),
|
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
<% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %>
|
<% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %>
|
||||||
<script src="https://www.edx.org/optimizelyjs/<%= htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
<script src="https://www.edx.org/optimizelyjs/<%= htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (htmlWebpackPlugin.options.META_TAG_ROBOTS_CONTENT_ATTR) { %>
|
||||||
|
<meta name="robots" content="<%= htmlWebpackPlugin.options.META_TAG_ROBOTS_CONTENT_ATTR %>">
|
||||||
|
<% } %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import {
|
import { FormattedMessage, FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
FormattedMessage, FormattedDate, injectIntl, intlShape,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||||
import { Info } from '@openedx/paragon/icons';
|
import { Info } from '@openedx/paragon/icons';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const AccessExpirationAlert = ({ intl, payload }) => {
|
const AccessExpirationAlert = ({ payload }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
courseId,
|
courseId,
|
||||||
@@ -119,7 +118,6 @@ const AccessExpirationAlert = ({ intl, payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AccessExpirationAlert.propTypes = {
|
AccessExpirationAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
accessExpiration: PropTypes.shape({
|
accessExpiration: PropTypes.shape({
|
||||||
expirationDate: PropTypes.string.isRequired,
|
expirationDate: PropTypes.string.isRequired,
|
||||||
@@ -134,4 +132,4 @@ AccessExpirationAlert.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(AccessExpirationAlert);
|
export default AccessExpirationAlert;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||||
import { WarningFilled } from '@openedx/paragon/icons';
|
import { WarningFilled } from '@openedx/paragon/icons';
|
||||||
@@ -7,7 +7,8 @@ import { WarningFilled } from '@openedx/paragon/icons';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import genericMessages from './messages';
|
import genericMessages from './messages';
|
||||||
|
|
||||||
const ActiveEnterpriseAlert = ({ intl, payload }) => {
|
const ActiveEnterpriseAlert = ({ payload }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const { text, courseId } = payload;
|
const { text, courseId } = payload;
|
||||||
const changeActiveEnterprise = (
|
const changeActiveEnterprise = (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
@@ -38,11 +39,10 @@ const ActiveEnterpriseAlert = ({ intl, payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ActiveEnterpriseAlert.propTypes = {
|
ActiveEnterpriseAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
courseId: PropTypes.string,
|
courseId: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(ActiveEnterpriseAlert);
|
export default ActiveEnterpriseAlert;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Alert, Button } from '@openedx/paragon';
|
import { Alert, Button } from '@openedx/paragon';
|
||||||
import { Info, WarningFilled } from '@openedx/paragon/icons';
|
import { Info, WarningFilled } from '@openedx/paragon/icons';
|
||||||
@@ -11,7 +11,8 @@ import { useModel } from '../../generic/model-store';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import useEnrollClickHandler from './clickHook';
|
import useEnrollClickHandler from './clickHook';
|
||||||
|
|
||||||
const EnrollmentAlert = ({ intl, payload }) => {
|
const EnrollmentAlert = ({ payload }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
canEnroll,
|
canEnroll,
|
||||||
courseId,
|
courseId,
|
||||||
@@ -58,7 +59,6 @@ const EnrollmentAlert = ({ intl, payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
EnrollmentAlert.propTypes = {
|
EnrollmentAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
canEnroll: PropTypes.bool,
|
canEnroll: PropTypes.bool,
|
||||||
courseId: PropTypes.string,
|
courseId: PropTypes.string,
|
||||||
@@ -67,4 +67,4 @@ EnrollmentAlert.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(EnrollmentAlert);
|
export default EnrollmentAlert;
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import { Check, ArrowForward } from '@openedx/paragon/icons';
|
import { Check, ArrowForward } from '@openedx/paragon/icons';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { sendActivationEmail } from '../../courseware/data';
|
import { sendActivationEmail } from '../../courseware/data';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const AccountActivationAlert = ({
|
const AccountActivationAlert = () => {
|
||||||
intl,
|
const intl = useIntl();
|
||||||
}) => {
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [showSpinner, setShowSpinner] = useState(false);
|
const [showSpinner, setShowSpinner] = useState(false);
|
||||||
const [showCheck, setShowCheck] = useState(false);
|
const [showCheck, setShowCheck] = useState(false);
|
||||||
@@ -125,8 +124,4 @@ const AccountActivationAlert = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AccountActivationAlert.propTypes = {
|
export default AccountActivationAlert;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(AccountActivationAlert);
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||||
import { WarningFilled } from '@openedx/paragon/icons';
|
import { WarningFilled } from '@openedx/paragon/icons';
|
||||||
|
|
||||||
import genericMessages from '../../generic/messages';
|
import genericMessages from '../../generic/messages';
|
||||||
|
|
||||||
const LogistrationAlert = ({ intl }) => {
|
const LogistrationAlert = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const signIn = (
|
const signIn = (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
style={{ textDecoration: 'underline' }}
|
style={{ textDecoration: 'underline' }}
|
||||||
@@ -43,8 +44,4 @@ const LogistrationAlert = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
LogistrationAlert.propTypes = {
|
export default LogistrationAlert;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(LogistrationAlert);
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export const DECODE_ROUTES = {
|
|||||||
'/course/:courseId/:sequenceId/:unitId',
|
'/course/:courseId/:sequenceId/:unitId',
|
||||||
'/course/:courseId/:sequenceId',
|
'/course/:courseId/:sequenceId',
|
||||||
'/course/:courseId',
|
'/course/:courseId',
|
||||||
|
'/preview/course/:courseId/:sequenceId/:unitId',
|
||||||
|
'/preview/course/:courseId/:sequenceId',
|
||||||
],
|
],
|
||||||
REDIRECT_HOME: 'home/:courseId',
|
REDIRECT_HOME: 'home/:courseId',
|
||||||
REDIRECT_SURVEY: 'survey/:courseId',
|
REDIRECT_SURVEY: 'survey/:courseId',
|
||||||
@@ -20,7 +22,7 @@ export const DECODE_ROUTES = {
|
|||||||
|
|
||||||
export const ROUTES = {
|
export const ROUTES = {
|
||||||
UNSUBSCRIBE: '/goal-unsubscribe/:token',
|
UNSUBSCRIBE: '/goal-unsubscribe/:token',
|
||||||
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch',
|
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch?',
|
||||||
REDIRECT: '/redirect/*',
|
REDIRECT: '/redirect/*',
|
||||||
DASHBOARD: 'dashboard',
|
DASHBOARD: 'dashboard',
|
||||||
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
|
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
|
||||||
@@ -46,6 +48,20 @@ export const VERIFIED_MODES = [
|
|||||||
'paid-bootcamp',
|
'paid-bootcamp',
|
||||||
] as const satisfies readonly string[];
|
] as const satisfies readonly string[];
|
||||||
|
|
||||||
|
export const AUDIT_MODES = [
|
||||||
|
'audit',
|
||||||
|
'honor',
|
||||||
|
'unpaid-executive-education',
|
||||||
|
'unpaid-bootcamp',
|
||||||
|
] as const satisfies readonly string[];
|
||||||
|
|
||||||
|
// In sync with CourseMode.UPSELL_TO_VERIFIED_MODES
|
||||||
|
// https://github.com/openedx/edx-platform/blob/master/common/djangoapps/course_modes/models.py#L231
|
||||||
|
export const ALLOW_UPSELL_MODES = [
|
||||||
|
'audit',
|
||||||
|
'honor',
|
||||||
|
] as const satisfies readonly string[];
|
||||||
|
|
||||||
export const WIDGETS = {
|
export const WIDGETS = {
|
||||||
DISCUSSIONS: 'DISCUSSIONS',
|
DISCUSSIONS: 'DISCUSSIONS',
|
||||||
NOTIFICATIONS: 'NOTIFICATIONS',
|
NOTIFICATIONS: 'NOTIFICATIONS',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Tabs, Tab } from '@openedx/paragon';
|
import { Tabs, Tab } from '@openedx/paragon';
|
||||||
|
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
@@ -13,7 +13,8 @@ const filterTypes = ['text', 'video', 'sequence'];
|
|||||||
const filterOther = 'other';
|
const filterOther = 'other';
|
||||||
const validFilters = [filterAll, ...filterTypes, filterOther];
|
const validFilters = [filterAll, ...filterTypes, filterOther];
|
||||||
|
|
||||||
export const CoursewareSearchResultsFilter = ({ intl }) => {
|
export const CoursewareSearchResultsFilter = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
const lastSearch = useModel('contentSearchResults', courseId);
|
const lastSearch = useModel('contentSearchResults', courseId);
|
||||||
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
|
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
|
||||||
@@ -73,8 +74,4 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CoursewareSearchResultsFilter.propTypes = {
|
export default CoursewareSearchResultsFilter;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CoursewareSearchResultsFilter);
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Alert, Button, Icon, Spinner,
|
Alert, Button, Icon, Spinner,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
@@ -18,7 +18,8 @@ import CoursewareSearchResultsFilterContainer from './CoursewareResultsFilter';
|
|||||||
import { updateModel, useModel } from '../../generic/model-store';
|
import { updateModel, useModel } from '../../generic/model-store';
|
||||||
import { searchCourseContent } from '../data/thunks';
|
import { searchCourseContent } from '../data/thunks';
|
||||||
|
|
||||||
const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
const CoursewareSearch = ({ ...sectionProps }) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
const { query: searchKeyword, setQuery, clearSearchParams } = useCoursewareSearchParams();
|
const { query: searchKeyword, setQuery, clearSearchParams } = useCoursewareSearchParams();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -29,6 +30,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
|||||||
errors,
|
errors,
|
||||||
total,
|
total,
|
||||||
} = useModel('contentSearchResults', courseId);
|
} = useModel('contentSearchResults', courseId);
|
||||||
|
const dialogRef = useRef();
|
||||||
|
|
||||||
useLockScroll();
|
useLockScroll();
|
||||||
|
|
||||||
@@ -44,7 +46,8 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
|||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
results: [],
|
results: [],
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
loading: false,
|
loading:
|
||||||
|
false,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -66,20 +69,46 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
|||||||
setQuery(value);
|
setQuery(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleSubmit(searchKeyword);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleOnChange = (value) => {
|
const handleOnChange = (value) => {
|
||||||
if (value === searchKeyword) { return; }
|
if (value === searchKeyword) { return; }
|
||||||
if (!value) { clearSearch(); }
|
if (!value) { clearSearch(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchCloseClick = () => {
|
const close = () => {
|
||||||
clearSearch();
|
clearSearch();
|
||||||
dispatch(setShowSearch(false));
|
dispatch(setShowSearch(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePopState = () => close();
|
||||||
|
|
||||||
|
const handleBackdropClick = function (event) {
|
||||||
|
if (event.target === dialogRef.current) {
|
||||||
|
dialogRef.current.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// We need this to keep the dialog reference when unmounting.
|
||||||
|
const dialog = dialogRef.current;
|
||||||
|
|
||||||
|
// Open the dialog as a modal on render to confine focus within it.
|
||||||
|
dialogRef.current.showModal();
|
||||||
|
|
||||||
|
if (searchKeyword) {
|
||||||
|
handleSubmit(searchKeyword); // In case it's opened with a search link, we run the search.
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const { signal } = controller;
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handlePopState, { signal });
|
||||||
|
dialog.addEventListener('click', handleBackdropClick, { signal });
|
||||||
|
|
||||||
|
return () => controller.abort(); // Removes event listeners.
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearchClose = () => close();
|
||||||
|
|
||||||
let status = 'idle';
|
let status = 'idle';
|
||||||
if (loading) {
|
if (loading) {
|
||||||
status = 'loading';
|
status = 'loading';
|
||||||
@@ -90,59 +119,58 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="courseware-search" style={{ '--modal-top-position': top }} data-testid="courseware-search-section" {...sectionProps}>
|
<dialog ref={dialogRef} className="courseware-search" style={{ '--modal-top-position': top }} data-testid="courseware-search-dialog" onClose={handleSearchClose} {...sectionProps}>
|
||||||
<div className="courseware-search__close">
|
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
className="p-1"
|
|
||||||
aria-label={intl.formatMessage(messages.searchCloseAction)}
|
|
||||||
onClick={handleSearchCloseClick}
|
|
||||||
data-testid="courseware-search-close-button"
|
|
||||||
><Icon src={Close} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="courseware-search__outer-content">
|
<div className="courseware-search__outer-content">
|
||||||
<div className="courseware-search__content">
|
<div className="courseware-search__content" data-testid="courseware-search-content">
|
||||||
<h1 className="h2">{intl.formatMessage(messages.searchModuleTitle)}</h1>
|
<div className="courseware-search__form">
|
||||||
<CoursewareSearchForm
|
<h1 className="h2">{formatMessage(messages.searchModuleTitle)}</h1>
|
||||||
searchTerm={searchKeyword}
|
<CoursewareSearchForm
|
||||||
onSubmit={handleSubmit}
|
searchTerm={searchKeyword}
|
||||||
onChange={handleOnChange}
|
onSubmit={handleSubmit}
|
||||||
placeholder={intl.formatMessage(messages.searchBarPlaceholderText)}
|
onChange={handleOnChange}
|
||||||
/>
|
placeholder={formatMessage(messages.searchBarPlaceholderText)}
|
||||||
{status === 'loading' ? (
|
/>
|
||||||
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
<div className="courseware-search__close">
|
||||||
<Spinner animation="border" variant="light" screenReaderText={intl.formatMessage(messages.loading)} />
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
className="p-1"
|
||||||
|
aria-label={formatMessage(messages.searchCloseAction)}
|
||||||
|
onClick={() => dialogRef.current.close()}
|
||||||
|
data-testid="courseware-search-close-button"
|
||||||
|
><Icon src={Close} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
{status === 'error' && (
|
<div className="courseware-search__results" aria-live="polite" data-testid="courseware-search-results">
|
||||||
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
|
{status === 'loading' ? (
|
||||||
{intl.formatMessage(messages.searchResultsError)}
|
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
||||||
</Alert>
|
<Spinner animation="border" variant="light" screenReaderText={formatMessage(messages.loading)} />
|
||||||
)}
|
</div>
|
||||||
{status === 'results' ? (
|
) : null}
|
||||||
<>
|
{status === 'error' && (
|
||||||
{total > 0 ? (
|
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error">
|
||||||
<div
|
{formatMessage(messages.searchResultsError)}
|
||||||
className="courseware-search__results-summary"
|
</Alert>
|
||||||
aria-live="polite"
|
)}
|
||||||
aria-relevant="all"
|
{status === 'results' ? (
|
||||||
aria-atomic="true"
|
<>
|
||||||
data-testid="courseware-search-summary"
|
{total > 0 ? (
|
||||||
>{intl.formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
|
<div
|
||||||
</div>
|
className="courseware-search__results-summary"
|
||||||
) : null}
|
aria-relevant="all"
|
||||||
<CoursewareSearchResultsFilterContainer />
|
aria-atomic="true"
|
||||||
</>
|
data-testid="courseware-search-summary"
|
||||||
) : null}
|
>{formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<CoursewareSearchResultsFilterContainer />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CoursewareSearch.propTypes = {
|
export default CoursewareSearch;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CoursewareSearch);
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
screen,
|
screen,
|
||||||
waitFor,
|
waitFor,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
|
within,
|
||||||
} from '../../setupTest';
|
} from '../../setupTest';
|
||||||
import { CoursewareSearch } from './index';
|
import { CoursewareSearch } from './index';
|
||||||
import { useElementBoundingBox, useLockScroll, useCoursewareSearchParams } from './hooks';
|
import { useElementBoundingBox, useLockScroll, useCoursewareSearchParams } from './hooks';
|
||||||
@@ -19,6 +20,7 @@ import { updateModel, useModel } from '../../generic/model-store';
|
|||||||
|
|
||||||
jest.mock('./hooks');
|
jest.mock('./hooks');
|
||||||
jest.mock('../../generic/model-store', () => ({
|
jest.mock('../../generic/model-store', () => ({
|
||||||
|
...jest.requireActual('../../generic/model-store'),
|
||||||
updateModel: jest.fn(),
|
updateModel: jest.fn(),
|
||||||
useModel: jest.fn(),
|
useModel: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -56,7 +58,7 @@ const defaultProps = {
|
|||||||
total: 0,
|
total: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const coursewareSearch = {
|
const defaultSearchParams = {
|
||||||
query: '',
|
query: '',
|
||||||
filter: '',
|
filter: '',
|
||||||
setQuery: jest.fn(),
|
setQuery: jest.fn(),
|
||||||
@@ -96,14 +98,20 @@ const mockModels = ((props = defaultProps) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockSearchParams = ((props = coursewareSearch) => {
|
const mockSearchParams = ((params) => {
|
||||||
|
const props = { ...defaultSearchParams, ...params };
|
||||||
useCoursewareSearchParams.mockReturnValue(props);
|
useCoursewareSearchParams.mockReturnValue(props);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CoursewareSearch', () => {
|
describe('CoursewareSearch', () => {
|
||||||
beforeAll(initializeMockApp);
|
beforeAll(() => initializeMockApp());
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockModels();
|
||||||
|
mockSearchParams();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,27 +121,22 @@ describe('CoursewareSearch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
|
it('should use useElementBoundingBox() and useLockScroll() hooks', () => {
|
||||||
mockModels();
|
|
||||||
mockSearchParams();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
expect(useElementBoundingBox).toBeCalledTimes(1);
|
expect(useElementBoundingBox).toHaveBeenCalledTimes(1);
|
||||||
expect(useLockScroll).toBeCalledTimes(1);
|
expect(useLockScroll).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
|
it('should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
|
||||||
mockModels();
|
|
||||||
mockSearchParams();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
const section = screen.getByTestId('courseware-search-section');
|
const section = screen.getByTestId('courseware-search-dialog');
|
||||||
expect(section.style.getPropertyValue('--modal-top-position')).toBe(`${tabsTopPosition}px`);
|
expect(section.style.getPropertyValue('--modal-top-position')).toBe(`${tabsTopPosition}px`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when clicking on the "Close" button', () => {
|
describe('when clicking on the "Close" button', () => {
|
||||||
it('should dispatch setShowSearch(false)', async () => {
|
it('should close the dialog', async () => {
|
||||||
mockModels();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -141,7 +144,8 @@ describe('CoursewareSearch', () => {
|
|||||||
fireEvent.click(close);
|
fireEvent.click(close);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(setShowSearch).toBeCalledWith(false);
|
expect(HTMLDialogElement.prototype.close).toHaveBeenCalled();
|
||||||
|
expect(setShowSearch).toHaveBeenCalledWith(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,29 +153,24 @@ describe('CoursewareSearch', () => {
|
|||||||
it('should use "--modal-top-position: 0" if nce element is not present', () => {
|
it('should use "--modal-top-position: 0" if nce element is not present', () => {
|
||||||
useElementBoundingBox.mockImplementation(() => undefined);
|
useElementBoundingBox.mockImplementation(() => undefined);
|
||||||
|
|
||||||
mockModels();
|
|
||||||
mockSearchParams();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
const section = screen.getByTestId('courseware-search-section');
|
const section = screen.getByTestId('courseware-search-dialog');
|
||||||
expect(section.style.getPropertyValue('--modal-top-position')).toBe('0');
|
expect(section.style.getPropertyValue('--modal-top-position')).toBe('0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when passing extra props', () => {
|
describe('when passing extra props', () => {
|
||||||
it('should pass on extra props to section element', () => {
|
it('should pass on extra props to section element', () => {
|
||||||
mockModels();
|
|
||||||
mockSearchParams();
|
|
||||||
renderComponent({ foo: 'bar' });
|
renderComponent({ foo: 'bar' });
|
||||||
|
|
||||||
const section = screen.getByTestId('courseware-search-section');
|
const section = screen.getByTestId('courseware-search-dialog');
|
||||||
expect(section).toHaveAttribute('foo', 'bar');
|
expect(section).toHaveAttribute('foo', 'bar');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when submitting an empty search', () => {
|
describe('when submitting an empty search', () => {
|
||||||
it('should clear the search by dispatch updateModel', async () => {
|
it('should clear the search by dispatch updateModel', async () => {
|
||||||
mockModels();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -203,7 +202,6 @@ describe('CoursewareSearch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call searchCourseContent', async () => {
|
it('should call searchCourseContent', async () => {
|
||||||
mockModels();
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
const searchKeyword = 'course';
|
const searchKeyword = 'course';
|
||||||
@@ -246,19 +244,23 @@ describe('CoursewareSearch', () => {
|
|||||||
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show a summary for the results', () => {
|
it('should show a summary for the results within a container with aria-live="polite"', () => {
|
||||||
mockModels({
|
mockModels({
|
||||||
searchKeyword: 'fubar',
|
searchKeyword: 'fubar',
|
||||||
total: 1,
|
total: 1,
|
||||||
});
|
});
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
|
const results = screen.queryByTestId('courseware-search-results');
|
||||||
|
|
||||||
|
expect(results).toHaveAttribute('aria-live', 'polite');
|
||||||
|
expect(within(results).queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when clearing the search input', () => {
|
describe('when clearing the search input', () => {
|
||||||
it('should clear the search by dispatch updateModel', async () => {
|
it('should clear the search by dispatch updateModel', async () => {
|
||||||
|
mockSearchParams({ query: 'fubar' });
|
||||||
mockModels({
|
mockModels({
|
||||||
searchKeyword: 'fubar',
|
searchKeyword: 'fubar',
|
||||||
total: 2,
|
total: 2,
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CoursewareSearchEmpty = ({ intl }) => (
|
const CoursewareSearchEmpty = () => {
|
||||||
<div className="courseware-search-results">
|
const intl = useIntl();
|
||||||
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
|
return (
|
||||||
</div>
|
<div className="courseware-search-results">
|
||||||
);
|
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
|
||||||
|
</div>
|
||||||
CoursewareSearchEmpty.propTypes = {
|
);
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CoursewareSearchEmpty);
|
export default CoursewareSearchEmpty;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
screen,
|
screen,
|
||||||
} from '../../setupTest';
|
} from '../../setupTest';
|
||||||
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
function renderComponent() {
|
function renderComponent() {
|
||||||
const { container } = render(<CoursewareSearchEmpty />);
|
const { container } = render(<CoursewareSearchEmpty />);
|
||||||
@@ -16,9 +17,12 @@ describe('CoursewareSearchEmpty', () => {
|
|||||||
initializeMockApp();
|
initializeMockApp();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match the snapshot', () => {
|
it('render empty results text and corresponding classes', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
const emptyText = screen.getByText(messages.searchResultsNone.defaultMessage);
|
||||||
expect(screen.getByTestId('no-results')).toMatchSnapshot();
|
expect(emptyText).toBeInTheDocument();
|
||||||
|
expect(emptyText).toHaveClass('courseware-search-results__empty');
|
||||||
|
expect(emptyText).toHaveAttribute('data-testid', 'no-results');
|
||||||
|
expect(emptyText.parentElement).toHaveClass('courseware-search-results');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SearchField } from '@openedx/paragon';
|
import { SearchField } from '@openedx/paragon';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CoursewareSearchForm = ({
|
const CoursewareSearchForm = ({
|
||||||
intl,
|
|
||||||
searchTerm,
|
searchTerm,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
}) => (
|
}) => {
|
||||||
<SearchField.Advanced
|
const { formatMessage } = useIntl();
|
||||||
value={searchTerm}
|
|
||||||
onSubmit={onSubmit}
|
return (
|
||||||
onChange={onChange}
|
<SearchField.Advanced
|
||||||
submitButtonLocation="external"
|
value={searchTerm}
|
||||||
className="courseware-search-form"
|
onSubmit={onSubmit}
|
||||||
screenReaderText={{
|
onChange={onChange}
|
||||||
label: intl.formatMessage(messages.searchSubmitLabel),
|
|
||||||
clearButton: intl.formatMessage(messages.searchClearAction),
|
|
||||||
submitButton: null, // Remove the sr-only label in the button.
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="pgn__searchfield_wrapper" data-testid="courseware-search-form">
|
|
||||||
<SearchField.Label />
|
|
||||||
<SearchField.Input placeholder={placeholder} autoFocus />
|
|
||||||
<SearchField.ClearButton />
|
|
||||||
</div>
|
|
||||||
<SearchField.SubmitButton
|
|
||||||
buttonText={intl.formatMessage(messages.searchSubmitLabel)}
|
|
||||||
submitButtonLocation="external"
|
submitButtonLocation="external"
|
||||||
data-testid="courseware-search-form-submit"
|
className="courseware-search-form"
|
||||||
/>
|
screenReaderText={{
|
||||||
</SearchField.Advanced>
|
label: formatMessage(messages.searchSubmitLabel),
|
||||||
);
|
clearButton: formatMessage(messages.searchClearAction),
|
||||||
|
submitButton: null, // Remove the sr-only label in the button.
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="pgn__searchfield_wrapper" data-testid="courseware-search-form">
|
||||||
|
<SearchField.Label />
|
||||||
|
<SearchField.Input placeholder={placeholder} autoFocus />
|
||||||
|
<SearchField.ClearButton />
|
||||||
|
</div>
|
||||||
|
<SearchField.SubmitButton
|
||||||
|
buttonText={formatMessage(messages.searchSubmitLabel)}
|
||||||
|
submitButtonLocation="external"
|
||||||
|
data-testid="courseware-search-form-submit"
|
||||||
|
/>
|
||||||
|
</SearchField.Advanced>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
CoursewareSearchForm.propTypes = {
|
CoursewareSearchForm.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
searchTerm: PropTypes.string,
|
searchTerm: PropTypes.string,
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
@@ -51,4 +52,4 @@ CoursewareSearchForm.defaultProps = {
|
|||||||
placeholder: undefined,
|
placeholder: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CoursewareSearchForm);
|
export default CoursewareSearchForm;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import CoursewareSearchResults from './CoursewareSearchResults';
|
import CoursewareSearchResults from './CoursewareSearchResults';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import searchResultsFactory from './test-data/search-results-factory';
|
import searchResultsFactory from './test-data/search-results-factory';
|
||||||
|
import * as mock from './test-data/mocked-response.json';
|
||||||
|
|
||||||
jest.mock('react-redux');
|
jest.mock('react-redux');
|
||||||
|
|
||||||
@@ -34,8 +35,53 @@ describe('CoursewareSearchResults', () => {
|
|||||||
renderComponent({ results });
|
renderComponent({ results });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match the snapshot', () => {
|
it('should render complete list', () => {
|
||||||
expect(screen.getByTestId('search-results')).toMatchSnapshot();
|
const courses = screen.getAllByRole('link');
|
||||||
|
expect(courses.length).toBe(mock.results.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correct link for internal course', () => {
|
||||||
|
const courses = screen.getAllByRole('link');
|
||||||
|
const firstCourse = courses[0];
|
||||||
|
const firstCourseTitle = firstCourse.querySelector('.courseware-search-results__title span');
|
||||||
|
expect(firstCourseTitle.innerHTML).toEqual(mock.results[0].data.content.display_name);
|
||||||
|
expect(firstCourse.href).toContain(mock.results[0].data.url);
|
||||||
|
expect(firstCourse).not.toHaveAttribute('target', '_blank');
|
||||||
|
expect(firstCourse).not.toHaveAttribute('rel', 'nofollow');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correct link if is External url course', () => {
|
||||||
|
const courses = screen.getAllByRole('link');
|
||||||
|
const externalCourse = courses[courses.length - 1];
|
||||||
|
const externalCourseTitle = externalCourse.querySelector('.courseware-search-results__title span');
|
||||||
|
expect(externalCourseTitle.innerHTML).toEqual(mock.results[mock.results.length - 1].data.content.display_name);
|
||||||
|
expect(externalCourse.href).toContain(mock.results[mock.results.length - 1].data.url);
|
||||||
|
expect(externalCourse).toHaveAttribute('target', '_blank');
|
||||||
|
expect(externalCourse).toHaveAttribute('rel', 'nofollow');
|
||||||
|
const icon = externalCourse.querySelector('svg');
|
||||||
|
expect(icon).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render location breadcrumbs', () => {
|
||||||
|
const breadcrumbs = screen.getAllByText(mock.results[0].data.location[0]);
|
||||||
|
expect(breadcrumbs.length).toBeGreaterThan(0);
|
||||||
|
const firstBreadcrumb = breadcrumbs[0].closest('li');
|
||||||
|
expect(firstBreadcrumb).toBeInTheDocument();
|
||||||
|
expect(firstBreadcrumb.querySelector('div').textContent).toBe(mock.results[0].data.location[0]);
|
||||||
|
expect(firstBreadcrumb.nextSibling.querySelector('div').textContent).toBe(mock.results[0].data.location[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when results are provided with content hits', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const { results } = searchResultsFactory('Passing');
|
||||||
|
renderComponent({ results });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render content hits', () => {
|
||||||
|
const contentHits = screen.getByText('1');
|
||||||
|
expect(contentHits).toBeInTheDocument();
|
||||||
|
expect(contentHits.tagName).toBe('EM');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@openedx/paragon';
|
import { Button } from '@openedx/paragon';
|
||||||
import { ManageSearch } from '@openedx/paragon/icons';
|
import { ManageSearch } from '@openedx/paragon/icons';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
@@ -7,9 +7,8 @@ import messages from './messages';
|
|||||||
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
|
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
|
||||||
import { setShowSearch } from '../data/slice';
|
import { setShowSearch } from '../data/slice';
|
||||||
|
|
||||||
const CoursewareSearchToggle = ({
|
const CoursewareSearchToggle = () => {
|
||||||
intl,
|
const intl = useIntl();
|
||||||
}) => {
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const enabled = useCoursewareSearchFeatureFlag();
|
const enabled = useCoursewareSearchFeatureFlag();
|
||||||
const { query } = useCoursewareSearchParams();
|
const { query } = useCoursewareSearchParams();
|
||||||
@@ -41,8 +40,4 @@ const CoursewareSearchToggle = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CoursewareSearchToggle.propTypes = {
|
export default CoursewareSearchToggle;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CoursewareSearchToggle);
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`CoursewareSearchEmpty should match the snapshot 1`] = `
|
|
||||||
<p
|
|
||||||
class="courseware-search-results__empty"
|
|
||||||
data-testid="no-results"
|
|
||||||
>
|
|
||||||
No results found.
|
|
||||||
</p>
|
|
||||||
`;
|
|
||||||
@@ -1,1238 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`CoursewareSearchResults when list of results is provided should match the snapshot 1`] = `
|
|
||||||
<div
|
|
||||||
class="courseware-search-results"
|
|
||||||
data-testid="search-results"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10 4H2v16h20V6H12l-2-2z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Demo Course Overview
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Introduction
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Demo Course Overview
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Passing a Course
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
1
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
About Exams and Certificates
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
edX Exams
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Passing a Course
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10 4H2v16h20V6H12l-2-2z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Passing a Course
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
About Exams and Certificates
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
edX Exams
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Passing a Course
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Text Input
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 1: Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Homework - Question Styles
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Text input
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Pointing on a Picture
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 1: Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Homework - Question Styles
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Pointing on a Picture
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Getting Answers
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
About Exams and Certificates
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
edX Exams
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Getting Answers
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Welcome!
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
30
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Introduction
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Demo Course Overview
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Introduction: Video and Sequences
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Multiple Choice Questions
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 1: Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Homework - Question Styles
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Multiple Choice Questions
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Numerical Input
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 1: Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Homework - Question Styles
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Numerical Input
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Connecting a Circuit and a Circuit Diagram
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
3
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 1: Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Lesson 1 - Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Video Presentation Styles
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
CAPA
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 2: Get Interactive
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Homework - Labs and Demos
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Code Grader
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Interactive Questions
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 1: Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Lesson 1 - Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Interactive Questions
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Blank HTML Page
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
6
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Introduction
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Demo Course Overview
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Introduction: Video and Sequences
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Discussion Forums
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
5
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 3: Be Social
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Lesson 3 - Be Social
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Discussion Forums
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Overall Grade
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
7
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
About Exams and Certificates
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
edX Exams
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Overall Grade Performance
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Blank HTML Page
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
3
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 3: Be Social
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Lesson 3 - Be Social
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Homework - Find Your Study Buddy
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Find Your Study Buddy
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
3
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 3: Be Social
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Homework - Find Your Study Buddy
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Homework - Find Your Study Buddy
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Be Social
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
4
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 3: Be Social
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Lesson 3 - Be Social
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Be Social
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
EdX Exams
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
4
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
About Exams and Certificates
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
edX Exams
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
EdX Exams
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
When Are Your Exams?
|
|
||||||
</span>
|
|
||||||
<em>
|
|
||||||
2
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="courseware-search-results__breadcrumbs"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Example Week 1: Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
Lesson 1 - Getting Started
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
When Are Your Exams?
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="courseware-search-results__item"
|
|
||||||
href="https://www.edx.org"
|
|
||||||
rel="nofollow"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__icon"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="pgn__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
height="24"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="courseware-search-results__title"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
External Course Link Test
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`mapSearchResponse when the response is correct should match snapshot 1`] = `
|
|
||||||
{
|
|
||||||
"filters": [
|
|
||||||
{
|
|
||||||
"count": 7,
|
|
||||||
"key": "capa",
|
|
||||||
"label": "CAPA",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"count": 2,
|
|
||||||
"key": "sequence",
|
|
||||||
"label": "Sequence",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"count": 9,
|
|
||||||
"key": "text",
|
|
||||||
"label": "Text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"count": 1,
|
|
||||||
"key": "unknown",
|
|
||||||
"label": "Unknown",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"count": 2,
|
|
||||||
"key": "video",
|
|
||||||
"label": "Video",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"maxScore": 3.4545178,
|
|
||||||
"ms": 5,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
|
||||||
"location": [
|
|
||||||
"Introduction",
|
|
||||||
"Demo Course Overview",
|
|
||||||
],
|
|
||||||
"score": 3.4545178,
|
|
||||||
"title": "Demo Course Overview",
|
|
||||||
"type": "sequence",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
|
||||||
"location": [
|
|
||||||
"About Exams and Certificates",
|
|
||||||
"edX Exams",
|
|
||||||
"Passing a Course",
|
|
||||||
],
|
|
||||||
"score": 3.4545178,
|
|
||||||
"title": "Passing a Course",
|
|
||||||
"type": "text",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
|
||||||
"location": [
|
|
||||||
"About Exams and Certificates",
|
|
||||||
"edX Exams",
|
|
||||||
"Passing a Course",
|
|
||||||
],
|
|
||||||
"score": 3.4545178,
|
|
||||||
"title": "Passing a Course",
|
|
||||||
"type": "sequence",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
|
||||||
"location": [
|
|
||||||
"Example Week 1: Getting Started",
|
|
||||||
"Homework - Question Styles",
|
|
||||||
"Text input",
|
|
||||||
],
|
|
||||||
"score": 1.5874016,
|
|
||||||
"title": "Text Input",
|
|
||||||
"type": "capa",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
|
||||||
"location": [
|
|
||||||
"Example Week 1: Getting Started",
|
|
||||||
"Homework - Question Styles",
|
|
||||||
"Pointing on a Picture",
|
|
||||||
],
|
|
||||||
"score": 1.5499392,
|
|
||||||
"title": "Pointing on a Picture",
|
|
||||||
"type": "capa",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
|
||||||
"location": [
|
|
||||||
"About Exams and Certificates",
|
|
||||||
"edX Exams",
|
|
||||||
"Getting Answers",
|
|
||||||
],
|
|
||||||
"score": 1.5003732,
|
|
||||||
"title": "Getting Answers",
|
|
||||||
"type": "capa",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
|
||||||
"location": [
|
|
||||||
"Introduction",
|
|
||||||
"Demo Course Overview",
|
|
||||||
"Introduction: Video and Sequences",
|
|
||||||
],
|
|
||||||
"score": 1.4792063,
|
|
||||||
"title": "Welcome!",
|
|
||||||
"type": "video",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
|
||||||
"location": [
|
|
||||||
"Example Week 1: Getting Started",
|
|
||||||
"Homework - Question Styles",
|
|
||||||
"Multiple Choice Questions",
|
|
||||||
],
|
|
||||||
"score": 1.4341705,
|
|
||||||
"title": "Multiple Choice Questions",
|
|
||||||
"type": "capa",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
|
||||||
"location": [
|
|
||||||
"Example Week 1: Getting Started",
|
|
||||||
"Homework - Question Styles",
|
|
||||||
"Numerical Input",
|
|
||||||
],
|
|
||||||
"score": 1.2987298,
|
|
||||||
"title": "Numerical Input",
|
|
||||||
"type": "capa",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
|
||||||
"location": [
|
|
||||||
"Example Week 1: Getting Started",
|
|
||||||
"Lesson 1 - Getting Started",
|
|
||||||
"Video Presentation Styles",
|
|
||||||
],
|
|
||||||
"score": 1.1870136,
|
|
||||||
"title": "Connecting a Circuit and a Circuit Diagram",
|
|
||||||
"type": "video",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
|
||||||
"location": [
|
|
||||||
"Example Week 2: Get Interactive",
|
|
||||||
"Homework - Labs and Demos",
|
|
||||||
"Code Grader",
|
|
||||||
],
|
|
||||||
"score": 1.0107487,
|
|
||||||
"title": "CAPA",
|
|
||||||
"type": "capa",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
|
||||||
"location": [
|
|
||||||
"Example Week 1: Getting Started",
|
|
||||||
"Lesson 1 - Getting Started",
|
|
||||||
"Interactive Questions",
|
|
||||||
],
|
|
||||||
"score": 0.96387196,
|
|
||||||
"title": "Interactive Questions",
|
|
||||||
"type": "capa",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
|
||||||
"location": [
|
|
||||||
"Introduction",
|
|
||||||
"Demo Course Overview",
|
|
||||||
"Introduction: Video and Sequences",
|
|
||||||
],
|
|
||||||
"score": 0.8844358,
|
|
||||||
"title": "Blank HTML Page",
|
|
||||||
"type": "text",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
|
||||||
"location": [
|
|
||||||
"Example Week 3: Be Social",
|
|
||||||
"Lesson 3 - Be Social",
|
|
||||||
"Discussion Forums",
|
|
||||||
],
|
|
||||||
"score": 0.8803684,
|
|
||||||
"title": "Discussion Forums",
|
|
||||||
"type": "text",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
|
||||||
"location": [
|
|
||||||
"About Exams and Certificates",
|
|
||||||
"edX Exams",
|
|
||||||
"Overall Grade Performance",
|
|
||||||
],
|
|
||||||
"score": 0.87981963,
|
|
||||||
"title": "Overall Grade",
|
|
||||||
"type": "text",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
|
||||||
"location": [
|
|
||||||
"Example Week 3: Be Social",
|
|
||||||
"Lesson 3 - Be Social",
|
|
||||||
"Homework - Find Your Study Buddy",
|
|
||||||
],
|
|
||||||
"score": 0.84284115,
|
|
||||||
"title": "Blank HTML Page",
|
|
||||||
"type": "text",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
|
||||||
"location": [
|
|
||||||
"Example Week 3: Be Social",
|
|
||||||
"Homework - Find Your Study Buddy",
|
|
||||||
"Homework - Find Your Study Buddy",
|
|
||||||
],
|
|
||||||
"score": 0.84284115,
|
|
||||||
"title": "Find Your Study Buddy",
|
|
||||||
"type": "text",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
|
||||||
"location": [
|
|
||||||
"Example Week 3: Be Social",
|
|
||||||
"Lesson 3 - Be Social",
|
|
||||||
"Be Social",
|
|
||||||
],
|
|
||||||
"score": 0.84210813,
|
|
||||||
"title": "Be Social",
|
|
||||||
"type": "text",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
|
||||||
"location": [
|
|
||||||
"About Exams and Certificates",
|
|
||||||
"edX Exams",
|
|
||||||
"EdX Exams",
|
|
||||||
],
|
|
||||||
"score": 0.8306555,
|
|
||||||
"title": "EdX Exams",
|
|
||||||
"type": "text",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
|
||||||
"location": [
|
|
||||||
"Example Week 1: Getting Started",
|
|
||||||
"Lesson 1 - Getting Started",
|
|
||||||
"When Are Your Exams? ",
|
|
||||||
],
|
|
||||||
"score": 0.82610154,
|
|
||||||
"title": "When Are Your Exams? ",
|
|
||||||
"type": "text",
|
|
||||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"contentHits": 0,
|
|
||||||
"id": "random-element-id",
|
|
||||||
"location": null,
|
|
||||||
"score": 0.82610154,
|
|
||||||
"title": "External Course Link Test",
|
|
||||||
"type": "unknown",
|
|
||||||
"url": "https://www.edx.org",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"total": 29,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -5,13 +5,25 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
border-top: 1px solid $light-300;
|
width: 100%;
|
||||||
z-index: $zindex-modal; // Bootstrap's z-index layer for Modals.
|
height: 100%;
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
border-top: 1px solid var(--pgn-color-light-300);
|
||||||
|
z-index: var(--pgn-elevation-modal-zindex); // Bootstrap's z-index layer for Modals.
|
||||||
|
|
||||||
|
&__form {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.h2 {
|
||||||
|
margin-right: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__close {
|
&__close {
|
||||||
position: absolute !important; // For some reason it gets overridden
|
position: absolute !important; // For some reason it gets overridden
|
||||||
top: 0.5rem;
|
top: 0;
|
||||||
right: 1rem;
|
right: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -35,7 +47,7 @@
|
|||||||
|
|
||||||
&__results-summary {
|
&__results-summary {
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
color: $gray-500;
|
color: var(--pgn-color-gray-500);
|
||||||
padding: 1rem 0 .5rem;
|
padding: 1rem 0 .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +62,7 @@
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
&__empty {
|
&__empty {
|
||||||
color: $gray-500;
|
color: var(--pgn-color-gray-500);
|
||||||
padding: 6rem 0;
|
padding: 6rem 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -64,17 +76,17 @@
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: $light-300;
|
background: var(--pgn-color-light-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
border-top: 1px solid $light-300;
|
border-top: 1px solid var(--pgn-color-light-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
padding: 0.375rem 0 0 0.375rem;
|
padding: 0.375rem 0 0 0.375rem;
|
||||||
color: $gray-300;
|
color: var(--pgn-color-gray-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
@@ -87,7 +99,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 2.5;
|
line-height: 2.5;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: $black;
|
color: var(--pgn-color-black);
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -101,7 +113,7 @@
|
|||||||
font-variant-numeric: lining-nums tabular-nums;
|
font-variant-numeric: lining-nums tabular-nums;
|
||||||
min-width: 1.25rem;
|
min-width: 1.25rem;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
background: $light-300;
|
background: var(--pgn-color-light-300);
|
||||||
border-radius: 99rem;
|
border-radius: 99rem;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
margin-left: 0.375rem;
|
margin-left: 0.375rem;
|
||||||
@@ -113,7 +125,7 @@
|
|||||||
&__breadcrumbs {
|
&__breadcrumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
color: $gray-500;
|
color: var(--pgn-color-gray-500);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -144,17 +156,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.courseware-search-results-tabs {
|
.courseware-search-results-tabs {
|
||||||
border-bottom-color: $gray-400 !important;
|
border-bottom-color: var(--pgn-color-gray-400) !important;
|
||||||
|
|
||||||
&.nav-tabs .nav-link.active {
|
&.nav-tabs .nav-link.active {
|
||||||
border-bottom-width: 4px !important;
|
border-bottom-width: 4px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: map-get($grid-breakpoints, 'md')) {
|
@media (--pgn-size-breakpoint-min-width-md) {
|
||||||
.courseware-search__content {
|
.courseware-search {
|
||||||
padding-top: 8rem;
|
&__close {
|
||||||
|
right: -2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
padding-top: 8rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body._search-no-scroll {
|
body._search-no-scroll {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { renderHook, act } from '@testing-library/react-hooks';
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { fetchCoursewareSearchSettings } from '../data/thunks';
|
import { fetchCoursewareSearchSettings } from '../data/thunks';
|
||||||
@@ -38,13 +38,13 @@ describe('CoursewareSearch Hooks', () => {
|
|||||||
|
|
||||||
it('should return true if feature is enabled', async () => {
|
it('should return true if feature is enabled', async () => {
|
||||||
const hook = await renderTestHook();
|
const hook = await renderTestHook();
|
||||||
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||||
expect(hook.result.current).toBe(true);
|
expect(hook.result.current).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if feature is disabled', async () => {
|
it('should return false if feature is disabled', async () => {
|
||||||
const hook = await renderTestHook(false);
|
const hook = await renderTestHook(false);
|
||||||
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||||
expect(hook.result.current).toBe(false);
|
expect(hook.result.current).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -125,7 +125,7 @@ describe('CoursewareSearch Hooks', () => {
|
|||||||
it('should return the element bounding box', async () => {
|
it('should return the element bounding box', async () => {
|
||||||
const hook = await renderTestHook({ elementId: 'test', mockedInfo });
|
const hook = await renderTestHook({ elementId: 'test', mockedInfo });
|
||||||
|
|
||||||
hook.waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
|
await waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
|
||||||
|
|
||||||
expect(hook.result.current).toEqual(mockedInfo);
|
expect(hook.result.current).toEqual(mockedInfo);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ describe('mapSearchResponse', () => {
|
|||||||
response = mapSearchResponse(camelCaseObject(mockedResponse));
|
response = mapSearchResponse(camelCaseObject(mockedResponse));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match snapshot', () => {
|
it('should match number of results', () => {
|
||||||
expect(response).toMatchSnapshot();
|
expect(response.results.length).toBe(mockedResponse.results.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match expected filters', () => {
|
it('should match expected filters', () => {
|
||||||
@@ -24,6 +24,25 @@ describe('mapSearchResponse', () => {
|
|||||||
];
|
];
|
||||||
expect(response.filters).toEqual(expectedFilters);
|
expect(response.filters).toEqual(expectedFilters);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should match expected results', () => {
|
||||||
|
const mockFirstResult = mockedResponse.results[0];
|
||||||
|
const expectedFirstResult = {
|
||||||
|
id: mockFirstResult.data.id,
|
||||||
|
title: mockFirstResult.data.content.display_name,
|
||||||
|
type: mockFirstResult.data.content_type.toLowerCase(),
|
||||||
|
location: mockFirstResult.data.location,
|
||||||
|
url: mockFirstResult.data.url,
|
||||||
|
contentHits: 0,
|
||||||
|
score: mockFirstResult.score,
|
||||||
|
};
|
||||||
|
expect(response.results[0]).toEqual(expectedFirstResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match expected ms and max score', () => {
|
||||||
|
expect(response.maxScore).toBe(mockedResponse.max_score);
|
||||||
|
expect(response.ms).toBe(mockedResponse.took);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the a keyword is provided', () => {
|
describe('when the a keyword is provided', () => {
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ Factory.define('outlineTabData')
|
|||||||
course_access_redirect: false,
|
course_access_redirect: false,
|
||||||
has_scheduled_content: null,
|
has_scheduled_content: null,
|
||||||
access_expiration: null,
|
access_expiration: null,
|
||||||
can_show_upgrade_sock: false,
|
|
||||||
cert_data: {
|
cert_data: {
|
||||||
cert_status: null,
|
cert_status: null,
|
||||||
cert_web_view_url: null,
|
cert_web_view_url: null,
|
||||||
|
|||||||
@@ -17,7 +17,21 @@ Factory.define('progressTabData')
|
|||||||
percent: 1,
|
percent: 1,
|
||||||
is_passing: true,
|
is_passing: true,
|
||||||
},
|
},
|
||||||
|
final_grades: 0.5,
|
||||||
credit_course_requirements: null,
|
credit_course_requirements: null,
|
||||||
|
assignment_type_grade_summary: [
|
||||||
|
{
|
||||||
|
type: 'Homework',
|
||||||
|
short_label: 'HW',
|
||||||
|
weight: 1,
|
||||||
|
average_grade: 1,
|
||||||
|
weighted_grade: 1,
|
||||||
|
num_droppable: 1,
|
||||||
|
num_total: 2,
|
||||||
|
has_hidden_contribution: 'none',
|
||||||
|
last_grade_publish_date: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
section_scores: [
|
section_scores: [
|
||||||
{
|
{
|
||||||
display_name: 'First section',
|
display_name: 'First section',
|
||||||
|
|||||||
@@ -1,942 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
|
|
||||||
{
|
|
||||||
"courseHome": {
|
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"courseStatus": "loaded",
|
|
||||||
"proctoringPanelStatus": "loading",
|
|
||||||
"showSearch": false,
|
|
||||||
"targetUserId": undefined,
|
|
||||||
"toastBodyLink": null,
|
|
||||||
"toastBodyText": null,
|
|
||||||
"toastHeader": "",
|
|
||||||
},
|
|
||||||
"courseware": {
|
|
||||||
"courseId": null,
|
|
||||||
"courseOutline": {},
|
|
||||||
"courseOutlineShouldUpdate": false,
|
|
||||||
"courseOutlineStatus": "loading",
|
|
||||||
"courseStatus": "loading",
|
|
||||||
"coursewareOutlineSidebarSettings": {},
|
|
||||||
"sequenceId": null,
|
|
||||||
"sequenceMightBeUnit": false,
|
|
||||||
"sequenceStatus": "loading",
|
|
||||||
},
|
|
||||||
"learningAssistant": ObjectContaining {
|
|
||||||
"conversationId": Any<String>,
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"courseHomeMeta": {
|
|
||||||
"course-v1:edX+DemoX+Demo_Course": {
|
|
||||||
"canViewCertificate": true,
|
|
||||||
"celebrations": null,
|
|
||||||
"courseAccess": {
|
|
||||||
"additionalContextUserMessage": null,
|
|
||||||
"developerMessage": null,
|
|
||||||
"errorCode": null,
|
|
||||||
"hasAccess": true,
|
|
||||||
"userFragment": null,
|
|
||||||
"userMessage": null,
|
|
||||||
},
|
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"isEnrolled": false,
|
|
||||||
"isMasquerading": false,
|
|
||||||
"isNewDiscussionSidebarViewEnabled": false,
|
|
||||||
"isSelfPaced": false,
|
|
||||||
"isStaff": false,
|
|
||||||
"number": "DemoX",
|
|
||||||
"org": "edX",
|
|
||||||
"originalUserIsStaff": false,
|
|
||||||
"start": "2013-02-05T05:00:00Z",
|
|
||||||
"tabs": [
|
|
||||||
{
|
|
||||||
"slug": "outline",
|
|
||||||
"title": "Course",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "discussion",
|
|
||||||
"title": "Discussion",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "wiki",
|
|
||||||
"title": "Wiki",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "progress",
|
|
||||||
"title": "Progress",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "instructor",
|
|
||||||
"title": "Instructor",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "dates",
|
|
||||||
"title": "Dates",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"title": "Demonstration Course",
|
|
||||||
"userTimezone": "UTC",
|
|
||||||
"username": "MockUser",
|
|
||||||
"verifiedMode": {
|
|
||||||
"accessExpirationDate": null,
|
|
||||||
"currency": "USD",
|
|
||||||
"currencySymbol": "$",
|
|
||||||
"price": 149,
|
|
||||||
"sku": "8CF08E5",
|
|
||||||
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"dates": {
|
|
||||||
"course-v1:edX+DemoX+Demo_Course": {
|
|
||||||
"courseDateBlocks": [
|
|
||||||
{
|
|
||||||
"date": "2020-05-01T17:59:41Z",
|
|
||||||
"dateType": "course-start-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "",
|
|
||||||
"title": "Course Starts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"complete": true,
|
|
||||||
"date": "2020-05-04T02:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"title": "Multi Badges Completed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2020-05-05T02:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"title": "Multi Badges Past Due",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2020-05-27T02:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "Both Past Due 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2020-05-27T02:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "Both Past Due 2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"complete": true,
|
|
||||||
"date": "2020-05-28T08:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "One Completed/Due 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2020-05-28T08:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "One Completed/Due 2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"complete": true,
|
|
||||||
"date": "2020-05-29T08:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "Both Completed 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"complete": true,
|
|
||||||
"date": "2020-05-29T08:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "Both Completed 2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2020-06-16T17:59:40.942669Z",
|
|
||||||
"dateType": "verified-upgrade-deadline",
|
|
||||||
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "Upgrade to Verified Certificate",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2030-08-17T05:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": false,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "One Verified 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2030-08-17T05:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "One Verified 2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2030-08-17T05:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": "ORA Dates are set by the instructor, and can't be changed",
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "ORA Verified 2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2030-08-18T05:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": false,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "Both Verified 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2030-08-18T05:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": false,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "Both Verified 2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2030-08-19T05:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"title": "One Unreleased 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2030-08-19T05:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "One Unreleased 2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2030-08-20T05:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"title": "Both Unreleased 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"date": "2030-08-20T05:59:40.942669Z",
|
|
||||||
"dateType": "assignment-due-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"title": "Both Unreleased 2",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2030-08-23T00:00:00Z",
|
|
||||||
"dateType": "course-end-date",
|
|
||||||
"description": "",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"link": "",
|
|
||||||
"title": "Course Ends",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"date": "2030-09-01T00:00:00Z",
|
|
||||||
"dateType": "verification-deadline-date",
|
|
||||||
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
|
|
||||||
"extraInfo": null,
|
|
||||||
"learnerHasAccess": false,
|
|
||||||
"link": "https://example.com/",
|
|
||||||
"title": "Verification Deadline",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"datesBannerInfo": {
|
|
||||||
"contentTypeGatingEnabled": false,
|
|
||||||
"missedDeadlines": false,
|
|
||||||
"missedGatedContent": false,
|
|
||||||
"verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
|
||||||
},
|
|
||||||
"hasEnded": false,
|
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"learnerIsFullAccess": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"plugins": {},
|
|
||||||
"recommendations": {
|
|
||||||
"recommendationsStatus": "loading",
|
|
||||||
},
|
|
||||||
"specialExams": {
|
|
||||||
"activeAttempt": null,
|
|
||||||
"allowProctoringOptOut": false,
|
|
||||||
"apiErrorMsg": "",
|
|
||||||
"exam": {
|
|
||||||
"attempt": {
|
|
||||||
"attempt_code": "",
|
|
||||||
"attempt_id": null,
|
|
||||||
"attempt_status": "",
|
|
||||||
"course_id": "",
|
|
||||||
"desktop_application_js_url": "",
|
|
||||||
"exam_display_name": "",
|
|
||||||
"exam_started_poll_url": "",
|
|
||||||
"exam_type": "",
|
|
||||||
"exam_url_path": "",
|
|
||||||
"external_id": "",
|
|
||||||
"in_timed_exam": true,
|
|
||||||
"ping_interval": null,
|
|
||||||
"taking_as_proctored": true,
|
|
||||||
"time_remaining_seconds": null,
|
|
||||||
"use_legacy_attempt_api": true,
|
|
||||||
},
|
|
||||||
"backend": "",
|
|
||||||
"content_id": "",
|
|
||||||
"course_id": "",
|
|
||||||
"due_date": null,
|
|
||||||
"exam_name": "",
|
|
||||||
"external_id": "",
|
|
||||||
"hide_after_due": false,
|
|
||||||
"id": null,
|
|
||||||
"is_active": true,
|
|
||||||
"is_practice_exam": false,
|
|
||||||
"is_proctored": false,
|
|
||||||
"prerequisite_status": {
|
|
||||||
"are_prerequisites_satisifed": true,
|
|
||||||
"declined_prerequisites": [],
|
|
||||||
"failed_prerequisites": [],
|
|
||||||
"pending_prerequisites": [],
|
|
||||||
"satisfied_prerequisites": [],
|
|
||||||
},
|
|
||||||
"time_limit_mins": null,
|
|
||||||
"type": "",
|
|
||||||
},
|
|
||||||
"examAccessToken": {
|
|
||||||
"exam_access_token": "",
|
|
||||||
"exam_access_token_expiration": "",
|
|
||||||
},
|
|
||||||
"isLoading": true,
|
|
||||||
"proctoringSettings": {
|
|
||||||
"exam_proctoring_backend": {
|
|
||||||
"download_url": "",
|
|
||||||
"instructions": [],
|
|
||||||
"name": "",
|
|
||||||
"rules": {},
|
|
||||||
},
|
|
||||||
"integration_specific_email": "",
|
|
||||||
"learner_notification_from_email": "",
|
|
||||||
"provider_name": "",
|
|
||||||
"provider_tech_support_email": "",
|
|
||||||
"provider_tech_support_phone": "",
|
|
||||||
"provider_tech_support_url": "",
|
|
||||||
},
|
|
||||||
"timeIsOver": false,
|
|
||||||
},
|
|
||||||
"tours": {
|
|
||||||
"showCoursewareTour": false,
|
|
||||||
"showExistingUserCourseHomeTour": false,
|
|
||||||
"showNewUserCourseHomeModal": false,
|
|
||||||
"showNewUserCourseHomeTour": false,
|
|
||||||
"toursEnabled": false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
|
|
||||||
{
|
|
||||||
"courseHome": {
|
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"courseStatus": "loaded",
|
|
||||||
"proctoringPanelStatus": "loading",
|
|
||||||
"showSearch": false,
|
|
||||||
"targetUserId": undefined,
|
|
||||||
"toastBodyLink": null,
|
|
||||||
"toastBodyText": null,
|
|
||||||
"toastHeader": "",
|
|
||||||
},
|
|
||||||
"courseware": {
|
|
||||||
"courseId": null,
|
|
||||||
"courseOutline": {},
|
|
||||||
"courseOutlineShouldUpdate": false,
|
|
||||||
"courseOutlineStatus": "loading",
|
|
||||||
"courseStatus": "loading",
|
|
||||||
"coursewareOutlineSidebarSettings": {},
|
|
||||||
"sequenceId": null,
|
|
||||||
"sequenceMightBeUnit": false,
|
|
||||||
"sequenceStatus": "loading",
|
|
||||||
},
|
|
||||||
"learningAssistant": ObjectContaining {
|
|
||||||
"conversationId": Any<String>,
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"courseHomeMeta": {
|
|
||||||
"course-v1:edX+DemoX+Demo_Course": {
|
|
||||||
"canViewCertificate": true,
|
|
||||||
"celebrations": null,
|
|
||||||
"courseAccess": {
|
|
||||||
"additionalContextUserMessage": null,
|
|
||||||
"developerMessage": null,
|
|
||||||
"errorCode": null,
|
|
||||||
"hasAccess": true,
|
|
||||||
"userFragment": null,
|
|
||||||
"userMessage": null,
|
|
||||||
},
|
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"isEnrolled": false,
|
|
||||||
"isMasquerading": false,
|
|
||||||
"isNewDiscussionSidebarViewEnabled": false,
|
|
||||||
"isSelfPaced": false,
|
|
||||||
"isStaff": false,
|
|
||||||
"number": "DemoX",
|
|
||||||
"org": "edX",
|
|
||||||
"originalUserIsStaff": false,
|
|
||||||
"start": "2013-02-05T05:00:00Z",
|
|
||||||
"tabs": [
|
|
||||||
{
|
|
||||||
"slug": "outline",
|
|
||||||
"title": "Course",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "discussion",
|
|
||||||
"title": "Discussion",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "wiki",
|
|
||||||
"title": "Wiki",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "progress",
|
|
||||||
"title": "Progress",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "instructor",
|
|
||||||
"title": "Instructor",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "dates",
|
|
||||||
"title": "Dates",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"title": "Demonstration Course",
|
|
||||||
"userTimezone": "UTC",
|
|
||||||
"username": "MockUser",
|
|
||||||
"verifiedMode": {
|
|
||||||
"accessExpirationDate": null,
|
|
||||||
"currency": "USD",
|
|
||||||
"currencySymbol": "$",
|
|
||||||
"price": 149,
|
|
||||||
"sku": "8CF08E5",
|
|
||||||
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"outline": {
|
|
||||||
"course-v1:edX+DemoX+Demo_Course": {
|
|
||||||
"accessExpiration": null,
|
|
||||||
"canShowUpgradeSock": false,
|
|
||||||
"certData": {
|
|
||||||
"certStatus": null,
|
|
||||||
"certWebViewUrl": null,
|
|
||||||
"certificateAvailableDate": null,
|
|
||||||
},
|
|
||||||
"courseBlocks": {
|
|
||||||
"courses": {
|
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": {
|
|
||||||
"hasScheduledContent": false,
|
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"sectionIds": [
|
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
|
||||||
],
|
|
||||||
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"sections": {
|
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": {
|
|
||||||
"complete": false,
|
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"hideFromTOC": undefined,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
|
||||||
"resumeBlock": false,
|
|
||||||
"sequenceIds": [
|
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
|
||||||
],
|
|
||||||
"title": "Title of Section",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"sequences": {
|
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": {
|
|
||||||
"complete": false,
|
|
||||||
"description": null,
|
|
||||||
"due": null,
|
|
||||||
"effortActivities": 2,
|
|
||||||
"effortTime": 15,
|
|
||||||
"hideFromTOC": undefined,
|
|
||||||
"icon": null,
|
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
|
||||||
"navigationDisabled": undefined,
|
|
||||||
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
|
||||||
"showLink": true,
|
|
||||||
"title": "Title of Sequence",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"courseGoals": {
|
|
||||||
"daysPerWeek": null,
|
|
||||||
"goalOptions": [],
|
|
||||||
"selectedGoal": null,
|
|
||||||
"subscribedToReminders": null,
|
|
||||||
"weeklyLearningGoalEnabled": false,
|
|
||||||
},
|
|
||||||
"courseTools": [
|
|
||||||
{
|
|
||||||
"analyticsId": "edx.bookmarks",
|
|
||||||
"title": "Bookmarks",
|
|
||||||
"url": "https://example.com/bookmarks",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"datesBannerInfo": {
|
|
||||||
"contentTypeGatingEnabled": false,
|
|
||||||
"missedDeadlines": false,
|
|
||||||
"missedGatedContent": false,
|
|
||||||
},
|
|
||||||
"datesWidget": {
|
|
||||||
"courseDateBlocks": [],
|
|
||||||
},
|
|
||||||
"enableProctoredExams": undefined,
|
|
||||||
"enrollAlert": {
|
|
||||||
"canEnroll": true,
|
|
||||||
"extraText": "Contact the administrator.",
|
|
||||||
},
|
|
||||||
"enrollmentMode": undefined,
|
|
||||||
"handoutsHtml": "<ul><li>Handout 1</li></ul>",
|
|
||||||
"hasEnded": undefined,
|
|
||||||
"hasScheduledContent": null,
|
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"offer": null,
|
|
||||||
"resumeCourse": {
|
|
||||||
"hasVisitedCourse": false,
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
|
||||||
},
|
|
||||||
"timeOffsetMillis": 0,
|
|
||||||
"userHasPassingGrade": undefined,
|
|
||||||
"verifiedMode": {
|
|
||||||
"accessExpirationDate": "2050-01-01T12:00:00",
|
|
||||||
"currency": "USD",
|
|
||||||
"currencySymbol": "$",
|
|
||||||
"price": 149,
|
|
||||||
"sku": "ABCD1234",
|
|
||||||
"upgradeUrl": "http://localhost:18000/dashboard",
|
|
||||||
},
|
|
||||||
"welcomeMessageHtml": "<p>Welcome to this course!</p>",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"plugins": {},
|
|
||||||
"recommendations": {
|
|
||||||
"recommendationsStatus": "loading",
|
|
||||||
},
|
|
||||||
"specialExams": {
|
|
||||||
"activeAttempt": null,
|
|
||||||
"allowProctoringOptOut": false,
|
|
||||||
"apiErrorMsg": "",
|
|
||||||
"exam": {
|
|
||||||
"attempt": {
|
|
||||||
"attempt_code": "",
|
|
||||||
"attempt_id": null,
|
|
||||||
"attempt_status": "",
|
|
||||||
"course_id": "",
|
|
||||||
"desktop_application_js_url": "",
|
|
||||||
"exam_display_name": "",
|
|
||||||
"exam_started_poll_url": "",
|
|
||||||
"exam_type": "",
|
|
||||||
"exam_url_path": "",
|
|
||||||
"external_id": "",
|
|
||||||
"in_timed_exam": true,
|
|
||||||
"ping_interval": null,
|
|
||||||
"taking_as_proctored": true,
|
|
||||||
"time_remaining_seconds": null,
|
|
||||||
"use_legacy_attempt_api": true,
|
|
||||||
},
|
|
||||||
"backend": "",
|
|
||||||
"content_id": "",
|
|
||||||
"course_id": "",
|
|
||||||
"due_date": null,
|
|
||||||
"exam_name": "",
|
|
||||||
"external_id": "",
|
|
||||||
"hide_after_due": false,
|
|
||||||
"id": null,
|
|
||||||
"is_active": true,
|
|
||||||
"is_practice_exam": false,
|
|
||||||
"is_proctored": false,
|
|
||||||
"prerequisite_status": {
|
|
||||||
"are_prerequisites_satisifed": true,
|
|
||||||
"declined_prerequisites": [],
|
|
||||||
"failed_prerequisites": [],
|
|
||||||
"pending_prerequisites": [],
|
|
||||||
"satisfied_prerequisites": [],
|
|
||||||
},
|
|
||||||
"time_limit_mins": null,
|
|
||||||
"type": "",
|
|
||||||
},
|
|
||||||
"examAccessToken": {
|
|
||||||
"exam_access_token": "",
|
|
||||||
"exam_access_token_expiration": "",
|
|
||||||
},
|
|
||||||
"isLoading": true,
|
|
||||||
"proctoringSettings": {
|
|
||||||
"exam_proctoring_backend": {
|
|
||||||
"download_url": "",
|
|
||||||
"instructions": [],
|
|
||||||
"name": "",
|
|
||||||
"rules": {},
|
|
||||||
},
|
|
||||||
"integration_specific_email": "",
|
|
||||||
"learner_notification_from_email": "",
|
|
||||||
"provider_name": "",
|
|
||||||
"provider_tech_support_email": "",
|
|
||||||
"provider_tech_support_phone": "",
|
|
||||||
"provider_tech_support_url": "",
|
|
||||||
},
|
|
||||||
"timeIsOver": false,
|
|
||||||
},
|
|
||||||
"tours": {
|
|
||||||
"showCoursewareTour": false,
|
|
||||||
"showExistingUserCourseHomeTour": false,
|
|
||||||
"showNewUserCourseHomeModal": false,
|
|
||||||
"showNewUserCourseHomeTour": false,
|
|
||||||
"toursEnabled": false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
|
|
||||||
{
|
|
||||||
"courseHome": {
|
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"courseStatus": "loaded",
|
|
||||||
"proctoringPanelStatus": "loading",
|
|
||||||
"showSearch": false,
|
|
||||||
"targetUserId": undefined,
|
|
||||||
"toastBodyLink": null,
|
|
||||||
"toastBodyText": null,
|
|
||||||
"toastHeader": "",
|
|
||||||
},
|
|
||||||
"courseware": {
|
|
||||||
"courseId": null,
|
|
||||||
"courseOutline": {},
|
|
||||||
"courseOutlineShouldUpdate": false,
|
|
||||||
"courseOutlineStatus": "loading",
|
|
||||||
"courseStatus": "loading",
|
|
||||||
"coursewareOutlineSidebarSettings": {},
|
|
||||||
"sequenceId": null,
|
|
||||||
"sequenceMightBeUnit": false,
|
|
||||||
"sequenceStatus": "loading",
|
|
||||||
},
|
|
||||||
"learningAssistant": ObjectContaining {
|
|
||||||
"conversationId": Any<String>,
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"courseHomeMeta": {
|
|
||||||
"course-v1:edX+DemoX+Demo_Course": {
|
|
||||||
"canViewCertificate": true,
|
|
||||||
"celebrations": null,
|
|
||||||
"courseAccess": {
|
|
||||||
"additionalContextUserMessage": null,
|
|
||||||
"developerMessage": null,
|
|
||||||
"errorCode": null,
|
|
||||||
"hasAccess": true,
|
|
||||||
"userFragment": null,
|
|
||||||
"userMessage": null,
|
|
||||||
},
|
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"isEnrolled": false,
|
|
||||||
"isMasquerading": false,
|
|
||||||
"isNewDiscussionSidebarViewEnabled": false,
|
|
||||||
"isSelfPaced": false,
|
|
||||||
"isStaff": false,
|
|
||||||
"number": "DemoX",
|
|
||||||
"org": "edX",
|
|
||||||
"originalUserIsStaff": false,
|
|
||||||
"start": "2013-02-05T05:00:00Z",
|
|
||||||
"tabs": [
|
|
||||||
{
|
|
||||||
"slug": "outline",
|
|
||||||
"title": "Course",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "discussion",
|
|
||||||
"title": "Discussion",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "wiki",
|
|
||||||
"title": "Wiki",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "progress",
|
|
||||||
"title": "Progress",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "instructor",
|
|
||||||
"title": "Instructor",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "dates",
|
|
||||||
"title": "Dates",
|
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"title": "Demonstration Course",
|
|
||||||
"userTimezone": "UTC",
|
|
||||||
"username": "MockUser",
|
|
||||||
"verifiedMode": {
|
|
||||||
"accessExpirationDate": null,
|
|
||||||
"currency": "USD",
|
|
||||||
"currencySymbol": "$",
|
|
||||||
"price": 149,
|
|
||||||
"sku": "8CF08E5",
|
|
||||||
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"progress": {
|
|
||||||
"course-v1:edX+DemoX+Demo_Course": {
|
|
||||||
"accessExpiration": null,
|
|
||||||
"certificateData": {},
|
|
||||||
"completionSummary": {
|
|
||||||
"completeCount": 1,
|
|
||||||
"incompleteCount": 1,
|
|
||||||
"lockedCount": 0,
|
|
||||||
},
|
|
||||||
"courseGrade": {
|
|
||||||
"isPassing": true,
|
|
||||||
"letterGrade": "pass",
|
|
||||||
"percent": 1,
|
|
||||||
},
|
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"creditCourseRequirements": null,
|
|
||||||
"end": "3027-03-31T00:00:00Z",
|
|
||||||
"enrollmentMode": "audit",
|
|
||||||
"gradesFeatureIsFullyLocked": false,
|
|
||||||
"gradesFeatureIsPartiallyLocked": false,
|
|
||||||
"gradingPolicy": {
|
|
||||||
"assignmentPolicies": [
|
|
||||||
{
|
|
||||||
"averageGrade": "1.0000",
|
|
||||||
"numDroppable": 1,
|
|
||||||
"shortLabel": "HW",
|
|
||||||
"type": "Homework",
|
|
||||||
"weight": 1,
|
|
||||||
"weightedGrade": 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"gradeRange": {
|
|
||||||
"pass": 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"hasScheduledContent": false,
|
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
|
||||||
"sectionScores": [
|
|
||||||
{
|
|
||||||
"displayName": "First section",
|
|
||||||
"subsections": [
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
|
|
||||||
"displayName": "First subsection",
|
|
||||||
"hasGradedAssignment": true,
|
|
||||||
"learnerHasAccess": true,
|
|
||||||
"numPointsEarned": 0,
|
|
||||||
"numPointsPossible": 3,
|
|
||||||
"percentGraded": 0,
|
|
||||||
"problemScores": [
|
|
||||||
{
|
|
||||||
"earned": 0,
|
|
||||||
"possible": 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"earned": 0,
|
|
||||||
"possible": 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"earned": 0,
|
|
||||||
"possible": 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"showCorrectness": "always",
|
|
||||||
"showGrades": true,
|
|
||||||
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"displayName": "Second section",
|
|
||||||
"subsections": [
|
|
||||||
{
|
|
||||||
"assignmentType": "Homework",
|
|
||||||
"displayName": "Second subsection",
|
|
||||||
"hasGradedAssignment": true,
|
|
||||||
"numPointsEarned": 1,
|
|
||||||
"numPointsPossible": 1,
|
|
||||||
"percentGraded": 1,
|
|
||||||
"problemScores": [
|
|
||||||
{
|
|
||||||
"earned": 1,
|
|
||||||
"possible": 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"showCorrectness": "always",
|
|
||||||
"showGrades": true,
|
|
||||||
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
|
|
||||||
"userHasPassingGrade": false,
|
|
||||||
"verificationData": {
|
|
||||||
"link": null,
|
|
||||||
"status": "none",
|
|
||||||
"statusDate": null,
|
|
||||||
},
|
|
||||||
"verifiedMode": null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"plugins": {},
|
|
||||||
"recommendations": {
|
|
||||||
"recommendationsStatus": "loading",
|
|
||||||
},
|
|
||||||
"specialExams": {
|
|
||||||
"activeAttempt": null,
|
|
||||||
"allowProctoringOptOut": false,
|
|
||||||
"apiErrorMsg": "",
|
|
||||||
"exam": {
|
|
||||||
"attempt": {
|
|
||||||
"attempt_code": "",
|
|
||||||
"attempt_id": null,
|
|
||||||
"attempt_status": "",
|
|
||||||
"course_id": "",
|
|
||||||
"desktop_application_js_url": "",
|
|
||||||
"exam_display_name": "",
|
|
||||||
"exam_started_poll_url": "",
|
|
||||||
"exam_type": "",
|
|
||||||
"exam_url_path": "",
|
|
||||||
"external_id": "",
|
|
||||||
"in_timed_exam": true,
|
|
||||||
"ping_interval": null,
|
|
||||||
"taking_as_proctored": true,
|
|
||||||
"time_remaining_seconds": null,
|
|
||||||
"use_legacy_attempt_api": true,
|
|
||||||
},
|
|
||||||
"backend": "",
|
|
||||||
"content_id": "",
|
|
||||||
"course_id": "",
|
|
||||||
"due_date": null,
|
|
||||||
"exam_name": "",
|
|
||||||
"external_id": "",
|
|
||||||
"hide_after_due": false,
|
|
||||||
"id": null,
|
|
||||||
"is_active": true,
|
|
||||||
"is_practice_exam": false,
|
|
||||||
"is_proctored": false,
|
|
||||||
"prerequisite_status": {
|
|
||||||
"are_prerequisites_satisifed": true,
|
|
||||||
"declined_prerequisites": [],
|
|
||||||
"failed_prerequisites": [],
|
|
||||||
"pending_prerequisites": [],
|
|
||||||
"satisfied_prerequisites": [],
|
|
||||||
},
|
|
||||||
"time_limit_mins": null,
|
|
||||||
"type": "",
|
|
||||||
},
|
|
||||||
"examAccessToken": {
|
|
||||||
"exam_access_token": "",
|
|
||||||
"exam_access_token_expiration": "",
|
|
||||||
},
|
|
||||||
"isLoading": true,
|
|
||||||
"proctoringSettings": {
|
|
||||||
"exam_proctoring_backend": {
|
|
||||||
"download_url": "",
|
|
||||||
"instructions": [],
|
|
||||||
"name": "",
|
|
||||||
"rules": {},
|
|
||||||
},
|
|
||||||
"integration_specific_email": "",
|
|
||||||
"learner_notification_from_email": "",
|
|
||||||
"provider_name": "",
|
|
||||||
"provider_tech_support_email": "",
|
|
||||||
"provider_tech_support_phone": "",
|
|
||||||
"provider_tech_support_url": "",
|
|
||||||
},
|
|
||||||
"timeIsOver": false,
|
|
||||||
},
|
|
||||||
"tours": {
|
|
||||||
"showCoursewareTour": false,
|
|
||||||
"showExistingUserCourseHomeTour": false,
|
|
||||||
"showNewUserCourseHomeModal": false,
|
|
||||||
"showNewUserCourseHomeTour": false,
|
|
||||||
"toursEnabled": false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -3,93 +3,6 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|||||||
import { logInfo } from '@edx/frontend-platform/logging';
|
import { logInfo } from '@edx/frontend-platform/logging';
|
||||||
import { appendBrowserTimezoneToUrl } from '../../utils';
|
import { appendBrowserTimezoneToUrl } from '../../utils';
|
||||||
|
|
||||||
const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) => {
|
|
||||||
let dropCount = numDroppable;
|
|
||||||
// Drop the lowest grades
|
|
||||||
while (dropCount && points.length >= dropCount) {
|
|
||||||
const lowestScore = Math.min(...points);
|
|
||||||
const lowestScoreIndex = points.indexOf(lowestScore);
|
|
||||||
points.splice(lowestScoreIndex, 1);
|
|
||||||
dropCount--;
|
|
||||||
}
|
|
||||||
let averageGrade = 0;
|
|
||||||
let weightedGrade = 0;
|
|
||||||
if (points.length) {
|
|
||||||
// Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately
|
|
||||||
// reflect what a learner's grade would be, however, we must have parity with the current grading behavior that
|
|
||||||
// exists in edx-platform.
|
|
||||||
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(4);
|
|
||||||
weightedGrade = averageGrade * assignmentWeight;
|
|
||||||
}
|
|
||||||
return { averageGrade, weightedGrade };
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
|
|
||||||
const gradeByAssignmentType = {};
|
|
||||||
assignmentPolicies.forEach(assignment => {
|
|
||||||
// Create an array with the number of total assignments and set the scores to 0
|
|
||||||
// as placeholders for assignments that have not yet been released
|
|
||||||
gradeByAssignmentType[assignment.type] = {
|
|
||||||
grades: Array(assignment.numTotal).fill(0),
|
|
||||||
numAssignmentsCreated: 0,
|
|
||||||
numTotalExpectedAssignments: assignment.numTotal,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
sectionScores.forEach((chapter) => {
|
|
||||||
chapter.subsections.forEach((subsection) => {
|
|
||||||
if (!(subsection.hasGradedAssignment && subsection.showGrades && subsection.numPointsPossible)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
assignmentType,
|
|
||||||
numPointsEarned,
|
|
||||||
numPointsPossible,
|
|
||||||
} = subsection;
|
|
||||||
|
|
||||||
// If a subsection's assignment type does not match an assignment policy in Studio,
|
|
||||||
// we won't be able to include it in this accumulation of grades by assignment type.
|
|
||||||
// This may happen if a course author has removed/renamed an assignment policy in Studio and
|
|
||||||
// neglected to update the subsection's of that assignment type
|
|
||||||
if (!gradeByAssignmentType[assignmentType]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
numAssignmentsCreated,
|
|
||||||
} = gradeByAssignmentType[assignmentType];
|
|
||||||
|
|
||||||
numAssignmentsCreated++;
|
|
||||||
if (numAssignmentsCreated <= gradeByAssignmentType[assignmentType].numTotalExpectedAssignments) {
|
|
||||||
// Remove a placeholder grade so long as the number of recorded created assignments is less than the number
|
|
||||||
// of expected assignments
|
|
||||||
gradeByAssignmentType[assignmentType].grades.shift();
|
|
||||||
}
|
|
||||||
// Add the graded assignment to the list
|
|
||||||
gradeByAssignmentType[assignmentType].grades.push(numPointsEarned ? numPointsEarned / numPointsPossible : 0);
|
|
||||||
// Record the created assignment
|
|
||||||
gradeByAssignmentType[assignmentType].numAssignmentsCreated = numAssignmentsCreated;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return assignmentPolicies.map((assignment) => {
|
|
||||||
const { averageGrade, weightedGrade } = calculateAssignmentTypeGrades(
|
|
||||||
gradeByAssignmentType[assignment.type].grades,
|
|
||||||
assignment.weight,
|
|
||||||
assignment.numDroppable,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
averageGrade,
|
|
||||||
numDroppable: assignment.numDroppable,
|
|
||||||
shortLabel: assignment.shortLabel,
|
|
||||||
type: assignment.type,
|
|
||||||
weight: assignment.weight,
|
|
||||||
weightedGrade,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tweak the metadata for consistency
|
* Tweak the metadata for consistency
|
||||||
* @param metadata the data to normalize
|
* @param metadata the data to normalize
|
||||||
@@ -236,11 +149,6 @@ export async function getProgressTabData(courseId, targetUserId) {
|
|||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
const camelCasedData = camelCaseObject(data);
|
const camelCasedData = camelCaseObject(data);
|
||||||
|
|
||||||
camelCasedData.gradingPolicy.assignmentPolicies = normalizeAssignmentPolicies(
|
|
||||||
camelCasedData.gradingPolicy.assignmentPolicies,
|
|
||||||
camelCasedData.sectionScores,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade.
|
// We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade.
|
||||||
// For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a")
|
// For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a")
|
||||||
// in order to preserve a course team's desired grade formatting.
|
// in order to preserve a course team's desired grade formatting.
|
||||||
@@ -289,9 +197,17 @@ export async function getProgressTabData(courseId, targetUserId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getProctoringInfoData(courseId, username) {
|
export async function getProctoringInfoData(courseId, username) {
|
||||||
let url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`;
|
let url;
|
||||||
if (username) {
|
if (!getConfig().EXAMS_BASE_URL) {
|
||||||
url += `&username=${encodeURIComponent(username)}`;
|
url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}`;
|
||||||
|
if (username) {
|
||||||
|
url += `&username=${encodeURIComponent(username)}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
url = `${getConfig().EXAMS_BASE_URL}/api/v1/student/course_id/${encodeURIComponent(courseId)}/onboarding`;
|
||||||
|
if (username) {
|
||||||
|
url += `?username=${encodeURIComponent(username)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
@@ -359,7 +275,6 @@ export async function getOutlineTabData(courseId) {
|
|||||||
} = tabData;
|
} = tabData;
|
||||||
|
|
||||||
const accessExpiration = camelCaseObject(data.access_expiration);
|
const accessExpiration = camelCaseObject(data.access_expiration);
|
||||||
const canShowUpgradeSock = data.can_show_upgrade_sock;
|
|
||||||
const certData = camelCaseObject(data.cert_data);
|
const certData = camelCaseObject(data.cert_data);
|
||||||
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
|
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
|
||||||
const courseGoals = camelCaseObject(data.course_goals);
|
const courseGoals = camelCaseObject(data.course_goals);
|
||||||
@@ -381,7 +296,6 @@ export async function getOutlineTabData(courseId) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
canShowUpgradeSock,
|
|
||||||
certData,
|
certData,
|
||||||
courseBlocks,
|
courseBlocks,
|
||||||
courseGoals,
|
courseGoals,
|
||||||
@@ -449,7 +363,7 @@ export async function unsubscribeFromCourseGoal(token) {
|
|||||||
.then(res => camelCaseObject(res));
|
.then(res => camelCaseObject(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCoursewareSearchEnabledFlag(courseId) {
|
export async function getCoursewareSearchEnabled(courseId) {
|
||||||
const url = new URL(`${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`);
|
const url = new URL(`${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-search/enabled/`);
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url.href);
|
const { data } = await getAuthenticatedHttpClient().get(url.href);
|
||||||
return { enabled: data.enabled || false };
|
return { enabled: data.enabled || false };
|
||||||
@@ -465,3 +379,24 @@ export async function searchCourseContentFromAPI(courseId, searchKeyword, option
|
|||||||
|
|
||||||
return camelCaseObject(response);
|
return camelCaseObject(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getExamsData(courseId, sequenceId) {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
if (!getConfig().EXAMS_BASE_URL) {
|
||||||
|
url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||||
|
} else {
|
||||||
|
url = `${getConfig().EXAMS_BASE_URL}/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
|
return camelCaseObject(data);
|
||||||
|
} catch (error) {
|
||||||
|
const { httpErrorStatus } = error && error.customAttributes;
|
||||||
|
if (httpErrorStatus === 404) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { getTimeOffsetMillis } from './api';
|
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import { getTimeOffsetMillis, getExamsData } from './api';
|
||||||
|
import { initializeMockApp } from '../../setupTest';
|
||||||
|
|
||||||
|
initializeMockApp();
|
||||||
|
|
||||||
|
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
|
||||||
describe('Calculate the time offset properly', () => {
|
describe('Calculate the time offset properly', () => {
|
||||||
it('Should return 0 if the headerDate is not set', async () => {
|
it('Should return 0 if the headerDate is not set', async () => {
|
||||||
@@ -14,3 +22,156 @@ describe('Calculate the time offset properly', () => {
|
|||||||
expect(offset).toBe(86398750);
|
expect(offset).toBe(86398750);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getExamsData', () => {
|
||||||
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
|
const sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345';
|
||||||
|
let originalConfig;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
axiosMock.reset();
|
||||||
|
originalConfig = getConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
axiosMock.reset();
|
||||||
|
if (originalConfig) {
|
||||||
|
setConfig(originalConfig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use LMS URL when EXAMS_BASE_URL is not configured', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: undefined,
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockExamData = {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceId,
|
||||||
|
exam_name: 'Test Exam',
|
||||||
|
attempt_status: 'created',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedUrl = `http://localhost:18000/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||||
|
axiosMock.onGet(expectedUrl).reply(200, mockExamData);
|
||||||
|
|
||||||
|
const result = await getExamsData(courseId, sequenceId);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceId,
|
||||||
|
examName: 'Test Exam',
|
||||||
|
attemptStatus: 'created',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(axiosMock.history.get).toHaveLength(1);
|
||||||
|
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use EXAMS_BASE_URL when configured', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: 'http://localhost:18740',
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockExamData = {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceId,
|
||||||
|
exam_name: 'Test Exam',
|
||||||
|
attempt_status: 'submitted',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedUrl = `http://localhost:18740/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}`;
|
||||||
|
axiosMock.onGet(expectedUrl).reply(200, mockExamData);
|
||||||
|
|
||||||
|
const result = await getExamsData(courseId, sequenceId);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceId,
|
||||||
|
examName: 'Test Exam',
|
||||||
|
attemptStatus: 'submitted',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(axiosMock.history.get).toHaveLength(1);
|
||||||
|
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty object when API returns 404', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: undefined,
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedUrl = `http://localhost:18000/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||||
|
|
||||||
|
// Mock a 404 error with the custom error response function to add customAttributes
|
||||||
|
axiosMock.onGet(expectedUrl).reply(() => {
|
||||||
|
const error = new Error('Request failed with status code 404');
|
||||||
|
error.response = { status: 404, data: {} };
|
||||||
|
error.customAttributes = { httpErrorStatus: 404 };
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getExamsData(courseId, sequenceId);
|
||||||
|
|
||||||
|
expect(result).toEqual({});
|
||||||
|
expect(axiosMock.history.get).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error for non-404 HTTP errors', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: undefined,
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedUrl = `http://localhost:18000/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||||
|
|
||||||
|
// Mock a 500 error with custom error response
|
||||||
|
axiosMock.onGet(expectedUrl).reply(() => {
|
||||||
|
const error = new Error('Request failed with status code 500');
|
||||||
|
error.response = { status: 500, data: { error: 'Server Error' } };
|
||||||
|
error.customAttributes = { httpErrorStatus: 500 };
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(getExamsData(courseId, sequenceId)).rejects.toThrow();
|
||||||
|
expect(axiosMock.history.get).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly encode URL parameters', async () => {
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: 'http://localhost:18740',
|
||||||
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const specialCourseId = 'course-v1:edX+Demo X+Demo Course';
|
||||||
|
const specialSequenceId = 'block-v1:edX+Demo X+Demo Course+type@sequential+block@test sequence';
|
||||||
|
|
||||||
|
const mockExamData = { exam: { id: 1 } };
|
||||||
|
const expectedUrl = `http://localhost:18740/api/v1/student/exam/attempt/course_id/${encodeURIComponent(specialCourseId)}/content_id/${encodeURIComponent(specialSequenceId)}`;
|
||||||
|
axiosMock.onGet(expectedUrl).reply(200, mockExamData);
|
||||||
|
|
||||||
|
await getExamsData(specialCourseId, specialSequenceId);
|
||||||
|
|
||||||
|
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
|
||||||
|
expect(axiosMock.history.get[0].url).toContain('course-v1%3AedX%2BDemo%20X%2BDemo%20Course');
|
||||||
|
expect(axiosMock.history.get[0].url).toContain('block-v1%3AedX%2BDemo%20X%2BDemo%20Course%2Btype%40sequential%2Bblock%40test%20sequence');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ describe('Course Home Service', () => {
|
|||||||
willRespondWith: {
|
willRespondWith: {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
can_show_upgrade_sock: boolean(false),
|
|
||||||
verified_mode: like({
|
verified_mode: like({
|
||||||
access_expiration_date: null,
|
access_expiration_date: null,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
@@ -89,11 +88,11 @@ describe('Course Home Service', () => {
|
|||||||
}),
|
}),
|
||||||
title: string('Demonstration Course'),
|
title: string('Demonstration Course'),
|
||||||
username: string('edx'),
|
username: string('edx'),
|
||||||
|
has_course_author_access: boolean(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const normalizedTabData = {
|
const normalizedTabData = {
|
||||||
canShowUpgradeSock: false,
|
|
||||||
verifiedMode: {
|
verifiedMode: {
|
||||||
accessExpirationDate: null,
|
accessExpirationDate: null,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
@@ -133,6 +132,7 @@ describe('Course Home Service', () => {
|
|||||||
],
|
],
|
||||||
title: 'Demonstration Course',
|
title: 'Demonstration Course',
|
||||||
username: 'edx',
|
username: 'edx',
|
||||||
|
hasCourseAuthorAccess: true,
|
||||||
};
|
};
|
||||||
const response = getCourseHomeCourseMetadata(courseId, 'outline');
|
const response = getCourseHomeCourseMetadata(courseId, 'outline');
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
|
|||||||
@@ -90,14 +90,14 @@ describe('Data layer integration tests', () => {
|
|||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||||
expect(state).toMatchSnapshot({
|
expect(state).toEqual(expect.objectContaining({
|
||||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||||
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
// to keep track of conversations. This UUID is generated on each run.
|
||||||
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
// Instead, we use an asymmetric matcher here.
|
||||||
learningAssistant: expect.objectContaining({
|
learningAssistant: expect.objectContaining({
|
||||||
conversationId: expect.any(String),
|
conversationId: expect.any(String),
|
||||||
}),
|
}),
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([401, 403, 404])(
|
it.each([401, 403, 404])(
|
||||||
@@ -137,14 +137,14 @@ describe('Data layer integration tests', () => {
|
|||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||||
expect(state).toMatchSnapshot({
|
expect(state).toEqual(expect.objectContaining({
|
||||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||||
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
// to keep track of conversations. This UUID is generated on each run.
|
||||||
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
// Instead, we use an asymmetric matcher here.
|
||||||
learningAssistant: expect.objectContaining({
|
learningAssistant: expect.objectContaining({
|
||||||
conversationId: expect.any(String),
|
conversationId: expect.any(String),
|
||||||
}),
|
}),
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([401, 403, 404])(
|
it.each([401, 403, 404])(
|
||||||
@@ -185,14 +185,14 @@ describe('Data layer integration tests', () => {
|
|||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||||
expect(state).toMatchSnapshot({
|
expect(state).toEqual(expect.objectContaining({
|
||||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||||
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
// to keep track of conversations. This UUID is generated on each run.
|
||||||
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
// Instead, we use an asymmetric matcher here.
|
||||||
learningAssistant: expect.objectContaining({
|
learningAssistant: expect.objectContaining({
|
||||||
conversationId: expect.any(String),
|
conversationId: expect.any(String),
|
||||||
}),
|
}),
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should handle the url including a targetUserId', async () => {
|
it('Should handle the url including a targetUserId', async () => {
|
||||||
@@ -297,4 +297,178 @@ describe('Data layer integration tests', () => {
|
|||||||
expect(enabled).toBe(false);
|
expect(enabled).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Test fetchExamAttemptsData', () => {
|
||||||
|
const sequenceIds = [
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@67890',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@abcde',
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock individual exam endpoints with different responses
|
||||||
|
sequenceIds.forEach((sequenceId, index) => {
|
||||||
|
// Handle both LMS and EXAMS service URL patterns
|
||||||
|
const lmsExamUrl = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceId)}.*`);
|
||||||
|
const examsServiceUrl = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}.*`);
|
||||||
|
|
||||||
|
let attemptStatus = 'ready_to_start';
|
||||||
|
if (index === 0) {
|
||||||
|
attemptStatus = 'created';
|
||||||
|
} else if (index === 1) {
|
||||||
|
attemptStatus = 'submitted';
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExamData = {
|
||||||
|
exam: {
|
||||||
|
id: index + 1,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceId,
|
||||||
|
exam_name: `Test Exam ${index + 1}`,
|
||||||
|
attempt_status: attemptStatus,
|
||||||
|
time_remaining_seconds: 3600,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock both URL patterns
|
||||||
|
axiosMock.onGet(lmsExamUrl).reply(200, mockExamData);
|
||||||
|
axiosMock.onGet(examsServiceUrl).reply(200, mockExamData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch exam data for all sequence IDs and dispatch setExamsData', async () => {
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
// Verify the examsData was set in the store
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
expect(state.courseHome.examsData).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceIds[0],
|
||||||
|
examName: 'Test Exam 1',
|
||||||
|
attemptStatus: 'created',
|
||||||
|
timeRemainingSeconds: 3600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceIds[1],
|
||||||
|
examName: 'Test Exam 2',
|
||||||
|
attemptStatus: 'submitted',
|
||||||
|
timeRemainingSeconds: 3600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceIds[2],
|
||||||
|
examName: 'Test Exam 3',
|
||||||
|
attemptStatus: 'ready_to_start',
|
||||||
|
timeRemainingSeconds: 3600,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify all API calls were made
|
||||||
|
expect(axiosMock.history.get).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 404 responses and include empty objects in results', async () => {
|
||||||
|
// Override one endpoint to return 404 for both URL patterns
|
||||||
|
const examUrl404LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[1])}.*`);
|
||||||
|
const examUrl404Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[1])}.*`);
|
||||||
|
axiosMock.onGet(examUrl404LMS).reply(404);
|
||||||
|
axiosMock.onGet(examUrl404Exams).reply(404);
|
||||||
|
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
// Verify the examsData includes empty object for 404 response
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
expect(state.courseHome.examsData[1]).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API errors and log them while continuing with other requests', async () => {
|
||||||
|
// Override one endpoint to return 500 error for both URL patterns
|
||||||
|
const examUrl500LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[0])}.*`);
|
||||||
|
const examUrl500Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[0])}.*`);
|
||||||
|
axiosMock.onGet(examUrl500LMS).reply(500, { error: 'Server Error' });
|
||||||
|
axiosMock.onGet(examUrl500Exams).reply(500, { error: 'Server Error' });
|
||||||
|
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
// Verify error was logged for the failed request
|
||||||
|
expect(loggingService.logError).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify the examsData still includes results for successful requests
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
// First item should be the error result (just empty object for API errors)
|
||||||
|
expect(state.courseHome.examsData[0]).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty sequence IDs array', async () => {
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, []), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
expect(state.courseHome.examsData).toEqual([]);
|
||||||
|
expect(axiosMock.history.get).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed success and error responses', async () => {
|
||||||
|
// Setup mixed responses
|
||||||
|
const examUrl1LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[0])}.*`);
|
||||||
|
const examUrl1Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[0])}.*`);
|
||||||
|
const examUrl2LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[1])}.*`);
|
||||||
|
const examUrl2Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[1])}.*`);
|
||||||
|
const examUrl3LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[2])}.*`);
|
||||||
|
const examUrl3Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[2])}.*`);
|
||||||
|
|
||||||
|
axiosMock.onGet(examUrl1LMS).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
exam_name: 'Success Exam',
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceIds[0],
|
||||||
|
attempt_status: 'created',
|
||||||
|
time_remaining_seconds: 3600,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
axiosMock.onGet(examUrl1Exams).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
exam_name: 'Success Exam',
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceIds[0],
|
||||||
|
attempt_status: 'created',
|
||||||
|
time_remaining_seconds: 3600,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
axiosMock.onGet(examUrl2LMS).reply(404);
|
||||||
|
axiosMock.onGet(examUrl2Exams).reply(404);
|
||||||
|
axiosMock.onGet(examUrl3LMS).reply(500, { error: 'Server Error' });
|
||||||
|
axiosMock.onGet(examUrl3Exams).reply(500, { error: 'Server Error' });
|
||||||
|
|
||||||
|
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
expect(state.courseHome.examsData[0]).toMatchObject({
|
||||||
|
id: 1,
|
||||||
|
examName: 'Success Exam',
|
||||||
|
courseId,
|
||||||
|
contentId: sequenceIds[0],
|
||||||
|
});
|
||||||
|
expect(state.courseHome.examsData[1]).toEqual({});
|
||||||
|
expect(state.courseHome.examsData[2]).toEqual({});
|
||||||
|
|
||||||
|
// Verify error was logged for the 500 error (may be called more than once due to multiple URL patterns)
|
||||||
|
expect(loggingService.logError).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const slice = createSlice({
|
|||||||
toastBodyLink: null,
|
toastBodyLink: null,
|
||||||
toastHeader: '',
|
toastHeader: '',
|
||||||
showSearch: false,
|
showSearch: false,
|
||||||
|
examsData: null,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
fetchProctoringInfoResolved: (state) => {
|
fetchProctoringInfoResolved: (state) => {
|
||||||
@@ -53,6 +54,9 @@ const slice = createSlice({
|
|||||||
setShowSearch: (state, { payload }) => {
|
setShowSearch: (state, { payload }) => {
|
||||||
state.showSearch = payload;
|
state.showSearch = payload;
|
||||||
},
|
},
|
||||||
|
setExamsData: (state, { payload }) => {
|
||||||
|
state.examsData = payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,6 +68,7 @@ export const {
|
|||||||
fetchTabSuccess,
|
fetchTabSuccess,
|
||||||
setCallToActionToast,
|
setCallToActionToast,
|
||||||
setShowSearch,
|
setShowSearch,
|
||||||
|
setExamsData,
|
||||||
} = slice.actions;
|
} = slice.actions;
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
|||||||
145
src/course-home/data/slice.test.js
Normal file
145
src/course-home/data/slice.test.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { reducer, setExamsData } from './slice';
|
||||||
|
|
||||||
|
describe('course home data slice', () => {
|
||||||
|
describe('setExamsData reducer', () => {
|
||||||
|
it('should set examsData in state', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loading',
|
||||||
|
courseId: null,
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'loading',
|
||||||
|
tabFetchStates: {},
|
||||||
|
toastBodyText: '',
|
||||||
|
toastBodyLink: null,
|
||||||
|
toastHeader: '',
|
||||||
|
showSearch: false,
|
||||||
|
examsData: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockExamsData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||||
|
examName: 'Midterm Exam',
|
||||||
|
attemptStatus: 'created',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||||
|
examName: 'Final Exam',
|
||||||
|
attemptStatus: 'submitted',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const action = setExamsData(mockExamsData);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState.examsData).toEqual(mockExamsData);
|
||||||
|
expect(newState).toEqual({
|
||||||
|
...initialState,
|
||||||
|
examsData: mockExamsData,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update examsData when state already has data', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loaded',
|
||||||
|
courseId: 'test-course',
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'loading',
|
||||||
|
tabFetchStates: {},
|
||||||
|
toastBodyText: '',
|
||||||
|
toastBodyLink: null,
|
||||||
|
toastHeader: '',
|
||||||
|
showSearch: false,
|
||||||
|
examsData: [{ id: 1, examName: 'Old Exam' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const newExamsData = [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||||
|
examName: 'New Exam',
|
||||||
|
attemptStatus: 'ready_to_start',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const action = setExamsData(newExamsData);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState.examsData).toEqual(newExamsData);
|
||||||
|
expect(newState.examsData).not.toEqual(initialState.examsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set examsData to empty array', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loaded',
|
||||||
|
courseId: 'test-course',
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'loading',
|
||||||
|
tabFetchStates: {},
|
||||||
|
toastBodyText: '',
|
||||||
|
toastBodyLink: null,
|
||||||
|
toastHeader: '',
|
||||||
|
showSearch: false,
|
||||||
|
examsData: [{ id: 1, examName: 'Some Exam' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = setExamsData([]);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState.examsData).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set examsData to null', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loaded',
|
||||||
|
courseId: 'test-course',
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'loading',
|
||||||
|
tabFetchStates: {},
|
||||||
|
toastBodyText: '',
|
||||||
|
toastBodyLink: null,
|
||||||
|
toastHeader: '',
|
||||||
|
showSearch: false,
|
||||||
|
examsData: [{ id: 1, examName: 'Some Exam' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = setExamsData(null);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
expect(newState.examsData).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not affect other state properties when setting examsData', () => {
|
||||||
|
const initialState = {
|
||||||
|
courseStatus: 'loaded',
|
||||||
|
courseId: 'test-course-id',
|
||||||
|
metadataModel: 'courseHomeCourseMetadata',
|
||||||
|
proctoringPanelStatus: 'complete',
|
||||||
|
tabFetchStates: { progress: 'loaded' },
|
||||||
|
toastBodyText: 'Toast message',
|
||||||
|
toastBodyLink: 'http://example.com',
|
||||||
|
toastHeader: 'Toast Header',
|
||||||
|
showSearch: true,
|
||||||
|
examsData: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockExamsData = [{ id: 1, examName: 'Test Exam' }];
|
||||||
|
const action = setExamsData(mockExamsData);
|
||||||
|
const newState = reducer(initialState, action);
|
||||||
|
|
||||||
|
// Verify that only examsData changed
|
||||||
|
expect(newState).toEqual({
|
||||||
|
...initialState,
|
||||||
|
examsData: mockExamsData,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify other properties remain unchanged
|
||||||
|
expect(newState.courseStatus).toBe(initialState.courseStatus);
|
||||||
|
expect(newState.courseId).toBe(initialState.courseId);
|
||||||
|
expect(newState.showSearch).toBe(initialState.showSearch);
|
||||||
|
expect(newState.toastBodyText).toBe(initialState.toastBodyText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
executePostFromPostEvent,
|
executePostFromPostEvent,
|
||||||
getCourseHomeCourseMetadata,
|
getCourseHomeCourseMetadata,
|
||||||
getDatesTabData,
|
getDatesTabData,
|
||||||
|
getExamsData,
|
||||||
getOutlineTabData,
|
getOutlineTabData,
|
||||||
getProgressTabData,
|
getProgressTabData,
|
||||||
postCourseDeadlines,
|
postCourseDeadlines,
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
postDismissWelcomeMessage,
|
postDismissWelcomeMessage,
|
||||||
postRequestCert,
|
postRequestCert,
|
||||||
getLiveTabIframe,
|
getLiveTabIframe,
|
||||||
getCoursewareSearchEnabledFlag,
|
getCoursewareSearchEnabled,
|
||||||
searchCourseContentFromAPI,
|
searchCourseContentFromAPI,
|
||||||
} from './api';
|
} from './api';
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
fetchTabRequest,
|
fetchTabRequest,
|
||||||
fetchTabSuccess,
|
fetchTabSuccess,
|
||||||
setCallToActionToast,
|
setCallToActionToast,
|
||||||
|
setExamsData,
|
||||||
} from './slice';
|
} from './slice';
|
||||||
|
|
||||||
import mapSearchResponse from '../courseware-search/map-search-response';
|
import mapSearchResponse from '../courseware-search/map-search-response';
|
||||||
@@ -159,7 +161,7 @@ export function processEvent(eventData, getTabData) {
|
|||||||
|
|
||||||
export async function fetchCoursewareSearchSettings(courseId) {
|
export async function fetchCoursewareSearchSettings(courseId) {
|
||||||
try {
|
try {
|
||||||
const { enabled } = await getCoursewareSearchEnabledFlag(courseId);
|
const { enabled } = await getCoursewareSearchEnabled(courseId);
|
||||||
return { enabled };
|
return { enabled };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { enabled: false };
|
return { enabled: false };
|
||||||
@@ -223,3 +225,19 @@ export function searchCourseContent(courseId, searchKeyword) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchExamAttemptsData(courseId, sequenceIds) {
|
||||||
|
return async (dispatch) => {
|
||||||
|
const results = await Promise.all(sequenceIds.map(async (sequenceId) => {
|
||||||
|
try {
|
||||||
|
const response = await getExamsData(courseId, sequenceId);
|
||||||
|
return response.exam || {};
|
||||||
|
} catch (e) {
|
||||||
|
logError(e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch(setExamsData(results));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import Timeline from './timeline/Timeline';
|
import Timeline from './timeline/Timeline';
|
||||||
@@ -14,7 +14,8 @@ import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
|||||||
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
|
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
|
||||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||||
|
|
||||||
const DatesTab = ({ intl }) => {
|
const DatesTab = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -59,8 +60,4 @@ const DatesTab = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DatesTab.propTypes = {
|
export default DatesTab;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(DatesTab);
|
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ describe('DatesTab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows extra info', async () => {
|
it('shows extra info', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
const { items } = await getDay('Sat, Aug 17, 2030');
|
const { items } = await getDay('Sat, Aug 17, 2030');
|
||||||
expect(items).toHaveLength(3);
|
expect(items).toHaveLength(3);
|
||||||
|
|
||||||
@@ -142,10 +143,12 @@ describe('DatesTab', () => {
|
|||||||
const tipText = "ORA Dates are set by the instructor, and can't be changed";
|
const tipText = "ORA Dates are set by the instructor, and can't be changed";
|
||||||
|
|
||||||
expect(screen.queryByText(tipText)).toBeNull(); // tooltip does not start in DOM
|
expect(screen.queryByText(tipText)).toBeNull(); // tooltip does not start in DOM
|
||||||
userEvent.hover(tipIcon);
|
await user.hover(tipIcon);
|
||||||
const tooltip = screen.getByText(tipText); // now it's there
|
screen.getByText(tipText); // now it's there
|
||||||
userEvent.unhover(tipIcon);
|
await user.unhover(tipIcon);
|
||||||
await waitForElementToBeRemoved(tooltip); // and it's gone again
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText(tipText)).toBeNull(); // and it's gone again
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import {
|
import {
|
||||||
FormattedDate,
|
FormattedDate,
|
||||||
FormattedTime,
|
FormattedTime,
|
||||||
injectIntl,
|
useIntl,
|
||||||
intlShape,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Tooltip, OverlayTrigger } from '@openedx/paragon';
|
import { Tooltip, OverlayTrigger } from '@openedx/paragon';
|
||||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
@@ -20,10 +19,10 @@ import { isLearnerAssignment } from '../utils';
|
|||||||
const Day = ({
|
const Day = ({
|
||||||
date,
|
date,
|
||||||
first,
|
first,
|
||||||
intl,
|
|
||||||
items,
|
items,
|
||||||
last,
|
last,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -108,7 +107,6 @@ const Day = ({
|
|||||||
Day.propTypes = {
|
Day.propTypes = {
|
||||||
date: PropTypes.objectOf(Date).isRequired,
|
date: PropTypes.objectOf(Date).isRequired,
|
||||||
first: PropTypes.bool,
|
first: PropTypes.bool,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.shape({
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
date: PropTypes.string,
|
date: PropTypes.string,
|
||||||
dateType: PropTypes.string,
|
dateType: PropTypes.string,
|
||||||
@@ -126,4 +124,4 @@ Day.defaultProps = {
|
|||||||
last: false,
|
last: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(Day);
|
export default Day;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useParams, generatePath, useNavigate } from 'react-router-dom';
|
import { useParams, generatePath, useNavigate } from 'react-router-dom';
|
||||||
@@ -30,6 +29,4 @@ const DiscussionTab = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DiscussionTab.propTypes = {};
|
export default DiscussionTab;
|
||||||
|
|
||||||
export default injectIntl(DiscussionTab);
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import HeaderSlot from '../../plugin-slots/HeaderSlot';
|
import HeaderSlot from '../../plugin-slots/HeaderSlot';
|
||||||
import PageLoading from '../../generic/PageLoading';
|
import PageLoading from '../../generic/PageLoading';
|
||||||
@@ -10,7 +10,8 @@ import { unsubscribeFromCourseGoal } from '../data/api';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ResultPage from './ResultPage';
|
import ResultPage from './ResultPage';
|
||||||
|
|
||||||
const GoalUnsubscribe = ({ intl }) => {
|
const GoalUnsubscribe = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const { token } = useParams();
|
const { token } = useParams();
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -51,8 +52,4 @@ const GoalUnsubscribe = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
GoalUnsubscribe.propTypes = {
|
export default GoalUnsubscribe;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(GoalUnsubscribe);
|
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button, Hyperlink } from '@openedx/paragon';
|
import { Button, Hyperlink } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';
|
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';
|
||||||
|
|
||||||
const ResultPage = ({ courseTitle, error, intl }) => {
|
const ResultPage = ({ courseTitle, error }) => {
|
||||||
const errorDescription = (
|
const intl = useIntl();
|
||||||
<FormattedMessage
|
const errorDescription = intl.formatMessage(
|
||||||
id="learning.goals.unsubscribe.errorDescription"
|
messages.errorDescription,
|
||||||
defaultMessage="We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help."
|
{
|
||||||
values={{
|
contactSupport: (
|
||||||
contactSupport: (
|
<Hyperlink
|
||||||
<Hyperlink
|
className="text-reset"
|
||||||
className="text-reset"
|
style={{ textDecoration: 'underline' }}
|
||||||
style={{ textDecoration: 'underline' }}
|
destination={`${getConfig().CONTACT_URL}`}
|
||||||
destination={`${getConfig().CONTACT_URL}`}
|
>
|
||||||
>
|
{intl.formatMessage(messages.contactSupport)}
|
||||||
{intl.formatMessage(messages.contactSupport)}
|
</Hyperlink>
|
||||||
</Hyperlink>
|
),
|
||||||
),
|
},
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const header = error
|
const header = error
|
||||||
@@ -54,7 +52,6 @@ ResultPage.defaultProps = {
|
|||||||
ResultPage.propTypes = {
|
ResultPage.propTypes = {
|
||||||
courseTitle: PropTypes.string,
|
courseTitle: PropTypes.string,
|
||||||
error: PropTypes.bool,
|
error: PropTypes.bool,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(ResultPage);
|
export default ResultPage;
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Something went wrong',
|
defaultMessage: 'Something went wrong',
|
||||||
description: 'It indicate that the unsubscribing request has failed',
|
description: 'It indicate that the unsubscribing request has failed',
|
||||||
},
|
},
|
||||||
|
errorDescription: {
|
||||||
|
id: 'learning.goals.unsubscribe.errorDescription',
|
||||||
|
defaultMessage: 'We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help.',
|
||||||
|
description: 'Message that notifies user that unsubscribing failed and to try again',
|
||||||
|
},
|
||||||
goToDashboard: {
|
goToDashboard: {
|
||||||
id: 'learning.goals.unsubscribe.goToDashboard',
|
id: 'learning.goals.unsubscribe.goToDashboard',
|
||||||
defaultMessage: 'Go to dashboard',
|
defaultMessage: 'Go to dashboard',
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const DateSummary = ({
|
|||||||
)}
|
)}
|
||||||
{!linkedTitle && dateBlock.link && (
|
{!linkedTitle && dateBlock.link && (
|
||||||
<a
|
<a
|
||||||
|
id={dateBlock.dateType === 'verified-upgrade-deadline' ? 'date-verified-upgrade-deadline' : ''}
|
||||||
href={dateBlock.link}
|
href={dateBlock.link}
|
||||||
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
|
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
|
||||||
className="description-link"
|
className="description-link"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@openedx/paragon';
|
import { Button } from '@openedx/paragon';
|
||||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
import { CourseOutlineTabNotificationsSlot } from '../../plugin-slots/CourseOutlineTabNotificationsSlot';
|
||||||
import { AlertList } from '../../generic/user-messages';
|
import { AlertList } from '../../generic/user-messages';
|
||||||
|
|
||||||
import CourseDates from './widgets/CourseDates';
|
import CourseDates from './widgets/CourseDates';
|
||||||
@@ -15,9 +15,7 @@ import WeeklyLearningGoalCard from './widgets/WeeklyLearningGoalCard';
|
|||||||
import CourseTools from './widgets/CourseTools';
|
import CourseTools from './widgets/CourseTools';
|
||||||
import { fetchOutlineTab } from '../data';
|
import { fetchOutlineTab } from '../data';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import Section from './Section';
|
|
||||||
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||||
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
|
|
||||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||||
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
|
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
|
||||||
import useCourseEndAlert from './alerts/course-end-alert';
|
import useCourseEndAlert from './alerts/course-end-alert';
|
||||||
@@ -28,8 +26,10 @@ import { useModel } from '../../generic/model-store';
|
|||||||
import WelcomeMessage from './widgets/WelcomeMessage';
|
import WelcomeMessage from './widgets/WelcomeMessage';
|
||||||
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
|
import ProctoringInfoPanel from './widgets/ProctoringInfoPanel';
|
||||||
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
|
import AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
|
||||||
|
import CourseHomeSectionOutlineSlot from '../../plugin-slots/CourseHomeSectionOutlineSlot';
|
||||||
|
|
||||||
const OutlineTab = ({ intl }) => {
|
const OutlineTab = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
proctoringPanelStatus,
|
proctoringPanelStatus,
|
||||||
@@ -39,11 +39,11 @@ const OutlineTab = ({ intl }) => {
|
|||||||
isSelfPaced,
|
isSelfPaced,
|
||||||
org,
|
org,
|
||||||
title,
|
title,
|
||||||
userTimezone,
|
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
|
const expandButtonRef = useRef();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
accessExpiration,
|
|
||||||
courseBlocks: {
|
courseBlocks: {
|
||||||
courses,
|
courses,
|
||||||
sections,
|
sections,
|
||||||
@@ -52,20 +52,12 @@ const OutlineTab = ({ intl }) => {
|
|||||||
selectedGoal,
|
selectedGoal,
|
||||||
weeklyLearningGoalEnabled,
|
weeklyLearningGoalEnabled,
|
||||||
} = {},
|
} = {},
|
||||||
datesBannerInfo,
|
|
||||||
datesWidget: {
|
datesWidget: {
|
||||||
courseDateBlocks,
|
courseDateBlocks,
|
||||||
},
|
},
|
||||||
enableProctoredExams,
|
enableProctoredExams,
|
||||||
offer,
|
|
||||||
timeOffsetMillis,
|
|
||||||
verifiedMode,
|
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
const {
|
|
||||||
marketingUrl,
|
|
||||||
} = useModel('coursewareMeta', courseId);
|
|
||||||
|
|
||||||
const [expandAll, setExpandAll] = useState(false);
|
const [expandAll, setExpandAll] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -159,27 +151,21 @@ const OutlineTab = ({ intl }) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<StartOrResumeCourseCard />
|
<StartOrResumeCourseCard />
|
||||||
<WelcomeMessage courseId={courseId} />
|
<WelcomeMessage courseId={courseId} nextElementRef={expandButtonRef} />
|
||||||
{rootCourseId && (
|
{rootCourseId && (
|
||||||
<>
|
<>
|
||||||
<div className="row w-100 m-0 mb-3 justify-content-end">
|
<div id="expand-button-row" className="row w-100 m-0 mb-3 justify-content-end">
|
||||||
<div className="col-12 col-md-auto p-0">
|
<div className="col-12 col-md-auto p-0">
|
||||||
<Button variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
|
<Button ref={expandButtonRef} variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
|
||||||
{expandAll ? intl.formatMessage(messages.collapseAll) : intl.formatMessage(messages.expandAll)}
|
{expandAll ? intl.formatMessage(messages.collapseAll) : intl.formatMessage(messages.expandAll)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ol id="courseHome-outline" className="list-unstyled">
|
<CourseHomeSectionOutlineSlot
|
||||||
{courses[rootCourseId].sectionIds.map((sectionId) => (
|
expandAll={expandAll}
|
||||||
<Section
|
sectionIds={courses[rootCourseId].sectionIds}
|
||||||
key={sectionId}
|
sections={sections}
|
||||||
courseId={courseId}
|
/>
|
||||||
defaultOpen={sections[sectionId].resumeBlock}
|
|
||||||
expand={expandAll}
|
|
||||||
section={sections[sectionId]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -195,27 +181,7 @@ const OutlineTab = ({ intl }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CourseTools />
|
<CourseTools />
|
||||||
<PluginSlot
|
<CourseOutlineTabNotificationsSlot courseId={courseId} />
|
||||||
id="outline_tab_notifications_slot"
|
|
||||||
pluginProps={{
|
|
||||||
courseId,
|
|
||||||
model: 'outline',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UpgradeNotification
|
|
||||||
offer={offer}
|
|
||||||
verifiedMode={verifiedMode}
|
|
||||||
accessExpiration={accessExpiration}
|
|
||||||
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
|
||||||
marketingUrl={marketingUrl}
|
|
||||||
upsellPageName="course_home"
|
|
||||||
userTimezone={userTimezone}
|
|
||||||
shouldDisplayBorder
|
|
||||||
timeOffsetMillis={timeOffsetMillis}
|
|
||||||
courseId={courseId}
|
|
||||||
org={org}
|
|
||||||
/>
|
|
||||||
</PluginSlot>
|
|
||||||
<CourseDates />
|
<CourseDates />
|
||||||
<CourseHandouts />
|
<CourseHandouts />
|
||||||
</div>
|
</div>
|
||||||
@@ -225,8 +191,4 @@ const OutlineTab = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
OutlineTab.propTypes = {
|
export default OutlineTab;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(OutlineTab);
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React from 'react';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { Factory } from 'rosie';
|
import { Factory } from 'rosie';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
@@ -54,7 +54,7 @@ describe('Outline Tab', () => {
|
|||||||
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
||||||
const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`;
|
const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`;
|
||||||
const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
|
const outlineUrl = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
|
||||||
const proctoringInfoUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}&username=MockUser`;
|
const proctoringInfoUrl = `${getConfig().EXAMS_BASE_URL}/api/v1/student/course_id/${encodeURIComponent(courseId)}/onboarding?username=MockUser`;
|
||||||
|
|
||||||
const store = initializeStore();
|
const store = initializeStore();
|
||||||
const defaultMetadata = Factory.build('courseHomeMetadata');
|
const defaultMetadata = Factory.build('courseHomeMetadata');
|
||||||
@@ -139,10 +139,11 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
|
|
||||||
expect(screen.getByTestId('outline_tab_notifications_slot')).toBeInTheDocument();
|
expect(screen.getByTestId('org.openedx.frontend.learning.course_outline_tab_notifications.v1')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles expand/collapse all button click', async () => {
|
it('handles expand/collapse all button click', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
// Button renders as "Expand All"
|
// Button renders as "Expand All"
|
||||||
const expandButton = screen.getByRole('button', { name: 'Expand all' });
|
const expandButton = screen.getByRole('button', { name: 'Expand all' });
|
||||||
@@ -153,11 +154,11 @@ describe('Outline Tab', () => {
|
|||||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
// Click to expand section
|
// Click to expand section
|
||||||
userEvent.click(expandButton);
|
await user.click(expandButton);
|
||||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
|
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
|
||||||
|
|
||||||
// Click to collapse section
|
// Click to collapse section
|
||||||
userEvent.click(expandButton);
|
await user.click(expandButton);
|
||||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,7 +168,7 @@ describe('Outline Tab', () => {
|
|||||||
course_blocks: { blocks: courseBlocks.blocks },
|
course_blocks: { blocks: courseBlocks.blocks },
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByTitle('Completed section')).toBeInTheDocument();
|
expect(screen.getByLabelText('Completed section')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays correct icon for incomplete assignment', async () => {
|
it('displays correct icon for incomplete assignment', async () => {
|
||||||
@@ -176,7 +177,7 @@ describe('Outline Tab', () => {
|
|||||||
course_blocks: { blocks: courseBlocks.blocks },
|
course_blocks: { blocks: courseBlocks.blocks },
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
|
expect(screen.getByLabelText('Incomplete section')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('SequenceLink displays link', async () => {
|
it('SequenceLink displays link', async () => {
|
||||||
@@ -275,21 +276,34 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders show more/less button and handles click', async () => {
|
it('renders show more/less button and handles click', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
|
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
|
||||||
let showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
let showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
||||||
expect(showMoreButton).toBeInTheDocument();
|
expect(showMoreButton).toBeInTheDocument();
|
||||||
|
|
||||||
userEvent.click(showMoreButton);
|
await user.click(showMoreButton);
|
||||||
let showLessButton = screen.getByRole('button', { name: 'Show Less' });
|
let showLessButton = screen.getByRole('button', { name: 'Show Less' });
|
||||||
expect(showLessButton).toBeInTheDocument();
|
expect(showLessButton).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('long-welcome-message-iframe')).toBeInTheDocument();
|
expect(screen.getByTestId('long-welcome-message-iframe')).toBeInTheDocument();
|
||||||
|
|
||||||
userEvent.click(showLessButton);
|
await user.click(showLessButton);
|
||||||
showLessButton = screen.queryByRole('button', { name: 'Show Less' });
|
showLessButton = screen.queryByRole('button', { name: 'Show Less' });
|
||||||
expect(showLessButton).not.toBeInTheDocument();
|
expect(showLessButton).not.toBeInTheDocument();
|
||||||
showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
showMoreButton = screen.getByRole('button', { name: 'Show More' });
|
||||||
expect(showMoreButton).toBeInTheDocument();
|
expect(showMoreButton).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('dismisses message', async () => {
|
||||||
|
expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
|
||||||
|
const dismissButton = screen.queryByRole('button', { name: 'Dismiss' });
|
||||||
|
const expandButton = screen.queryByRole('button', { name: 'Expand all' });
|
||||||
|
|
||||||
|
fireEvent.click(dismissButton);
|
||||||
|
|
||||||
|
expect(expandButton).toHaveFocus();
|
||||||
|
|
||||||
|
expect(screen.queryByText('Welcome Message')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores comments and misformatted HTML', async () => {
|
it('ignores comments and misformatted HTML', async () => {
|
||||||
@@ -1176,80 +1190,6 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Upgrade Card', () => {
|
|
||||||
it('renders title when upgrade is available', async () => {
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.queryByRole('heading', { name: 'Pursue a verified certificate' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays link to upgrade', async () => {
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('viewing upgrade card sends analytics', async () => {
|
|
||||||
sendTrackEvent.mockClear();
|
|
||||||
sendTrackingLogEvent.mockClear();
|
|
||||||
await fetchAndRender();
|
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('Promotion Viewed', {
|
|
||||||
org_key: 'edX',
|
|
||||||
courserun_key: courseId,
|
|
||||||
creative: 'sidebarupsell',
|
|
||||||
name: 'In-Course Verification Prompt',
|
|
||||||
position: 'sidebar-message',
|
|
||||||
promotion_id: 'courseware_verified_certificate_upsell',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(1);
|
|
||||||
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.bi.course.upgrade.sidebarupsell.displayed', {
|
|
||||||
org_key: 'edX',
|
|
||||||
courserun_key: courseId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clicking upgrade link sends analytics', async () => {
|
|
||||||
await fetchAndRender();
|
|
||||||
|
|
||||||
// Clearing after render to remove any events sent on view (ex. 'Promotion Viewed')
|
|
||||||
sendTrackEvent.mockClear();
|
|
||||||
sendTrackingLogEvent.mockClear();
|
|
||||||
const upgradeButton = screen.getByRole('link', { name: 'Upgrade for $149' });
|
|
||||||
|
|
||||||
fireEvent.click(upgradeButton);
|
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
|
|
||||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'Promotion Clicked', {
|
|
||||||
org_key: 'edX',
|
|
||||||
courserun_key: courseId,
|
|
||||||
creative: 'sidebarupsell',
|
|
||||||
name: 'In-Course Verification Prompt',
|
|
||||||
position: 'sidebar-message',
|
|
||||||
promotion_id: 'courseware_verified_certificate_upsell',
|
|
||||||
});
|
|
||||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.bi.ecommerce.upsell_links_clicked', {
|
|
||||||
org_key: 'edX',
|
|
||||||
courserun_key: courseId,
|
|
||||||
linkCategory: 'green_upgrade',
|
|
||||||
linkName: 'course_home_green',
|
|
||||||
linkType: 'button',
|
|
||||||
pageName: 'course_home',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(2);
|
|
||||||
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(1, 'edx.bi.course.upgrade.sidebarupsell.clicked', {
|
|
||||||
org_key: 'edX',
|
|
||||||
courserun_key: courseId,
|
|
||||||
});
|
|
||||||
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(2, 'edx.course.enrollment.upgrade.clicked', {
|
|
||||||
org_key: 'edX',
|
|
||||||
courserun_key: courseId,
|
|
||||||
location: 'sidebar-message',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Account Activation Alert', () => {
|
describe('Account Activation Alert', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const intersectionObserverMock = () => ({
|
const intersectionObserverMock = () => ({
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Collapsible, IconButton, Icon } from '@openedx/paragon';
|
|
||||||
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
|
|
||||||
import { DisabledVisible } from '@openedx/paragon/icons';
|
|
||||||
import SequenceLink from './SequenceLink';
|
|
||||||
import { useModel } from '../../generic/model-store';
|
|
||||||
|
|
||||||
import genericMessages from '../../generic/messages';
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const Section = ({
|
|
||||||
courseId,
|
|
||||||
defaultOpen,
|
|
||||||
expand,
|
|
||||||
intl,
|
|
||||||
section,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
complete,
|
|
||||||
sequenceIds,
|
|
||||||
title,
|
|
||||||
hideFromTOC,
|
|
||||||
} = section;
|
|
||||||
const {
|
|
||||||
courseBlocks: {
|
|
||||||
sequences,
|
|
||||||
},
|
|
||||||
} = useModel('outline', courseId);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOpen(expand);
|
|
||||||
}, [expand]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOpen(defaultOpen);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sectionTitle = (
|
|
||||||
<div className="d-flex row w-100 m-0">
|
|
||||||
<div className="col-auto p-0">
|
|
||||||
{complete ? (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={fasCheckCircle}
|
|
||||||
fixedWidth
|
|
||||||
className="float-left mt-1 text-success"
|
|
||||||
aria-hidden="true"
|
|
||||||
title={intl.formatMessage(messages.completedSection)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={farCheckCircle}
|
|
||||||
fixedWidth
|
|
||||||
className="float-left mt-1 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
title={intl.formatMessage(messages.incompleteSection)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-7 ml-3 p-0 font-weight-bold text-dark-500">
|
|
||||||
<span className="align-middle col-6">{title}</span>
|
|
||||||
<span className="sr-only">
|
|
||||||
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{hideFromTOC && (
|
|
||||||
<div className="row">
|
|
||||||
{hideFromTOC && (
|
|
||||||
<span className="small d-flex align-content-end">
|
|
||||||
<Icon className="mr-2" src={DisabledVisible} data-testid="hide-from-toc-section-icon" />
|
|
||||||
<span data-testid="hide-from-toc-section-text">
|
|
||||||
{intl.formatMessage(messages.hiddenSection)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<Collapsible
|
|
||||||
className="mb-2"
|
|
||||||
styling="card-lg"
|
|
||||||
title={sectionTitle}
|
|
||||||
open={open}
|
|
||||||
onToggle={() => { setOpen(!open); }}
|
|
||||||
iconWhenClosed={(
|
|
||||||
<IconButton
|
|
||||||
alt={intl.formatMessage(messages.openSection)}
|
|
||||||
icon={faPlus}
|
|
||||||
onClick={() => { setOpen(true); }}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
iconWhenOpen={(
|
|
||||||
<IconButton
|
|
||||||
alt={intl.formatMessage(genericMessages.close)}
|
|
||||||
icon={faMinus}
|
|
||||||
onClick={() => { setOpen(false); }}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ol className="list-unstyled">
|
|
||||||
{sequenceIds.map((sequenceId, index) => (
|
|
||||||
<SequenceLink
|
|
||||||
key={sequenceId}
|
|
||||||
id={sequenceId}
|
|
||||||
courseId={courseId}
|
|
||||||
sequence={sequences[sequenceId]}
|
|
||||||
first={index === 0}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</Collapsible>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Section.propTypes = {
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
defaultOpen: PropTypes.bool.isRequired,
|
|
||||||
expand: PropTypes.bool.isRequired,
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
section: PropTypes.shape().isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(Section);
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
FormattedMessage,
|
|
||||||
FormattedTime,
|
|
||||||
injectIntl,
|
|
||||||
intlShape,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
|
|
||||||
import { Icon } from '@openedx/paragon';
|
|
||||||
import { Block } from '@openedx/paragon/icons';
|
|
||||||
import EffortEstimate from '../../shared/effort-estimate';
|
|
||||||
import { useModel } from '../../generic/model-store';
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const SequenceLink = ({
|
|
||||||
id,
|
|
||||||
intl,
|
|
||||||
courseId,
|
|
||||||
first,
|
|
||||||
sequence,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
complete,
|
|
||||||
description,
|
|
||||||
due,
|
|
||||||
showLink,
|
|
||||||
title,
|
|
||||||
hideFromTOC,
|
|
||||||
} = sequence;
|
|
||||||
const {
|
|
||||||
userTimezone,
|
|
||||||
} = useModel('outline', courseId);
|
|
||||||
|
|
||||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
|
||||||
|
|
||||||
const coursewareUrl = <Link to={`/course/${courseId}/${id}`}>{title}</Link>;
|
|
||||||
const displayTitle = showLink ? coursewareUrl : title;
|
|
||||||
|
|
||||||
const dueDateMessage = (
|
|
||||||
<FormattedMessage
|
|
||||||
id="learning.outline.sequence-due-date-set"
|
|
||||||
defaultMessage="{description} due {assignmentDue}"
|
|
||||||
description="Used below an assignment title"
|
|
||||||
values={{
|
|
||||||
assignmentDue: (
|
|
||||||
<FormattedTime
|
|
||||||
key={`${id}-due`}
|
|
||||||
day="numeric"
|
|
||||||
month="short"
|
|
||||||
year="numeric"
|
|
||||||
timeZoneName="short"
|
|
||||||
value={due}
|
|
||||||
{...timezoneFormatArgs}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: description || '',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const noDueDateMessage = (
|
|
||||||
<FormattedMessage
|
|
||||||
id="learning.outline.sequence-due-date-not-set"
|
|
||||||
defaultMessage="{description}"
|
|
||||||
description="Used below an assignment title"
|
|
||||||
values={{
|
|
||||||
assignmentDue: (
|
|
||||||
<FormattedTime
|
|
||||||
key={`${id}-due`}
|
|
||||||
day="numeric"
|
|
||||||
month="short"
|
|
||||||
year="numeric"
|
|
||||||
timeZoneName="short"
|
|
||||||
value={due}
|
|
||||||
{...timezoneFormatArgs}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: description || '',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
|
||||||
<div className="row w-100 m-0">
|
|
||||||
<div className="col-auto p-0">
|
|
||||||
{complete ? (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={fasCheckCircle}
|
|
||||||
fixedWidth
|
|
||||||
className="float-left text-success mt-1"
|
|
||||||
aria-hidden={complete}
|
|
||||||
title={intl.formatMessage(messages.completedAssignment)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={farCheckCircle}
|
|
||||||
fixedWidth
|
|
||||||
className="float-left text-gray-400 mt-1"
|
|
||||||
aria-hidden={complete}
|
|
||||||
title={intl.formatMessage(messages.incompleteAssignment)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-10 p-0 ml-3 text-break">
|
|
||||||
<span className="align-middle">{displayTitle}</span>
|
|
||||||
<span className="sr-only">
|
|
||||||
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
|
|
||||||
</span>
|
|
||||||
<EffortEstimate className="ml-3 align-middle" block={sequence} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{hideFromTOC && (
|
|
||||||
<div className="row w-100 my-2 mx-4 pl-3">
|
|
||||||
<span className="small d-flex">
|
|
||||||
<Icon className="mr-2" src={Block} data-testid="hide-from-toc-sequence-link-icon" />
|
|
||||||
<span data-testid="hide-from-toc-sequence-link-text">
|
|
||||||
{intl.formatMessage(messages.hiddenSequenceLink)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="row w-100 m-0 ml-3 pl-3">
|
|
||||||
<small className="text-body pl-2">
|
|
||||||
{due ? dueDateMessage : noDueDateMessage}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
SequenceLink.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
first: PropTypes.bool.isRequired,
|
|
||||||
sequence: PropTypes.shape().isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(SequenceLink);
|
|
||||||
@@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import {
|
import {
|
||||||
FormattedDate,
|
FormattedDate,
|
||||||
FormattedMessage,
|
FormattedMessage,
|
||||||
injectIntl,
|
useIntl,
|
||||||
intlShape,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Alert, Button } from '@openedx/paragon';
|
import { Alert, Button } from '@openedx/paragon';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
@@ -25,7 +24,8 @@ export const CERT_STATUS_TYPE = {
|
|||||||
UNVERIFIED: 'unverified',
|
UNVERIFIED: 'unverified',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CertificateStatusAlert = ({ intl, payload }) => {
|
const CertificateStatusAlert = ({ payload }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
const {
|
||||||
certificateAvailableDate,
|
certificateAvailableDate,
|
||||||
@@ -192,7 +192,6 @@ const CertificateStatusAlert = ({ intl, payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
CertificateStatusAlert.propTypes = {
|
CertificateStatusAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
certificateAvailableDate: PropTypes.string,
|
certificateAvailableDate: PropTypes.string,
|
||||||
certStatus: PropTypes.string,
|
certStatus: PropTypes.string,
|
||||||
@@ -210,4 +209,4 @@ CertificateStatusAlert.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CertificateStatusAlert);
|
export default CertificateStatusAlert;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||||
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
@@ -14,7 +14,8 @@ import outlineMessages from '../../messages';
|
|||||||
import useEnrollClickHandler from '../../../../alerts/enrollment-alert/clickHook';
|
import useEnrollClickHandler from '../../../../alerts/enrollment-alert/clickHook';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
const PrivateCourseAlert = ({ intl, payload }) => {
|
const PrivateCourseAlert = ({ payload }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
anonymousUser,
|
anonymousUser,
|
||||||
canEnroll,
|
canEnroll,
|
||||||
@@ -103,7 +104,6 @@ const PrivateCourseAlert = ({ intl, payload }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
PrivateCourseAlert.propTypes = {
|
PrivateCourseAlert.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
payload: PropTypes.shape({
|
payload: PropTypes.shape({
|
||||||
anonymousUser: PropTypes.bool,
|
anonymousUser: PropTypes.bool,
|
||||||
canEnroll: PropTypes.bool,
|
canEnroll: PropTypes.bool,
|
||||||
@@ -111,4 +111,4 @@ PrivateCourseAlert.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(PrivateCourseAlert);
|
export default PrivateCourseAlert;
|
||||||
|
|||||||
@@ -204,122 +204,122 @@ const messages = defineMessages({
|
|||||||
notStartedProctoringStatus: {
|
notStartedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.notStarted',
|
id: 'learning.proctoringPanel.status.notStarted',
|
||||||
defaultMessage: 'Not Started',
|
defaultMessage: 'Not Started',
|
||||||
description: 'It indcate that proctortrack onboarding exam hasn’t started yet',
|
description: 'It indcate that proctored onboarding exam hasn’t started yet',
|
||||||
},
|
},
|
||||||
startedProctoringStatus: {
|
startedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.started',
|
id: 'learning.proctoringPanel.status.started',
|
||||||
defaultMessage: 'Started',
|
defaultMessage: 'Started',
|
||||||
description: 'Label to indicate the starting status of the proctortrack onboarding exam',
|
description: 'Label to indicate the starting status of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
submittedProctoringStatus: {
|
submittedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.submitted',
|
id: 'learning.proctoringPanel.status.submitted',
|
||||||
defaultMessage: 'Submitted',
|
defaultMessage: 'Submitted',
|
||||||
description: 'Label to indicate the submitted status of proctortrack onboarding exam',
|
description: 'Label to indicate the submitted status of proctored onboarding exam',
|
||||||
},
|
},
|
||||||
verifiedProctoringStatus: {
|
verifiedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.verified',
|
id: 'learning.proctoringPanel.status.verified',
|
||||||
defaultMessage: 'Verified',
|
defaultMessage: 'Verified',
|
||||||
description: 'Label to indicate the verified status of the proctortrack onboarding exam',
|
description: 'Label to indicate the verified status of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
rejectedProctoringStatus: {
|
rejectedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.rejected',
|
id: 'learning.proctoringPanel.status.rejected',
|
||||||
defaultMessage: 'Rejected',
|
defaultMessage: 'Rejected',
|
||||||
description: 'Label to indicate the rejection status of the proctortrack onboarding exam',
|
description: 'Label to indicate the rejection status of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
errorProctoringStatus: {
|
errorProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.error',
|
id: 'learning.proctoringPanel.status.error',
|
||||||
defaultMessage: 'Error',
|
defaultMessage: 'Error',
|
||||||
description: 'Label to indicate that there is error in proctortrack onboarding exam',
|
description: 'Label to indicate that there is error in proctored onboarding exam',
|
||||||
},
|
},
|
||||||
otherCourseApprovedProctoringStatus: {
|
otherCourseApprovedProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.otherCourseApproved',
|
id: 'learning.proctoringPanel.status.otherCourseApproved',
|
||||||
defaultMessage: 'Approved in Another Course',
|
defaultMessage: 'Approved in Another Course',
|
||||||
description: 'Label to indicate that the proctortrack onboarding exam is verified based on taking onboarding exam on another course',
|
description: 'Label to indicate that the proctored onboarding exam is verified based on taking onboarding exam on another course',
|
||||||
},
|
},
|
||||||
expiringSoonProctoringStatus: {
|
expiringSoonProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.expiringSoon',
|
id: 'learning.proctoringPanel.status.expiringSoon',
|
||||||
defaultMessage: 'Expiring Soon',
|
defaultMessage: 'Expiring Soon',
|
||||||
description: 'A label to indicate that proctortrack onboarding exam will expire soon',
|
description: 'A label to indicate that proctored onboarding exam will expire soon',
|
||||||
},
|
},
|
||||||
expiredProctoringStatus: {
|
expiredProctoringStatus: {
|
||||||
id: 'learning.proctoringPanel.status.expired',
|
id: 'learning.proctoringPanel.status.expired',
|
||||||
defaultMessage: 'Expired',
|
defaultMessage: 'Expired',
|
||||||
description: 'A label to indicate that proctortrack onboarding exam has expired',
|
description: 'A label to indicate that proctored onboarding exam has expired',
|
||||||
},
|
},
|
||||||
proctoringCurrentStatus: {
|
proctoringCurrentStatus: {
|
||||||
id: 'learning.proctoringPanel.status',
|
id: 'learning.proctoringPanel.status',
|
||||||
defaultMessage: 'Current Onboarding Status:',
|
defaultMessage: 'Current Onboarding Status:',
|
||||||
description: 'The text that precede the status label of proctortrack onboarding exam',
|
description: 'The text that precede the status label of proctored onboarding exam',
|
||||||
},
|
},
|
||||||
notStartedProctoringMessage: {
|
notStartedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.notStarted',
|
id: 'learning.proctoringPanel.message.notStarted',
|
||||||
defaultMessage: 'You have not started your onboarding exam.',
|
defaultMessage: 'You have not started your onboarding exam.',
|
||||||
description: 'The text that explain the meaning of (not started) label of the proctortrack onboarding exam',
|
description: 'The text that explain the meaning of (not started) label of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
startedProctoringMessage: {
|
startedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.started',
|
id: 'learning.proctoringPanel.message.started',
|
||||||
defaultMessage: 'You have started your onboarding exam.',
|
defaultMessage: 'You have started your onboarding exam.',
|
||||||
description: 'The text that explain the meaning of (started) label of the proctortrack onboarding exam',
|
description: 'The text that explain the meaning of (started) label of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
submittedProctoringMessage: {
|
submittedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.submitted',
|
id: 'learning.proctoringPanel.message.submitted',
|
||||||
defaultMessage: 'You have submitted your onboarding exam.',
|
defaultMessage: 'You have submitted your onboarding exam.',
|
||||||
description: 'The text that explain the meaning of (submitted) label of the proctortrack onboarding exam',
|
description: 'The text that explain the meaning of (submitted) label of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
verifiedProctoringMessage: {
|
verifiedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.verified',
|
id: 'learning.proctoringPanel.message.verified',
|
||||||
defaultMessage: 'Your onboarding exam has been approved in this course.',
|
defaultMessage: 'Your onboarding exam has been approved in this course.',
|
||||||
description: 'The text that explain the meaning of (verified) label of the proctortrack onboarding exam',
|
description: 'The text that explain the meaning of (verified) label of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
rejectedProctoringMessage: {
|
rejectedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.rejected',
|
id: 'learning.proctoringPanel.message.rejected',
|
||||||
defaultMessage: 'Your onboarding exam has been rejected. Please retry onboarding.',
|
defaultMessage: 'Your onboarding exam has been rejected. Please retry onboarding.',
|
||||||
description: 'The text that explain the meaning of (rejected) label of the proctortrack onboarding exam',
|
description: 'The text that explain the meaning of (rejected) label of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
errorProctoringMessage: {
|
errorProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.error',
|
id: 'learning.proctoringPanel.message.error',
|
||||||
defaultMessage: 'An error has occurred during your onboarding exam. Please retry onboarding.',
|
defaultMessage: 'An error has occurred during your onboarding exam. Please retry onboarding.',
|
||||||
description: 'The text that explain the meaning of (error) label of the proctortrack onboarding exam',
|
description: 'The text that explain the meaning of (error) label of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
otherCourseApprovedProctoringMessage: {
|
otherCourseApprovedProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.otherCourseApproved',
|
id: 'learning.proctoringPanel.message.otherCourseApproved',
|
||||||
defaultMessage: 'Your onboarding exam has been approved in another course.',
|
defaultMessage: 'Your onboarding exam has been approved in another course.',
|
||||||
description: 'The text that explain the meaning of (approved in another course) label of the proctortrack onboarding exam',
|
description: 'The text that explain the meaning of (approved in another course) label of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
otherCourseApprovedProctoringDetail: {
|
otherCourseApprovedProctoringDetail: {
|
||||||
id: 'learning.proctoringPanel.detail.otherCourseApproved',
|
id: 'learning.proctoringPanel.detail.otherCourseApproved',
|
||||||
defaultMessage: '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.',
|
defaultMessage: '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.',
|
||||||
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (approved in another course)',
|
description: 'The text that recommend an action when the status of the proctored onboarding exam is (approved in another course)',
|
||||||
},
|
},
|
||||||
expiringSoonProctoringMessage: {
|
expiringSoonProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.expiringSoon',
|
id: 'learning.proctoringPanel.message.expiringSoon',
|
||||||
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.',
|
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)',
|
description: 'The text that recommend an action when the status of the proctored onboarding exam is (expiring soon)',
|
||||||
},
|
},
|
||||||
expiredProctoringMessage: {
|
expiredProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.expired',
|
id: 'learning.proctoringPanel.message.expired',
|
||||||
defaultMessage: 'Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.',
|
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)',
|
description: 'The text that recommend an action when the status of the proctored onboarding exam is (expired)',
|
||||||
},
|
},
|
||||||
proctoringPanelGeneralInfo: {
|
proctoringPanelGeneralInfo: {
|
||||||
id: 'learning.proctoringPanel.generalInfo',
|
id: 'learning.proctoringPanel.generalInfo',
|
||||||
defaultMessage: 'You must complete the onboarding process prior to taking any proctored exam. ',
|
defaultMessage: 'You must complete the onboarding process prior to taking any proctored exam. ',
|
||||||
description: 'It indicate key and important fact to learner about the importance of taking proctortrack onboarding exam',
|
description: 'It indicate key and important fact to learner about the importance of taking proctored onboarding exam',
|
||||||
},
|
},
|
||||||
proctoringPanelGeneralInfoSubmitted: {
|
proctoringPanelGeneralInfoSubmitted: {
|
||||||
id: 'learning.proctoringPanel.generalInfoSubmitted',
|
id: 'learning.proctoringPanel.generalInfoSubmitted',
|
||||||
defaultMessage: 'Your submitted profile is in review.',
|
defaultMessage: 'Your submitted profile is in review.',
|
||||||
description: 'The text that explain the meaning of (in review) label of the proctortrack onboarding exam',
|
description: 'The text that explain the meaning of (in review) label of the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
proctoringPanelGeneralTime: {
|
proctoringPanelGeneralTime: {
|
||||||
id: 'learning.proctoringPanel.generalTime',
|
id: 'learning.proctoringPanel.generalTime',
|
||||||
defaultMessage: 'Onboarding profile review can take 2+ business days.',
|
defaultMessage: 'Onboarding profile review can take 2+ business days.',
|
||||||
description: 'This text explain for how long the (in review) status of the proctortrack onboarding exam might remain',
|
description: 'This text explain for how long the (in review) status of the proctored onboarding exam might remain',
|
||||||
},
|
},
|
||||||
proctoringOnboardingButton: {
|
proctoringOnboardingButton: {
|
||||||
id: 'learning.proctoringPanel.onboardingButton',
|
id: 'learning.proctoringPanel.onboardingButton',
|
||||||
defaultMessage: 'Complete Onboarding',
|
defaultMessage: 'Complete Onboarding',
|
||||||
description: 'Text shown on the button that starts the actual proctortrack onboarding exam when it is released',
|
description: 'Text shown on the button that starts the actual proctored onboarding exam when it is released',
|
||||||
},
|
},
|
||||||
proctoringOnboardingPracticeButton: {
|
proctoringOnboardingPracticeButton: {
|
||||||
id: 'learning.proctoringPanel.onboardingPracticeButton',
|
id: 'learning.proctoringPanel.onboardingPracticeButton',
|
||||||
@@ -329,17 +329,27 @@ const messages = defineMessages({
|
|||||||
proctoringOnboardingButtonNotOpen: {
|
proctoringOnboardingButtonNotOpen: {
|
||||||
id: 'learning.proctoringPanel.onboardingButtonNotOpen',
|
id: 'learning.proctoringPanel.onboardingButtonNotOpen',
|
||||||
defaultMessage: 'Onboarding Opens: {releaseDate}',
|
defaultMessage: 'Onboarding Opens: {releaseDate}',
|
||||||
description: 'It indicate when or from when the learner can take the proctortrack onboarding exam',
|
description: 'It indicate when or from when the learner can take the proctored onboarding exam',
|
||||||
},
|
},
|
||||||
proctoringReviewRequirementsButton: {
|
proctoringReviewRequirementsButton: {
|
||||||
id: 'learning.proctoringPanel.reviewRequirementsButton',
|
id: 'learning.proctoringPanel.reviewRequirementsButton',
|
||||||
defaultMessage: 'Review instructions and system requirements',
|
defaultMessage: 'Review instructions and system requirements',
|
||||||
description: 'Anchor text for button which redirect leaner to doc or a detailed page about proctortrack onboarding exam',
|
description: 'Anchor text for button which redirect leaner to doc or a detailed page about proctored onboarding exam',
|
||||||
},
|
},
|
||||||
proctoringOnboardingButtonPastDue: {
|
proctoringOnboardingButtonPastDue: {
|
||||||
id: 'learning.proctoringPanel.onboardingButtonPastDue',
|
id: 'learning.proctoringPanel.onboardingButtonPastDue',
|
||||||
defaultMessage: 'Onboarding Past Due',
|
defaultMessage: 'Onboarding Past Due',
|
||||||
description: 'Text that show when the deadline of proctortrack onboarding exam has passed, it appears on button that start the onboarding exam however for this case the button is disabled for obvious reason',
|
description: 'Text that show when the deadline of proctored onboarding exam has passed, it appears on button that start the onboarding exam however for this case the button is disabled for obvious reason',
|
||||||
|
},
|
||||||
|
sequenceDueDate: {
|
||||||
|
id: 'learning.outline.sequence-due-date-set',
|
||||||
|
defaultMessage: '{description} due {assignmentDue}',
|
||||||
|
description: 'Used below an assignment title',
|
||||||
|
},
|
||||||
|
sequenceNoDueDate: {
|
||||||
|
id: 'learning.outline.sequence-due-date-not-set',
|
||||||
|
defaultMessage: '{description}',
|
||||||
|
description: 'Used below an assignment title',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Icon } from '@openedx/paragon';
|
||||||
|
import { Block } from '@openedx/paragon/icons';
|
||||||
|
|
||||||
|
import messages from '../messages';
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
const HiddenSequenceLink: React.FC<Props> = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row w-100 my-2 mx-4 pl-3">
|
||||||
|
<span className="small d-flex">
|
||||||
|
<Icon className="mr-2" src={Block} data-testid="hide-from-toc-sequence-link-icon" />
|
||||||
|
<span data-testid="hide-from-toc-sequence-link-text">
|
||||||
|
{intl.formatMessage(messages.hiddenSequenceLink)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HiddenSequenceLink;
|
||||||
94
src/course-home/outline-tab/section-outline/Section.tsx
Normal file
94
src/course-home/outline-tab/section-outline/Section.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Collapsible, IconButton } from '@openedx/paragon';
|
||||||
|
import { Minus, Plus } from '@openedx/paragon/icons';
|
||||||
|
|
||||||
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
import genericMessages from '../../../generic/messages';
|
||||||
|
import { useContextId } from '../../../data/hooks';
|
||||||
|
import messages from '../messages';
|
||||||
|
import SectionTitle from './SectionTitle';
|
||||||
|
import SequenceLink from './SequenceLink';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
defaultOpen: boolean;
|
||||||
|
expand: boolean;
|
||||||
|
section: {
|
||||||
|
complete: boolean;
|
||||||
|
sequenceIds: string[];
|
||||||
|
title: string;
|
||||||
|
hideFromTOC: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Section: React.FC<Props> = ({
|
||||||
|
defaultOpen,
|
||||||
|
expand,
|
||||||
|
section,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const courseId = useContextId();
|
||||||
|
const {
|
||||||
|
complete,
|
||||||
|
sequenceIds,
|
||||||
|
title,
|
||||||
|
hideFromTOC,
|
||||||
|
} = section;
|
||||||
|
const {
|
||||||
|
courseBlocks: {
|
||||||
|
sequences,
|
||||||
|
},
|
||||||
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(defaultOpen);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(expand);
|
||||||
|
}, [expand]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(defaultOpen);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<Collapsible
|
||||||
|
className="mb-2"
|
||||||
|
styling="card-lg"
|
||||||
|
title={<SectionTitle {...{ complete, hideFromTOC, title }} />}
|
||||||
|
open={open}
|
||||||
|
onToggle={() => { setOpen(!open); }}
|
||||||
|
iconWhenClosed={(
|
||||||
|
<IconButton
|
||||||
|
alt={intl.formatMessage(messages.openSection)}
|
||||||
|
iconAs={Plus}
|
||||||
|
onClick={() => { setOpen(true); }}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
iconWhenOpen={(
|
||||||
|
<IconButton
|
||||||
|
alt={intl.formatMessage(genericMessages.close)}
|
||||||
|
iconAs={Minus}
|
||||||
|
onClick={() => { setOpen(false); }}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ol className="list-unstyled">
|
||||||
|
{sequenceIds.map((sequenceId, index) => (
|
||||||
|
<SequenceLink
|
||||||
|
key={sequenceId}
|
||||||
|
id={sequenceId}
|
||||||
|
sequence={sequences[sequenceId]}
|
||||||
|
first={index === 0}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</Collapsible>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Section;
|
||||||
59
src/course-home/outline-tab/section-outline/SectionTitle.tsx
Normal file
59
src/course-home/outline-tab/section-outline/SectionTitle.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Icon } from '@openedx/paragon';
|
||||||
|
import { CheckCircle, CheckCircleOutline, DisabledVisible } from '@openedx/paragon/icons';
|
||||||
|
|
||||||
|
import messages from '../messages';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
complete: boolean;
|
||||||
|
hideFromTOC: boolean;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SectionTitle: React.FC<Props> = ({ complete, hideFromTOC, title }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
<div className="d-flex row w-100 m-0">
|
||||||
|
<div className="col-auto p-0">
|
||||||
|
{complete ? (
|
||||||
|
<Icon
|
||||||
|
src={CheckCircle}
|
||||||
|
className="float-left mt-1 text-success"
|
||||||
|
aria-hidden="true"
|
||||||
|
svgAttrs={{ 'aria-label': intl.formatMessage(messages.completedSection) }}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
src={CheckCircleOutline}
|
||||||
|
className="float-left mt-1 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
svgAttrs={{ 'aria-label': intl.formatMessage(messages.incompleteSection) }}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-7 ml-3 p-0 font-weight-bold text-dark-500">
|
||||||
|
<span className="align-middle col-6">{title}</span>
|
||||||
|
<span className="sr-only">
|
||||||
|
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{hideFromTOC && (
|
||||||
|
<div className="row">
|
||||||
|
{hideFromTOC && (
|
||||||
|
<span className="small d-flex align-content-end">
|
||||||
|
<Icon className="mr-2" src={DisabledVisible} data-testid="hide-from-toc-section-icon" />
|
||||||
|
<span data-testid="hide-from-toc-section-text">
|
||||||
|
{intl.formatMessage(messages.hiddenSection)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionTitle;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedTime, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
|
import { useContextId } from '../../../data/hooks';
|
||||||
|
import messages from '../messages';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
due: string;
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SequenceDueDate: React.FC<Props> = ({
|
||||||
|
due,
|
||||||
|
id,
|
||||||
|
description,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const courseId = useContextId();
|
||||||
|
let dueDateMessage: string | React.ReactNode = intl.formatMessage(
|
||||||
|
messages.sequenceNoDueDate,
|
||||||
|
{ description: description || '' },
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
userTimezone,
|
||||||
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
|
if (due) {
|
||||||
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
|
|
||||||
|
dueDateMessage = intl.formatMessage(
|
||||||
|
messages.sequenceDueDate,
|
||||||
|
{
|
||||||
|
assignmentDue: (
|
||||||
|
<FormattedTime
|
||||||
|
key={`${id}-due`}
|
||||||
|
day="numeric"
|
||||||
|
month="short"
|
||||||
|
year="numeric"
|
||||||
|
timeZoneName="short"
|
||||||
|
value={due}
|
||||||
|
{...timezoneFormatArgs}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: description || '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row w-100 m-0 ml-3 pl-3">
|
||||||
|
<small className="text-body pl-2">
|
||||||
|
{dueDateMessage}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SequenceDueDate;
|
||||||
56
src/course-home/outline-tab/section-outline/SequenceLink.tsx
Normal file
56
src/course-home/outline-tab/section-outline/SequenceLink.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import SequenceDueDate from './SequenceDueDate';
|
||||||
|
import HiddenSequenceLink from './HiddenSequenceLink';
|
||||||
|
import SequenceTitle from './SequenceTitle';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
first: boolean;
|
||||||
|
sequence: {
|
||||||
|
complete: boolean;
|
||||||
|
description: string;
|
||||||
|
due: string;
|
||||||
|
showLink: boolean;
|
||||||
|
title: string;
|
||||||
|
hideFromTOC: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SequenceLink: React.FC<Props> = ({
|
||||||
|
id,
|
||||||
|
first,
|
||||||
|
sequence,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
complete,
|
||||||
|
description,
|
||||||
|
due,
|
||||||
|
showLink,
|
||||||
|
title,
|
||||||
|
hideFromTOC,
|
||||||
|
} = sequence;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
||||||
|
<SequenceTitle
|
||||||
|
{...{
|
||||||
|
complete,
|
||||||
|
showLink,
|
||||||
|
title,
|
||||||
|
sequence,
|
||||||
|
id,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{hideFromTOC && (
|
||||||
|
<HiddenSequenceLink />
|
||||||
|
)}
|
||||||
|
<SequenceDueDate {...{ due, id, description }} />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SequenceLink;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Icon } from '@openedx/paragon';
|
||||||
|
import { CheckCircleOutline, CheckCircle } from '@openedx/paragon/icons';
|
||||||
|
|
||||||
|
import EffortEstimate from '../../../shared/effort-estimate';
|
||||||
|
import messages from '../messages';
|
||||||
|
import { useContextId } from '../../../data/hooks';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
complete: boolean;
|
||||||
|
showLink: boolean;
|
||||||
|
title: string;
|
||||||
|
sequence: object;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SequenceTitle: React.FC<Props> = ({
|
||||||
|
complete,
|
||||||
|
showLink,
|
||||||
|
title,
|
||||||
|
sequence,
|
||||||
|
id,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const courseId = useContextId();
|
||||||
|
const coursewareUrl = <Link to={`/course/${courseId}/${id}`}>{title}</Link>;
|
||||||
|
const displayTitle = showLink ? coursewareUrl : title;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row w-100 m-0">
|
||||||
|
<div className="col-auto p-0">
|
||||||
|
{complete ? (
|
||||||
|
<Icon
|
||||||
|
src={CheckCircle}
|
||||||
|
className="float-left text-success mt-1"
|
||||||
|
aria-hidden={complete}
|
||||||
|
svgAttrs={{ 'aria-label': intl.formatMessage(messages.completedAssignment) }}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
src={CheckCircleOutline}
|
||||||
|
className="float-left text-gray-400 mt-1"
|
||||||
|
aria-hidden={complete}
|
||||||
|
svgAttrs={{ 'aria-label': intl.formatMessage(messages.incompleteAssignment) }}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-10 p-0 ml-3 text-break">
|
||||||
|
<span className="align-middle">{displayTitle}</span>
|
||||||
|
<span className="sr-only">
|
||||||
|
, {intl.formatMessage(complete ? messages.completedAssignment : messages.incompleteAssignment)}
|
||||||
|
</span>
|
||||||
|
<EffortEstimate className="ml-3 align-middle" block={sequence} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SequenceTitle;
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import DateSummary from '../DateSummary';
|
import DateSummary from '../DateSummary';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
const CourseDates = ({
|
const CourseDates = () => {
|
||||||
intl,
|
const intl = useIntl();
|
||||||
}) => {
|
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -40,7 +39,7 @@ const CourseDates = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
<a className="font-weight-bold ml-4 pl-1 small" href={datesTabLink}>
|
<a id="dates-tab-link" className="font-weight-bold ml-4 pl-1 small" href={datesTabLink}>
|
||||||
{intl.formatMessage(messages.allDates)}
|
{intl.formatMessage(messages.allDates)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,8 +47,4 @@ const CourseDates = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseDates.propTypes = {
|
export default CourseDates;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CourseDates);
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import LmsHtmlFragment from '../LmsHtmlFragment';
|
import LmsHtmlFragment from '../LmsHtmlFragment';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
const CourseHandouts = ({ intl }) => {
|
const CourseHandouts = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -31,8 +32,4 @@ const CourseHandouts = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseHandouts.propTypes = {
|
export default CourseHandouts;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CourseHandouts);
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
|||||||
|
|
||||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import {
|
import {
|
||||||
faBookmark, faCertificate, faInfo, faCalendar, faStar,
|
faBookmark, faCertificate, faInfo, faCalendar, faStar,
|
||||||
@@ -14,7 +14,8 @@ import messages from '../messages';
|
|||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton';
|
import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton';
|
||||||
|
|
||||||
const CourseTools = ({ intl }) => {
|
const CourseTools = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -81,8 +82,4 @@ const CourseTools = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseTools.propTypes = {
|
export default CourseTools;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CourseTools);
|
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
@import "~@edx/brand/paragon/variables";
|
|
||||||
@import "~@openedx/paragon/scss/core/core";
|
|
||||||
@import "~@edx/brand/paragon/overrides";
|
|
||||||
|
|
||||||
.flag-button {
|
.flag-button {
|
||||||
background-color: $white;
|
background-color: var(--pgn-color-white);
|
||||||
border: 1px solid $light-400;
|
border: 1px solid var(--pgn-color-light-400);
|
||||||
border-radius: .2rem;
|
border-radius: .2rem;
|
||||||
box-shadow: 0 0 0 2px $light-400;
|
box-shadow: 0 0 0 2px var(--pgn-color-light-400);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid $primary-300;
|
border: 1px solid var(--pgn-color-primary-300);
|
||||||
box-shadow: 0 0 0 2px $white;
|
box-shadow: 0 0 0 2px var(--pgn-color-white);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flag-button-selected {
|
.flag-button-selected {
|
||||||
border: 1px solid $primary-300;
|
border: 1px solid var(--pgn-color-primary-300);
|
||||||
box-shadow: 0 0 0 2px $primary-300;
|
box-shadow: 0 0 0 2px var(--pgn-color-primary-300);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
// These flag svgs are derivatives of the Flag icon from paragon
|
// These flag svgs are derivatives of the Flag icon from paragon
|
||||||
import { ReactComponent as FlagIntenseIcon } from './flag_black.svg';
|
import { ReactComponent as FlagIntenseIcon } from './flag_black.svg';
|
||||||
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
|
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
|
||||||
@@ -13,8 +13,8 @@ const LearningGoalButton = ({
|
|||||||
level,
|
level,
|
||||||
isSelected,
|
isSelected,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
intl,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const buttonDetails = {
|
const buttonDetails = {
|
||||||
casual: {
|
casual: {
|
||||||
daysPerWeek: 1,
|
daysPerWeek: 1,
|
||||||
@@ -53,7 +53,6 @@ LearningGoalButton.propTypes = {
|
|||||||
level: PropTypes.string.isRequired,
|
level: PropTypes.string.isRequired,
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
handleSelect: PropTypes.func.isRequired,
|
handleSelect: PropTypes.func.isRequired,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(LearningGoalButton);
|
export default LearningGoalButton;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import camelCase from 'lodash.camelcase';
|
import camelCase from 'lodash.camelcase';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { getExternalLinkUrl } from '@edx/frontend-platform';
|
||||||
import { Button } from '@openedx/paragon';
|
import { Button } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
@@ -10,7 +11,8 @@ import { getProctoringInfoData } from '../../data/api';
|
|||||||
import { fetchProctoringInfoResolved } from '../../data/slice';
|
import { fetchProctoringInfoResolved } from '../../data/slice';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
const ProctoringInfoPanel = ({ intl }) => {
|
const ProctoringInfoPanel = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -206,7 +208,7 @@ const ProctoringInfoPanel = ({ intl }) => {
|
|||||||
{isSubmissionRequired(readableStatus) && (
|
{isSubmissionRequired(readableStatus) && (
|
||||||
onboardingExamButton
|
onboardingExamButton
|
||||||
)}
|
)}
|
||||||
<Button variant="outline-primary" block href="https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams">
|
<Button variant="outline-primary" block href={getExternalLinkUrl('https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams')}>
|
||||||
{intl.formatMessage(messages.proctoringReviewRequirementsButton)}
|
{intl.formatMessage(messages.proctoringReviewRequirementsButton)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,8 +218,4 @@ const ProctoringInfoPanel = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ProctoringInfoPanel.propTypes = {
|
export default ProctoringInfoPanel;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(ProctoringInfoPanel);
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
.outline-sidebar-proctoring-panel {
|
.outline-sidebar-proctoring-panel {
|
||||||
border: 1px solid $dark-500;
|
border: 1px solid var(--pgn-color-dark-500);
|
||||||
border-top: 5px solid $brand-600;
|
border-top: 5px solid var(--pgn-color-brand-600);
|
||||||
}
|
}
|
||||||
.proctoring-onboarding-success {
|
.proctoring-onboarding-success {
|
||||||
border-top: 5px solid $primary-500;
|
border-top: 5px solid var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
.proctoring-onboarding-submitted {
|
.proctoring-onboarding-submitted {
|
||||||
border-top: 5px solid $dark-500;
|
border-top: 5px solid var(--pgn-color-dark-500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Card } from '@openedx/paragon';
|
import { Button, Card } from '@openedx/paragon';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
const StartOrResumeCourseCard = ({ intl }) => {
|
const StartOrResumeCourseCard = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -62,8 +63,4 @@ const StartOrResumeCourseCard = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
StartOrResumeCourseCard.propTypes = {
|
export default StartOrResumeCourseCard;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(StartOrResumeCourseCard);
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Form, Card, Icon } from '@openedx/paragon';
|
|||||||
import { history } from '@edx/frontend-platform';
|
import { history } from '@edx/frontend-platform';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Email } from '@openedx/paragon/icons';
|
import { Email } from '@openedx/paragon/icons';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
@@ -18,8 +18,8 @@ import './FlagButton.scss';
|
|||||||
const WeeklyLearningGoalCard = ({
|
const WeeklyLearningGoalCard = ({
|
||||||
daysPerWeek,
|
daysPerWeek,
|
||||||
subscribedToReminders,
|
subscribedToReminders,
|
||||||
intl,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
@@ -152,11 +152,10 @@ const WeeklyLearningGoalCard = ({
|
|||||||
WeeklyLearningGoalCard.propTypes = {
|
WeeklyLearningGoalCard.propTypes = {
|
||||||
daysPerWeek: PropTypes.number,
|
daysPerWeek: PropTypes.number,
|
||||||
subscribedToReminders: PropTypes.bool,
|
subscribedToReminders: PropTypes.bool,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
WeeklyLearningGoalCard.defaultProps = {
|
WeeklyLearningGoalCard.defaultProps = {
|
||||||
daysPerWeek: null,
|
daysPerWeek: null,
|
||||||
subscribedToReminders: false,
|
subscribedToReminders: false,
|
||||||
};
|
};
|
||||||
export default injectIntl(WeeklyLearningGoalCard);
|
export default WeeklyLearningGoalCard;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import { useState, useMemo, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Alert, Button, TransitionReplace } from '@openedx/paragon';
|
import { Alert, Button, TransitionReplace } from '@openedx/paragon';
|
||||||
import truncate from 'truncate-html';
|
import truncate from 'truncate-html';
|
||||||
|
|
||||||
@@ -11,11 +11,13 @@ import messages from '../messages';
|
|||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import { dismissWelcomeMessage } from '../../data/thunks';
|
import { dismissWelcomeMessage } from '../../data/thunks';
|
||||||
|
|
||||||
const WelcomeMessage = ({ courseId, intl }) => {
|
const WelcomeMessage = ({ courseId, nextElementRef }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
welcomeMessageHtml,
|
welcomeMessageHtml,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
|
const messageBodyRef = useRef();
|
||||||
const [display, setDisplay] = useState(true);
|
const [display, setDisplay] = useState(true);
|
||||||
|
|
||||||
// welcomeMessageHtml can contain comments or malformatted HTML which can impact the length that determines
|
// welcomeMessageHtml can contain comments or malformatted HTML which can impact the length that determines
|
||||||
@@ -49,13 +51,20 @@ const WelcomeMessage = ({ courseId, intl }) => {
|
|||||||
dismissible
|
dismissible
|
||||||
show={display}
|
show={display}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
nextElementRef.current?.focus();
|
||||||
setDisplay(false);
|
setDisplay(false);
|
||||||
dispatch(dismissWelcomeMessage(courseId));
|
dispatch(dismissWelcomeMessage(courseId));
|
||||||
}}
|
}}
|
||||||
className="raised-card"
|
className="raised-card"
|
||||||
actions={messageCanBeShortened ? [
|
actions={messageCanBeShortened ? [
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowShortMessage(!showShortMessage)}
|
onClick={() => {
|
||||||
|
if (showShortMessage) {
|
||||||
|
messageBodyRef.current?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowShortMessage(!showShortMessage);
|
||||||
|
}}
|
||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
>
|
>
|
||||||
{showShortMessage ? intl.formatMessage(messages.welcomeMessageShowMoreButton)
|
{showShortMessage ? intl.formatMessage(messages.welcomeMessageShowMoreButton)
|
||||||
@@ -63,32 +72,34 @@ const WelcomeMessage = ({ courseId, intl }) => {
|
|||||||
</Button>,
|
</Button>,
|
||||||
] : []}
|
] : []}
|
||||||
>
|
>
|
||||||
<TransitionReplace className="mb-3" enterDuration={400} exitDuration={200}>
|
<div ref={messageBodyRef} tabIndex="-1">
|
||||||
{showShortMessage ? (
|
<TransitionReplace className="mb-3" enterDuration={400} exitDuration={200}>
|
||||||
<LmsHtmlFragment
|
{showShortMessage ? (
|
||||||
className="inline-link"
|
<LmsHtmlFragment
|
||||||
data-testid="short-welcome-message-iframe"
|
className="inline-link"
|
||||||
key="short-html"
|
data-testid="short-welcome-message-iframe"
|
||||||
html={shortWelcomeMessageHtml}
|
key="short-html"
|
||||||
title={intl.formatMessage(messages.welcomeMessage)}
|
html={shortWelcomeMessageHtml}
|
||||||
/>
|
title={intl.formatMessage(messages.welcomeMessage)}
|
||||||
) : (
|
/>
|
||||||
<LmsHtmlFragment
|
) : (
|
||||||
className="inline-link"
|
<LmsHtmlFragment
|
||||||
data-testid="long-welcome-message-iframe"
|
className="inline-link"
|
||||||
key="full-html"
|
data-testid="long-welcome-message-iframe"
|
||||||
html={cleanedWelcomeMessageHtml}
|
key="full-html"
|
||||||
title={intl.formatMessage(messages.welcomeMessage)}
|
html={cleanedWelcomeMessageHtml}
|
||||||
/>
|
title={intl.formatMessage(messages.welcomeMessage)}
|
||||||
)}
|
/>
|
||||||
</TransitionReplace>
|
)}
|
||||||
|
</TransitionReplace>
|
||||||
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
WelcomeMessage.propTypes = {
|
WelcomeMessage.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
intl: intlShape.isRequired,
|
nextElementRef: PropTypes.shape({ current: PropTypes.instanceOf(HTMLInputElement) }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(WelcomeMessage);
|
export default WelcomeMessage;
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@openedx/paragon';
|
import { Button } from '@openedx/paragon';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const ProgressHeader = ({ intl }) => {
|
const ProgressHeader = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
targetUserId,
|
targetUserId,
|
||||||
@@ -37,8 +36,4 @@ const ProgressHeader = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ProgressHeader.propTypes = {
|
export default ProgressHeader;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(ProgressHeader);
|
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useWindowSize } from '@openedx/paragon';
|
||||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
import { useContextId } from '../../data/hooks';
|
||||||
|
|
||||||
import CertificateStatus from './certificate-status/CertificateStatus';
|
|
||||||
import CourseCompletion from './course-completion/CourseCompletion';
|
|
||||||
import CourseGrade from './grades/course-grade/CourseGrade';
|
|
||||||
import DetailedGrades from './grades/detailed-grades/DetailedGrades';
|
|
||||||
import GradeSummary from './grades/grade-summary/GradeSummary';
|
|
||||||
import ProgressHeader from './ProgressHeader';
|
|
||||||
import RelatedLinks from './related-links/RelatedLinks';
|
|
||||||
|
|
||||||
import { useModel } from '../../generic/model-store';
|
import { useModel } from '../../generic/model-store';
|
||||||
|
import ProgressTabCertificateStatusSidePanelSlot from '../../plugin-slots/ProgressTabCertificateStatusSidePanelSlot';
|
||||||
|
|
||||||
|
import CourseCompletion from './course-completion/CourseCompletion';
|
||||||
|
import ProgressHeader from './ProgressHeader';
|
||||||
|
|
||||||
|
import ProgressTabCertificateStatusMainBodySlot from '../../plugin-slots/ProgressTabCertificateStatusMainBodySlot';
|
||||||
|
import ProgressTabCourseGradeSlot from '../../plugin-slots/ProgressTabCourseGradeSlot';
|
||||||
|
import ProgressTabGradeBreakdownSlot from '../../plugin-slots/ProgressTabGradeBreakdownSlot';
|
||||||
|
import ProgressTabRelatedLinksSlot from '../../plugin-slots/ProgressTabRelatedLinksSlot';
|
||||||
|
import { useGetExamsData } from './hooks';
|
||||||
|
|
||||||
const ProgressTab = () => {
|
const ProgressTab = () => {
|
||||||
const {
|
const courseId = useContextId();
|
||||||
courseId,
|
const { disableProgressGraph, sectionScores } = useModel('progress', courseId);
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const sequenceIds = useMemo(() => (
|
||||||
gradesFeatureIsFullyLocked, disableProgressGraph,
|
sectionScores.flatMap((section) => (section.subsections)).map((subsection) => subsection.blockKey)
|
||||||
} = useModel('progress', courseId);
|
), [sectionScores]);
|
||||||
|
|
||||||
const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
useGetExamsData(courseId, sequenceIds);
|
||||||
|
|
||||||
const windowWidth = useWindowSize().width;
|
const windowWidth = useWindowSize().width;
|
||||||
if (windowWidth === undefined) {
|
if (windowWidth === undefined) {
|
||||||
@@ -31,7 +31,6 @@ const ProgressTab = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wideScreen = windowWidth >= breakpoints.large.minWidth;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProgressHeader />
|
<ProgressHeader />
|
||||||
@@ -39,18 +38,15 @@ const ProgressTab = () => {
|
|||||||
{/* Main body */}
|
{/* Main body */}
|
||||||
<div className="col-12 col-md-8 p-0">
|
<div className="col-12 col-md-8 p-0">
|
||||||
{!disableProgressGraph && <CourseCompletion />}
|
{!disableProgressGraph && <CourseCompletion />}
|
||||||
{!wideScreen && <CertificateStatus />}
|
<ProgressTabCertificateStatusMainBodySlot />
|
||||||
<CourseGrade />
|
<ProgressTabCourseGradeSlot />
|
||||||
<div className={`grades my-4 p-4 rounded raised-card ${applyLockedOverlay}`} aria-hidden={gradesFeatureIsFullyLocked}>
|
<ProgressTabGradeBreakdownSlot />
|
||||||
<GradeSummary />
|
|
||||||
<DetailedGrades />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Side panel */}
|
{/* Side panel */}
|
||||||
<div className="col-12 col-md-4 p-0 px-md-4">
|
<div className="col-12 col-md-4 p-0 px-md-4">
|
||||||
{wideScreen && <CertificateStatus />}
|
<ProgressTabCertificateStatusSidePanelSlot />
|
||||||
<RelatedLinks />
|
<ProgressTabRelatedLinksSlot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Factory } from 'rosie';
|
import { Factory } from 'rosie';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import { breakpoints } from '@openedx/paragon';
|
import { breakpoints } from '@openedx/paragon';
|
||||||
@@ -111,7 +111,7 @@ describe('Progress Tab', () => {
|
|||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
sendTrackEvent.mockClear();
|
sendTrackEvent.mockClear();
|
||||||
|
|
||||||
const outlineTabLink = screen.getAllByRole('link', { name: 'Course Outline' });
|
const outlineTabLink = screen.getAllByRole('link', { name: 'Course outline' });
|
||||||
fireEvent.click(outlineTabLink[1]); // outlineTabLink[0] corresponds to the link in the DetailedGrades component
|
fireEvent.click(outlineTabLink[1]); // outlineTabLink[0] corresponds to the link in the DetailedGrades component
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||||
@@ -471,9 +471,12 @@ describe('Progress Tab', () => {
|
|||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByText('limited feature')).toBeInTheDocument();
|
expect(screen.getByText('limited feature')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Unlock to work towards a certificate.')).toBeInTheDocument();
|
expect(screen.getByText('Unlock to work towards a certificate.')).toBeInTheDocument();
|
||||||
expect(screen.queryAllByText('You have limited access to graded assignments as part of the audit track in this course.')).toHaveLength(2);
|
expect(screen.queryAllByText(
|
||||||
|
'You have limited access to graded assignments as part of the audit track in this course.',
|
||||||
|
{ exact: false },
|
||||||
|
)).toHaveLength(2);
|
||||||
|
|
||||||
expect(screen.queryAllByTestId('blocked-icon')).toHaveLength(4);
|
expect(screen.queryAllByTestId('locked-icon')).toHaveLength(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render subsections for which showGrades is false', async () => {
|
it('does not render subsections for which showGrades is false', async () => {
|
||||||
@@ -545,6 +548,111 @@ describe('Progress Tab', () => {
|
|||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByText('Grades & Credit')).toBeInTheDocument();
|
expect(screen.getByText('Grades & Credit')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not render ungraded subsections when SHOW_UNGRADED_ASSIGNMENT_PROGRESS is false', async () => {
|
||||||
|
// The second assignment has has_graded_assignment set to false, so it should not be shown.
|
||||||
|
setTabData({
|
||||||
|
section_scores: [
|
||||||
|
{
|
||||||
|
display_name: 'First section',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Homework',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||||
|
display_name: 'First subsection',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: true,
|
||||||
|
num_points_earned: 1,
|
||||||
|
num_points_possible: 2,
|
||||||
|
percent_graded: 1.0,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display_name: 'Second section',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Homework',
|
||||||
|
display_name: 'Second subsection',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: false,
|
||||||
|
num_points_earned: 1,
|
||||||
|
num_points_possible: 1,
|
||||||
|
percent_graded: 1.0,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText('First subsection')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Second subsection')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders both graded and ungraded subsections when SHOW_UNGRADED_ASSIGNMENT_PROGRESS is true', async () => {
|
||||||
|
// The second assignment has has_graded_assignment set to false.
|
||||||
|
setConfig({
|
||||||
|
...getConfig(),
|
||||||
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTabData({
|
||||||
|
section_scores: [
|
||||||
|
{
|
||||||
|
display_name: 'First section',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Homework',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||||
|
display_name: 'First subsection',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: true,
|
||||||
|
num_points_earned: 1,
|
||||||
|
num_points_possible: 2,
|
||||||
|
percent_graded: 1.0,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display_name: 'Second section',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Homework',
|
||||||
|
display_name: 'Second subsection',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: false,
|
||||||
|
num_points_earned: 1,
|
||||||
|
num_points_possible: 1,
|
||||||
|
percent_graded: 1.0,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText('First subsection')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Second subsection')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// reset config for other tests
|
||||||
|
setConfig({
|
||||||
|
...getConfig(),
|
||||||
|
SHOW_UNGRADED_ASSIGNMENT_PROGRESS: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Grade Summary', () => {
|
describe('Grade Summary', () => {
|
||||||
@@ -553,143 +661,133 @@ describe('Progress Tab', () => {
|
|||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render Grade Summary when assignment policies are not populated', async () => {
|
it('does not render Grade Summary when assignment type grade summary is not populated', async () => {
|
||||||
setTabData({
|
setTabData({
|
||||||
grading_policy: {
|
assignment_type_grade_summary: [],
|
||||||
assignment_policies: [],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
section_scores: [],
|
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.queryByText('Grade summary')).not.toBeInTheDocument();
|
expect(screen.queryByText('Grade summary')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates grades correctly when number of droppable assignments equals total number of assignments', async () => {
|
it('shows lock icon when all subsections of assignment type are hidden', async () => {
|
||||||
setTabData({
|
setTabData({
|
||||||
grading_policy: {
|
grading_policy: {
|
||||||
assignment_policies: [
|
assignment_policies: [
|
||||||
{
|
|
||||||
num_droppable: 2,
|
|
||||||
num_total: 2,
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 0% 0%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates grades correctly when number of droppable assignments is less than total number of assignments', async () => {
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates grades correctly when number of droppable assignments is zero', async () => {
|
|
||||||
setTabData({
|
|
||||||
grading_policy: {
|
|
||||||
assignment_policies: [
|
|
||||||
{
|
|
||||||
num_droppable: 0,
|
|
||||||
num_total: 2,
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 100% 50% 50%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates grades correctly when number of total assignments is less than the number of assignments created', async () => {
|
|
||||||
setTabData({
|
|
||||||
grading_policy: {
|
|
||||||
assignment_policies: [
|
|
||||||
{
|
|
||||||
num_droppable: 1,
|
|
||||||
num_total: 1, // two assignments created in the factory, but 1 is expected per Studio settings
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates grades correctly when number of total assignments is greater than the number of assignments created', async () => {
|
|
||||||
setTabData({
|
|
||||||
grading_policy: {
|
|
||||||
assignment_policies: [
|
|
||||||
{
|
|
||||||
num_droppable: 0,
|
|
||||||
num_total: 5, // two assignments created in the factory, but 5 are expected per Studio settings
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
grade_range: {
|
|
||||||
pass: 0.75,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
|
||||||
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
|
|
||||||
expect(screen.getByRole('row', { name: 'Homework 100% 20% 20%' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('calculates weighted grades correctly', async () => {
|
|
||||||
setTabData({
|
|
||||||
grading_policy: {
|
|
||||||
assignment_policies: [
|
|
||||||
{
|
|
||||||
num_droppable: 1,
|
|
||||||
num_total: 2,
|
|
||||||
short_label: 'HW',
|
|
||||||
type: 'Homework',
|
|
||||||
weight: 0.5,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
num_droppable: 0,
|
num_droppable: 0,
|
||||||
num_total: 1,
|
num_total: 1,
|
||||||
short_label: 'Ex',
|
short_label: 'Final',
|
||||||
type: 'Exam',
|
type: 'Final Exam',
|
||||||
weight: 0.5,
|
weight: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
grade_range: {
|
grade_range: {
|
||||||
pass: 0.75,
|
pass: 0.75,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
assignment_type_grade_summary: [
|
||||||
|
{
|
||||||
|
type: 'Final Exam',
|
||||||
|
weight: 0.4,
|
||||||
|
average_grade: 0.0,
|
||||||
|
weighted_grade: 0.0,
|
||||||
|
last_grade_publish_date: '2025-10-15T14:17:04.368903Z',
|
||||||
|
has_hidden_contribution: 'all',
|
||||||
|
short_label: 'Final',
|
||||||
|
num_droppable: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
// Should show lock icon for grade and weighted grade
|
||||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
expect(screen.getAllByTestId('lock-icon')).toHaveLength(2);
|
||||||
expect(screen.getByRole('row', { name: 'Homework 1 50% 100% 50%' })).toBeInTheDocument();
|
});
|
||||||
expect(screen.getByRole('row', { name: 'Exam 50% 0% 0%' })).toBeInTheDocument();
|
|
||||||
|
it('shows percent plus hidden grades when some subsections of assignment type are hidden', async () => {
|
||||||
|
setTabData({
|
||||||
|
grading_policy: {
|
||||||
|
assignment_policies: [
|
||||||
|
{
|
||||||
|
num_droppable: 0,
|
||||||
|
num_total: 2,
|
||||||
|
short_label: 'HW',
|
||||||
|
type: 'Homework',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
grade_range: {
|
||||||
|
pass: 0.75,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assignment_type_grade_summary: [
|
||||||
|
{
|
||||||
|
type: 'Homework',
|
||||||
|
weight: 1,
|
||||||
|
average_grade: 0.25,
|
||||||
|
weighted_grade: 0.25,
|
||||||
|
last_grade_publish_date: '2025-10-15T14:17:04.368903Z',
|
||||||
|
has_hidden_contribution: 'some',
|
||||||
|
short_label: 'HW',
|
||||||
|
num_droppable: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
// Should show percent + hidden scores for grade and weighted grade
|
||||||
|
const hiddenScoresCells = screen.getAllByText(/% \+ Hidden Scores/);
|
||||||
|
expect(hiddenScoresCells).toHaveLength(2);
|
||||||
|
// Only correct visible scores should be shown (from subsection2)
|
||||||
|
// The correct visible score is 1/4 = 0.25 -> 25%
|
||||||
|
expect(hiddenScoresCells[0]).toHaveTextContent('25% + Hidden Scores');
|
||||||
|
expect(hiddenScoresCells[1]).toHaveTextContent('25% + Hidden Scores');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a warning message with the latest due date when not all assignment scores are included in the total grade', async () => {
|
||||||
|
setTabData({
|
||||||
|
grading_policy: {
|
||||||
|
assignment_policies: [
|
||||||
|
{
|
||||||
|
num_droppable: 0,
|
||||||
|
num_total: 2,
|
||||||
|
short_label: 'HW',
|
||||||
|
type: 'Homework',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
grade_range: {
|
||||||
|
pass: 0.75,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assignment_type_grade_summary: [
|
||||||
|
{
|
||||||
|
type: 'Homework',
|
||||||
|
weight: 1,
|
||||||
|
average_grade: 1,
|
||||||
|
weighted_grade: 1,
|
||||||
|
last_grade_publish_date: tomorrow.toISOString(),
|
||||||
|
has_hidden_contribution: 'none',
|
||||||
|
short_label: 'HW',
|
||||||
|
num_droppable: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
const formattedDateTime = new Intl.DateTimeFormat('en', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZoneName: 'short',
|
||||||
|
}).format(tomorrow);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
`Some assignment scores are not yet included in your total grade. These grades will be released by ${formattedDateTime}.`,
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders override notice', async () => {
|
it('renders override notice', async () => {
|
||||||
@@ -788,7 +886,7 @@ describe('Progress Tab', () => {
|
|||||||
sendTrackEvent.mockClear();
|
sendTrackEvent.mockClear();
|
||||||
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||||
|
|
||||||
const outlineLink = screen.getAllByRole('link', { name: 'Course Outline' })[0];
|
const outlineLink = screen.getAllByRole('link', { name: 'Course outline' })[0];
|
||||||
fireEvent.click(outlineLink);
|
fireEvent.click(outlineLink);
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||||
@@ -809,7 +907,7 @@ describe('Progress Tab', () => {
|
|||||||
|
|
||||||
// Open the problem score drawer
|
// Open the problem score drawer
|
||||||
fireEvent.click(problemScoreDrawerToggle);
|
fireEvent.click(problemScoreDrawerToggle);
|
||||||
expect(screen.getByText('Problem Scores:')).toBeInTheDocument();
|
expect(screen.getAllByText('Graded Scores:').length).toBeGreaterThan(1);
|
||||||
expect(screen.getAllByText('0/1')).toHaveLength(3);
|
expect(screen.getAllByText('0/1')).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -821,6 +919,14 @@ describe('Progress Tab', () => {
|
|||||||
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||||
expect(screen.getByText('You currently have no graded problem scores.')).toBeInTheDocument();
|
expect(screen.getByText('You currently have no graded problem scores.')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders Detailed Grades table when section scores are populated', async () => {
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('First subsection'));
|
||||||
|
expect(screen.getByText('Second subsection'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Certificate Status', () => {
|
describe('Certificate Status', () => {
|
||||||
@@ -1384,4 +1490,287 @@ describe('Progress Tab', () => {
|
|||||||
expect(screen.getByText('Course progress for otherstudent')).toBeInTheDocument();
|
expect(screen.getByText('Course progress for otherstudent')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Exam data fetching integration', () => {
|
||||||
|
const mockSectionScores = [
|
||||||
|
{
|
||||||
|
display_name: 'Section 1',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Exam',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@exam1',
|
||||||
|
display_name: 'Midterm Exam',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: true,
|
||||||
|
percent_graded: 0.8,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: '/mock-url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assignment_type: 'Homework',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@homework1',
|
||||||
|
display_name: 'Homework 1',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: true,
|
||||||
|
percent_graded: 0.9,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: '/mock-url',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display_name: 'Section 2',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Exam',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||||
|
display_name: 'Final Exam',
|
||||||
|
learner_has_access: true,
|
||||||
|
has_graded_assignment: true,
|
||||||
|
percent_graded: 0.85,
|
||||||
|
show_correctness: 'always',
|
||||||
|
show_grades: true,
|
||||||
|
url: '/mock-url',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset any existing handlers to avoid conflicts
|
||||||
|
axiosMock.reset();
|
||||||
|
|
||||||
|
// Re-add the base mocks that other tests expect
|
||||||
|
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||||
|
axiosMock.onGet(progressUrl).reply(200, defaultTabData);
|
||||||
|
axiosMock.onGet(masqueradeUrl).reply(200, { success: true });
|
||||||
|
|
||||||
|
// Mock exam data endpoints using specific GET handlers
|
||||||
|
axiosMock.onGet(/.*exam1.*/).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@exam1',
|
||||||
|
exam_name: 'Midterm Exam',
|
||||||
|
attempt_status: 'submitted',
|
||||||
|
time_remaining_seconds: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosMock.onGet(/.*homework1.*/).reply(404);
|
||||||
|
|
||||||
|
axiosMock.onGet(/.*final_exam.*/).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 2,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||||
|
exam_name: 'Final Exam',
|
||||||
|
attempt_status: 'ready_to_start',
|
||||||
|
time_remaining_seconds: 7200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch exam data for all subsections when ProgressTab renders', async () => {
|
||||||
|
setTabData({ section_scores: mockSectionScores });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify exam API calls were made for all subsections
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(3);
|
||||||
|
|
||||||
|
// Verify the exam data is in the Redux store
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
|
||||||
|
// Check the exam data structure
|
||||||
|
expect(state.courseHome.examsData[0]).toEqual({
|
||||||
|
id: 1,
|
||||||
|
courseId,
|
||||||
|
contentId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@exam1',
|
||||||
|
examName: 'Midterm Exam',
|
||||||
|
attemptStatus: 'submitted',
|
||||||
|
timeRemainingSeconds: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state.courseHome.examsData[1]).toEqual({}); // 404 response for homework
|
||||||
|
|
||||||
|
expect(state.courseHome.examsData[2]).toEqual({
|
||||||
|
id: 2,
|
||||||
|
courseId,
|
||||||
|
contentId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||||
|
examName: 'Final Exam',
|
||||||
|
attemptStatus: 'ready_to_start',
|
||||||
|
timeRemainingSeconds: 7200,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty section scores gracefully', async () => {
|
||||||
|
setTabData({ section_scores: [] });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify no exam API calls were made
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(0);
|
||||||
|
|
||||||
|
// Verify empty exam data in Redux store
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.courseHome.examsData).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should re-fetch exam data when section scores change', async () => {
|
||||||
|
// Initial render with limited section scores
|
||||||
|
setTabData({
|
||||||
|
section_scores: [mockSectionScores[0]], // Only first section
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify initial API calls (2 subsections in first section)
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(2);
|
||||||
|
|
||||||
|
// Clear axios history to track new calls
|
||||||
|
axiosMock.resetHistory();
|
||||||
|
|
||||||
|
// Update with full section scores and re-render
|
||||||
|
setTabData({ section_scores: mockSectionScores });
|
||||||
|
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||||
|
|
||||||
|
// Verify additional API calls for all subsections
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle exam API errors gracefully without breaking ProgressTab', async () => {
|
||||||
|
// Clear existing mocks and setup specific error scenario
|
||||||
|
axiosMock.reset();
|
||||||
|
|
||||||
|
// Re-add base mocks
|
||||||
|
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||||
|
axiosMock.onGet(progressUrl).reply(200, defaultTabData);
|
||||||
|
axiosMock.onGet(masqueradeUrl).reply(200, { success: true });
|
||||||
|
|
||||||
|
// Mock first exam to return 500 error
|
||||||
|
axiosMock.onGet(/.*exam1.*/).reply(500, { error: 'Server Error' });
|
||||||
|
|
||||||
|
// Mock other exams to succeed
|
||||||
|
axiosMock.onGet(/.*homework1.*/).reply(404, { customAttributes: { httpErrorStatus: 404 } });
|
||||||
|
axiosMock.onGet(/.*final_exam.*/).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 2,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||||
|
exam_name: 'Final Exam',
|
||||||
|
attempt_status: 'ready_to_start',
|
||||||
|
time_remaining_seconds: 7200,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setTabData({ section_scores: mockSectionScores });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify ProgressTab still renders successfully despite API error
|
||||||
|
expect(screen.getByText('Grades')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify the exam data includes error placeholder for failed request
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.courseHome.examsData).toHaveLength(3);
|
||||||
|
expect(state.courseHome.examsData[0]).toEqual({}); // Failed request returns empty object
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use EXAMS_BASE_URL when configured for exam API calls', async () => {
|
||||||
|
// Configure EXAMS_BASE_URL
|
||||||
|
const originalConfig = getConfig();
|
||||||
|
setConfig({
|
||||||
|
...originalConfig,
|
||||||
|
EXAMS_BASE_URL: 'http://localhost:18740',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Override mock to use new base URL
|
||||||
|
const examUrlWithExamsBase = /http:\/\/localhost:18740\/api\/v1\/student\/exam\/attempt\/course_id.*/;
|
||||||
|
axiosMock.onGet(examUrlWithExamsBase).reply(200, {
|
||||||
|
exam: {
|
||||||
|
id: 1,
|
||||||
|
course_id: courseId,
|
||||||
|
exam_name: 'Test Exam',
|
||||||
|
attempt_status: 'created',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setTabData({ section_scores: [mockSectionScores[0]] });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify API calls use EXAMS_BASE_URL
|
||||||
|
const examApiCalls = axiosMock.history.get.filter(req => req.url.includes('localhost:18740'));
|
||||||
|
expect(examApiCalls.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Restore original config
|
||||||
|
setConfig(originalConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract sequence IDs correctly from nested section scores structure', async () => {
|
||||||
|
const complexSectionScores = [
|
||||||
|
{
|
||||||
|
display_name: 'Introduction',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Lecture',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@intro',
|
||||||
|
display_name: 'Course Introduction',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display_name: 'Assessments',
|
||||||
|
subsections: [
|
||||||
|
{
|
||||||
|
assignment_type: 'Exam',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz1',
|
||||||
|
display_name: 'Quiz 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
assignment_type: 'Exam',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz2',
|
||||||
|
display_name: 'Quiz 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock all the expected sequence IDs
|
||||||
|
const expectedSequenceIds = [
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@intro',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz1',
|
||||||
|
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz2',
|
||||||
|
];
|
||||||
|
|
||||||
|
expectedSequenceIds.forEach((sequenceId, index) => {
|
||||||
|
const examUrl = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}.*`);
|
||||||
|
axiosMock.onGet(examUrl).reply(index === 0 ? 404 : 200, {
|
||||||
|
exam: {
|
||||||
|
id: index,
|
||||||
|
course_id: courseId,
|
||||||
|
content_id: sequenceId,
|
||||||
|
exam_name: `Test ${index}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setTabData({ section_scores: complexSectionScores });
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
// Verify API calls were made for all extracted sequence IDs
|
||||||
|
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(3);
|
||||||
|
|
||||||
|
// Verify correct sequence IDs were used in API calls
|
||||||
|
const apiCalls = axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'));
|
||||||
|
expectedSequenceIds.forEach(sequenceId => {
|
||||||
|
expect(apiCalls.some(call => call.url.includes(encodeURIComponent(sequenceId)))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import React, { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import {
|
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import { Button, Card } from '@openedx/paragon';
|
import { Button, Card } from '@openedx/paragon';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useContextId } from '../../../data/hooks';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils';
|
import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils';
|
||||||
import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links';
|
import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links';
|
||||||
import { requestCert } from '../../data/thunks';
|
import { requestCert } from '../../data/thunks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import ProgressCertificateStatusSlot from '../../../plugin-slots/ProgressCertificateStatusSlot';
|
||||||
|
|
||||||
const CertificateStatus = ({ intl }) => {
|
const CertificateStatus = () => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
entranceExamData,
|
entranceExamData,
|
||||||
@@ -188,7 +187,8 @@ const CertificateStatus = ({ intl }) => {
|
|||||||
// regardless of passing or nonpassing status
|
// regardless of passing or nonpassing status
|
||||||
if (!canViewCertificate) {
|
if (!canViewCertificate) {
|
||||||
certCase = 'notAvailable';
|
certCase = 'notAvailable';
|
||||||
endDate = intl.formatDate(end, {
|
// use the certificate_available_date if it is available, otherwise use the end date of the course
|
||||||
|
endDate = intl.formatDate((certificateAvailableDate || end), {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -215,7 +215,6 @@ const CertificateStatus = ({ intl }) => {
|
|||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!certCase) {
|
if (!certCase) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -243,32 +242,32 @@ const CertificateStatus = ({ intl }) => {
|
|||||||
return (
|
return (
|
||||||
<section data-testid="certificate-status-component" className="text-dark-700 mb-4">
|
<section data-testid="certificate-status-component" className="text-dark-700 mb-4">
|
||||||
<Card className="bg-light-200 raised-card">
|
<Card className="bg-light-200 raised-card">
|
||||||
<Card.Header title={header} />
|
<ProgressCertificateStatusSlot courseId={courseId}>
|
||||||
<Card.Section className="small text-gray-700">
|
<div id={`${certCase}_certificate_status`}>
|
||||||
{body}
|
<Card.Header title={header} />
|
||||||
</Card.Section>
|
<Card.Section className="small text-gray-700">
|
||||||
<Card.Footer>
|
{body}
|
||||||
{buttonText && (buttonLocation || buttonAction) && (
|
</Card.Section>
|
||||||
<Button
|
<Card.Footer>
|
||||||
variant="outline-brand"
|
{buttonText && (buttonLocation || buttonAction) && (
|
||||||
onClick={() => {
|
<Button
|
||||||
logCertificateStatusButtonClicked(certStatus);
|
variant="outline-brand"
|
||||||
if (buttonAction) { buttonAction(); }
|
onClick={() => {
|
||||||
}}
|
logCertificateStatusButtonClicked(certStatus);
|
||||||
href={buttonLocation}
|
if (buttonAction) { buttonAction(); }
|
||||||
block
|
}}
|
||||||
>
|
href={buttonLocation}
|
||||||
{buttonText}
|
block
|
||||||
</Button>
|
>
|
||||||
)}
|
{buttonText}
|
||||||
</Card.Footer>
|
</Button>
|
||||||
|
)}
|
||||||
|
</Card.Footer>
|
||||||
|
</div>
|
||||||
|
</ProgressCertificateStatusSlot>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CertificateStatus.propTypes = {
|
export default CertificateStatus;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CertificateStatus);
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) => {
|
const CompleteDonutSegment = ({ completePercentage, lockedPercentage }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
||||||
|
|
||||||
if (!completePercentage) {
|
if (!completePercentage) {
|
||||||
@@ -82,8 +83,7 @@ const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) =>
|
|||||||
|
|
||||||
CompleteDonutSegment.propTypes = {
|
CompleteDonutSegment.propTypes = {
|
||||||
completePercentage: PropTypes.number.isRequired,
|
completePercentage: PropTypes.number.isRequired,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
lockedPercentage: PropTypes.number.isRequired,
|
lockedPercentage: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CompleteDonutSegment);
|
export default CompleteDonutSegment;
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import React from 'react';
|
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { useSelector } from 'react-redux';
|
import { useContextId } from '../../../data/hooks';
|
||||||
import {
|
|
||||||
getLocale, injectIntl, intlShape, isRtl,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
import CompleteDonutSegment from './CompleteDonutSegment';
|
import CompleteDonutSegment from './CompleteDonutSegment';
|
||||||
@@ -10,10 +7,9 @@ import IncompleteDonutSegment from './IncompleteDonutSegment';
|
|||||||
import LockedDonutSegment from './LockedDonutSegment';
|
import LockedDonutSegment from './LockedDonutSegment';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CompletionDonutChart = ({ intl }) => {
|
const CompletionDonutChart = () => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
completionSummary: {
|
completionSummary: {
|
||||||
@@ -62,8 +58,4 @@ const CompletionDonutChart = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CompletionDonutChart.propTypes = {
|
export default CompletionDonutChart;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CompletionDonutChart);
|
|
||||||
|
|||||||
@@ -7,18 +7,18 @@
|
|||||||
|
|
||||||
.donut-chart-label {
|
.donut-chart-label {
|
||||||
font: {
|
font: {
|
||||||
family: $font-family-sans-serif;
|
family: var(--pgn-typography-font-family-sans-serif);
|
||||||
size: .2rem;
|
size: .2rem;
|
||||||
weight: $font-weight-normal;
|
weight: var(--pgn-typography-font-weight-normal);
|
||||||
}
|
}
|
||||||
text-anchor: middle;
|
text-anchor: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donut-chart-number {
|
.donut-chart-number {
|
||||||
font: {
|
font: {
|
||||||
family: $font-family-monospace;
|
family: var(--pgn-typography-font-family-monospace);
|
||||||
size: .5rem;
|
size: .5rem;
|
||||||
weight: $font-weight-bold;
|
weight: var(--pgn-typography-font-weight-bold);
|
||||||
}
|
}
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
text-anchor: middle;
|
text-anchor: middle;
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.donut-chart-text {
|
.donut-chart-text {
|
||||||
fill: $primary-500;
|
fill: var(--pgn-color-primary-500);
|
||||||
-moz-transform: translateY(0.25em);
|
-moz-transform: translateY(0.25em);
|
||||||
-ms-transform: translateY(0.25em);
|
-ms-transform: translateY(0.25em);
|
||||||
-webkit-transform: translateY(0.25em);
|
-webkit-transform: translateY(0.25em);
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
.donut-ring, .donut-segment, .donut-hole {
|
.donut-ring, .donut-segment, .donut-hole {
|
||||||
&.complete-stroke {
|
&.complete-stroke {
|
||||||
stroke: $info-500;
|
stroke: var(--pgn-color-info-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.divider-stroke {
|
&.divider-stroke {
|
||||||
@@ -65,10 +65,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.incomplete-stroke {
|
&.incomplete-stroke {
|
||||||
stroke: $light-300;
|
stroke: var(--pgn-color-light-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.locked-stroke {
|
&.locked-stroke {
|
||||||
stroke: $primary-500;
|
stroke: var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import React from 'react';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import CompletionDonutChart from './CompletionDonutChart';
|
import CompletionDonutChart from './CompletionDonutChart';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CourseCompletion = ({ intl }) => (
|
const CourseCompletion = () => {
|
||||||
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
const intl = useIntl();
|
||||||
<div className="row w-100 m-0">
|
|
||||||
<div className="col-12 col-sm-6 col-md-7 p-0">
|
|
||||||
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
|
||||||
<p className="small">
|
|
||||||
{intl.formatMessage(messages.completionBody)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
|
|
||||||
<CompletionDonutChart />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
|
|
||||||
CourseCompletion.propTypes = {
|
return (
|
||||||
intl: intlShape.isRequired,
|
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
||||||
|
<div className="row w-100 m-0">
|
||||||
|
<div className="col-12 col-sm-6 col-md-7 p-0">
|
||||||
|
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
||||||
|
<p className="small">
|
||||||
|
{intl.formatMessage(messages.completionBody)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
|
||||||
|
<CompletionDonutChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CourseCompletion);
|
export default CourseCompletion;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
|
const IncompleteDonutSegment = ({ incompletePercentage }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
||||||
|
|
||||||
if (!incompletePercentage) {
|
if (!incompletePercentage) {
|
||||||
@@ -53,7 +54,6 @@ const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
|
|||||||
|
|
||||||
IncompleteDonutSegment.propTypes = {
|
IncompleteDonutSegment.propTypes = {
|
||||||
incompletePercentage: PropTypes.number.isRequired,
|
incompletePercentage: PropTypes.number.isRequired,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(IncompleteDonutSegment);
|
export default IncompleteDonutSegment;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const LockedDonutSegment = ({ intl, lockedPercentage }) => {
|
const LockedDonutSegment = ({ lockedPercentage }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [showLockedPopover, setShowLockedPopover] = useState(false);
|
const [showLockedPopover, setShowLockedPopover] = useState(false);
|
||||||
|
|
||||||
if (!lockedPercentage) {
|
if (!lockedPercentage) {
|
||||||
@@ -65,8 +66,7 @@ const LockedDonutSegment = ({ intl, lockedPercentage }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
LockedDonutSegment.propTypes = {
|
LockedDonutSegment.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
lockedPercentage: PropTypes.number.isRequired,
|
lockedPercentage: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(LockedDonutSegment);
|
export default LockedDonutSegment;
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { CheckCircle, WarningFilled, WatchFilled } from '@openedx/paragon/icons';
|
import { CheckCircle, WarningFilled, WatchFilled } from '@openedx/paragon/icons';
|
||||||
import { Hyperlink, Icon } from '@openedx/paragon';
|
import { Hyperlink, Icon } from '@openedx/paragon';
|
||||||
|
import { useContextId } from '../../../data/hooks';
|
||||||
|
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
import { DashboardLink } from '../../../shared/links';
|
import { DashboardLink } from '../../../shared/links';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CreditInformation = ({ intl }) => {
|
const CreditInformation = () => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
creditCourseRequirements,
|
creditCourseRequirements,
|
||||||
@@ -36,36 +34,13 @@ const CreditInformation = ({ intl }) => {
|
|||||||
|
|
||||||
switch (creditCourseRequirements.eligibilityStatus) {
|
switch (creditCourseRequirements.eligibilityStatus) {
|
||||||
case 'not_eligible':
|
case 'not_eligible':
|
||||||
eligibilityStatus = (
|
eligibilityStatus = intl.formatMessage(messages.creditNotEligibleStatus, { creditLink });
|
||||||
<FormattedMessage
|
|
||||||
id="progress.creditInformation.creditNotEligible"
|
|
||||||
defaultMessage="You are no longer eligible for credit in this course. Learn more about {creditLink}."
|
|
||||||
description="Message to learner who are not eligible for course credit, it can because the a requirement deadline have passed"
|
|
||||||
values={{ creditLink }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'eligible':
|
case 'eligible':
|
||||||
eligibilityStatus = (
|
eligibilityStatus = intl.formatMessage(messages.creditEligibleStatus, { dashboardLink, creditLink });
|
||||||
<FormattedMessage
|
|
||||||
id="progress.creditInformation.creditEligible"
|
|
||||||
defaultMessage="
|
|
||||||
You have met the requirements for credit in this course. Go to your
|
|
||||||
{dashboardLink} to purchase course credit. Or learn more about {creditLink}."
|
|
||||||
description="After the credit requirements are met, leaners can then do the last step which purchasing the credit. Note that is only doable for leaners after they met all the requirements"
|
|
||||||
values={{ dashboardLink, creditLink }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'partial_eligible':
|
case 'partial_eligible':
|
||||||
eligibilityStatus = (
|
eligibilityStatus = intl.formatMessage(messages.creditPartialEligibleStatus, { creditLink });
|
||||||
<FormattedMessage
|
|
||||||
id="progress.creditInformation.creditPartialEligible"
|
|
||||||
defaultMessage="You have not yet met the requirements for credit. Learn more about {creditLink}."
|
|
||||||
description="This means that one or more requirements is not satisfied yet"
|
|
||||||
values={{ creditLink }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -108,8 +83,4 @@ const CreditInformation = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CreditInformation.propTypes = {
|
export default CreditInformation;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CreditInformation);
|
|
||||||
|
|||||||
@@ -35,6 +35,22 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Verification submitted',
|
defaultMessage: 'Verification submitted',
|
||||||
description: 'It indicate that the learner submitted a requirement but is not graded or reviewed yet',
|
description: 'It indicate that the learner submitted a requirement but is not graded or reviewed yet',
|
||||||
},
|
},
|
||||||
|
creditNotEligibleStatus: {
|
||||||
|
id: 'progress.creditInformation.creditNotEligible',
|
||||||
|
defaultMessage: 'You are no longer eligible for credit in this course. Learn more about {creditLink}.',
|
||||||
|
description: 'Message to learner who are not eligible for course credit, it can be that a requirement deadline has passed',
|
||||||
|
},
|
||||||
|
creditEligibleStatus: {
|
||||||
|
id: 'progress.creditInformation.creditEligible',
|
||||||
|
defaultMessage: `You have met the requirements for credit in this course. Go to your
|
||||||
|
{dashboardLink} to purchase course credit. Or learn more about {creditLink}.`,
|
||||||
|
description: 'After the credit requirements are met, leaners can then do the last step which purchasing the credit. Note that is only doable for leaners after they met all the requirements',
|
||||||
|
},
|
||||||
|
creditPartialEligibleStatus: {
|
||||||
|
id: 'progress.creditInformation.creditPartialEligible',
|
||||||
|
defaultMessage: 'You have not yet met the requirements for credit. Learn more about {creditLink}.',
|
||||||
|
description: 'This means that one or more requirements is not satisfied yet',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { useSelector } from 'react-redux';
|
import { useContextId } from '../../../../data/hooks';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
@@ -11,10 +10,9 @@ import CreditInformation from '../../credit-information/CreditInformation';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const CourseGrade = ({ intl }) => {
|
const CourseGrade = () => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
creditCourseRequirements,
|
creditCourseRequirements,
|
||||||
@@ -54,8 +52,4 @@ const CourseGrade = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseGrade.propTypes = {
|
export default CourseGrade;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CourseGrade);
|
|
||||||
|
|||||||
@@ -1,35 +1,64 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { CheckCircle, WarningFilled } from '@openedx/paragon/icons';
|
import { CheckCircle, WarningFilled } from '@openedx/paragon/icons';
|
||||||
import { breakpoints, Icon, useWindowSize } from '@openedx/paragon';
|
import { breakpoints, Icon, useWindowSize } from '@openedx/paragon';
|
||||||
|
import { useContextId } from '../../../../data/hooks';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
import GradeRangeTooltip from './GradeRangeTooltip';
|
import GradeRangeTooltip from './GradeRangeTooltip';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
import { getLatestDueDateInFuture } from '../../utils';
|
||||||
|
|
||||||
const CourseGradeFooter = ({ intl, passingGrade }) => {
|
const ResponsiveText = ({
|
||||||
const {
|
wideScreen, children, hasLetterGrades, passingGrade,
|
||||||
courseId,
|
}) => {
|
||||||
} = useSelector(state => state.courseHome);
|
const className = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom';
|
||||||
|
const iconSize = wideScreen ? 'h3' : 'h4';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={className}>
|
||||||
|
{children}
|
||||||
|
{hasLetterGrades && (
|
||||||
|
<span style={{ whiteSpace: 'nowrap' }}>
|
||||||
|
|
||||||
|
<GradeRangeTooltip iconButtonClassName={iconSize} passingGrade={passingGrade} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NoticeRow = ({
|
||||||
|
wideScreen, icon, bgClass, message,
|
||||||
|
}) => {
|
||||||
|
const textClass = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom';
|
||||||
|
return (
|
||||||
|
<div className={`row w-100 m-0 px-4 py-3 py-md-4 rounded-bottom ${bgClass}`}>
|
||||||
|
<div className="col-auto p-0">{icon}</div>
|
||||||
|
<div className="col-11 pl-2 px-0">
|
||||||
|
<span className={textClass}>{message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CourseGradeFooter = ({ passingGrade }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const courseId = useContextId();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
courseGrade: {
|
assignmentTypeGradeSummary,
|
||||||
isPassing,
|
courseGrade: { isPassing, letterGrade },
|
||||||
letterGrade,
|
gradingPolicy: { gradeRange },
|
||||||
},
|
|
||||||
gradingPolicy: {
|
|
||||||
gradeRange,
|
|
||||||
},
|
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
|
const latestDueDate = getLatestDueDateInFuture(assignmentTypeGradeSummary);
|
||||||
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
||||||
|
const hasLetterGrades = Object.keys(gradeRange).length > 1;
|
||||||
|
|
||||||
const hasLetterGrades = Object.keys(gradeRange).length > 1; // A pass/fail course will only have one key
|
// build footer text
|
||||||
let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade });
|
let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade });
|
||||||
|
|
||||||
if (isPassing) {
|
if (isPassing) {
|
||||||
if (hasLetterGrades) {
|
if (hasLetterGrades) {
|
||||||
const minGradeRangeCutoff = gradeRange[letterGrade] * 100;
|
const minGradeRangeCutoff = gradeRange[letterGrade] * 100;
|
||||||
@@ -49,45 +78,65 @@ const CourseGradeFooter = ({ intl, passingGrade }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = isPassing ? <Icon src={CheckCircle} className="text-success-300 d-inline-flex align-bottom" />
|
const passingIcon = isPassing ? (
|
||||||
: <Icon src={WarningFilled} className="d-inline-flex align-bottom" />;
|
<Icon src={CheckCircle} className="text-success-300 d-inline-flex align-bottom" />
|
||||||
|
) : (
|
||||||
|
<Icon src={WarningFilled} className="d-inline-flex align-bottom" />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`row w-100 m-0 px-4 py-3 py-md-4 rounded-bottom ${isPassing ? 'bg-success-100' : 'bg-warning-100'}`}>
|
<div>
|
||||||
<div className="col-auto p-0">
|
<NoticeRow
|
||||||
{icon}
|
wideScreen={wideScreen}
|
||||||
</div>
|
icon={passingIcon}
|
||||||
<div className="col-11 pl-2 px-0">
|
bgClass={isPassing ? 'bg-success-100' : 'bg-warning-100'}
|
||||||
{!wideScreen && (
|
message={(
|
||||||
<span className="h5 align-bottom">
|
<ResponsiveText
|
||||||
|
wideScreen={wideScreen}
|
||||||
|
hasLetterGrades={hasLetterGrades}
|
||||||
|
passingGrade={passingGrade}
|
||||||
|
>
|
||||||
{footerText}
|
{footerText}
|
||||||
{hasLetterGrades && (
|
</ResponsiveText>
|
||||||
<span style={{ whiteSpace: 'nowrap' }}>
|
|
||||||
|
|
||||||
<GradeRangeTooltip iconButtonClassName="h4" passingGrade={passingGrade} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
{wideScreen && (
|
/>
|
||||||
<span className="h4 m-0 align-bottom">
|
{latestDueDate && (
|
||||||
{footerText}
|
<NoticeRow
|
||||||
{hasLetterGrades && (
|
wideScreen={wideScreen}
|
||||||
<span style={{ whiteSpace: 'nowrap' }}>
|
icon={<Icon src={WarningFilled} className="d-inline-flex align-bottom" />}
|
||||||
|
bgClass="bg-warning-100"
|
||||||
<GradeRangeTooltip iconButtonClassName="h3" passingGrade={passingGrade} />
|
message={intl.formatMessage(messages.courseGradeFooterDueDateNotice, {
|
||||||
</span>
|
dueDate: intl.formatDate(latestDueDate, {
|
||||||
)}
|
year: 'numeric',
|
||||||
</span>
|
month: 'long',
|
||||||
)}
|
day: 'numeric',
|
||||||
</div>
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
timeZoneName: 'short',
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseGradeFooter.propTypes = {
|
ResponsiveText.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
wideScreen: PropTypes.bool.isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
hasLetterGrades: PropTypes.bool.isRequired,
|
||||||
passingGrade: PropTypes.number.isRequired,
|
passingGrade: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CourseGradeFooter);
|
NoticeRow.propTypes = {
|
||||||
|
wideScreen: PropTypes.bool.isRequired,
|
||||||
|
icon: PropTypes.element.isRequired,
|
||||||
|
bgClass: PropTypes.string.isRequired,
|
||||||
|
message: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
CourseGradeFooter.propTypes = {
|
||||||
|
passingGrade: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CourseGradeFooter;
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Locked } from '@openedx/paragon/icons';
|
import { Locked } from '@openedx/paragon/icons';
|
||||||
import { Button, Icon } from '@openedx/paragon';
|
import { Button, Icon } from '@openedx/paragon';
|
||||||
|
import { useContextId } from '../../../../data/hooks';
|
||||||
|
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const CourseGradeHeader = ({ intl }) => {
|
const CourseGradeHeader = () => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
const {
|
const {
|
||||||
org,
|
org,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
@@ -51,7 +48,7 @@ const CourseGradeHeader = ({ intl }) => {
|
|||||||
previewText = intl.formatMessage(messages.courseGradePreviewUpgradeDeadlinePassedBody);
|
previewText = intl.formatMessage(messages.courseGradePreviewUpgradeDeadlinePassedBody);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="row w-100 m-0 p-4 rounded-top bg-primary-500 text-white">
|
<div id="grade-course-header" className="row w-100 m-0 p-4 rounded-top bg-primary-500 text-white">
|
||||||
<div className={`col-12 ${verifiedMode ? 'col-md-9' : ''} p-0`}>
|
<div className={`col-12 ${verifiedMode ? 'col-md-9' : ''} p-0`}>
|
||||||
<div className="row w-100 m-0 p-0">
|
<div className="row w-100 m-0 p-0">
|
||||||
<div className="col-1 p-0">
|
<div className="col-1 p-0">
|
||||||
@@ -74,7 +71,7 @@ const CourseGradeHeader = ({ intl }) => {
|
|||||||
</div>
|
</div>
|
||||||
{verifiedMode && (
|
{verifiedMode && (
|
||||||
<div className="col-12 col-md-3 mt-3 mt-md-0 p-0 align-self-center text-right">
|
<div className="col-12 col-md-3 mt-3 mt-md-0 p-0 align-self-center text-right">
|
||||||
<Button variant="brand" size="sm" href={verifiedMode.upgradeUrl} onClick={logUpgradeButtonClick}>
|
<Button id="upgrade-button" variant="brand" size="sm" href={verifiedMode.upgradeUrl} onClick={logUpgradeButtonClick}>
|
||||||
{intl.formatMessage(messages.courseGradePreviewUpgradeButton)}
|
{intl.formatMessage(messages.courseGradePreviewUpgradeButton)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,8 +80,4 @@ const CourseGradeHeader = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseGradeHeader.propTypes = {
|
export default CourseGradeHeader;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CourseGradeHeader);
|
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import {
|
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
getLocale, injectIntl, intlShape, isRtl,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||||
|
import { useContextId } from '../../../../data/hooks';
|
||||||
|
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const CurrentGradeTooltip = ({ intl, tooltipClassName }) => {
|
const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
assignmentTypeGradeSummary,
|
||||||
courseGrade: {
|
courseGrade: {
|
||||||
isPassing,
|
isPassing,
|
||||||
percent,
|
percent,
|
||||||
@@ -29,6 +26,8 @@ const CurrentGradeTooltip = ({ intl, tooltipClassName }) => {
|
|||||||
|
|
||||||
const isLocaleRtl = isRtl(getLocale());
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
|
const hasHiddenGrades = assignmentTypeGradeSummary.some((assignmentType) => assignmentType.hasHiddenContribution !== 'none');
|
||||||
|
|
||||||
if (isLocaleRtl) {
|
if (isLocaleRtl) {
|
||||||
currentGradeDirection = currentGrade < 50 ? '-' : '';
|
currentGradeDirection = currentGrade < 50 ? '-' : '';
|
||||||
}
|
}
|
||||||
@@ -60,6 +59,15 @@ const CurrentGradeTooltip = ({ intl, tooltipClassName }) => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage(messages.currentGradeLabel)}
|
{intl.formatMessage(messages.currentGradeLabel)}
|
||||||
</text>
|
</text>
|
||||||
|
<text
|
||||||
|
className="x-small"
|
||||||
|
textAnchor={currentGrade < 50 ? 'start' : 'end'}
|
||||||
|
x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`}
|
||||||
|
y="35px"
|
||||||
|
style={{ transform: `translateX(${currentGradeDirection}3.4em)` }}
|
||||||
|
>
|
||||||
|
{hasHiddenGrades ? ` + ${intl.formatMessage(messages.hiddenScoreLabel)}` : ''}
|
||||||
|
</text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -69,8 +77,7 @@ CurrentGradeTooltip.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
CurrentGradeTooltip.propTypes = {
|
CurrentGradeTooltip.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
tooltipClassName: PropTypes.string,
|
tooltipClassName: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CurrentGradeTooltip);
|
export default CurrentGradeTooltip;
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {
|
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
getLocale, injectIntl, intlShape, isRtl,
|
import { useContextId } from '../../../../data/hooks';
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
import CurrentGradeTooltip from './CurrentGradeTooltip';
|
import CurrentGradeTooltip from './CurrentGradeTooltip';
|
||||||
import PassingGradeTooltip from './PassingGradeTooltip';
|
import PassingGradeTooltip from './PassingGradeTooltip';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const GradeBar = ({ intl, passingGrade }) => {
|
const GradeBar = ({ passingGrade }) => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
courseGrade: {
|
courseGrade: {
|
||||||
@@ -52,8 +48,7 @@ const GradeBar = ({ intl, passingGrade }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
GradeBar.propTypes = {
|
GradeBar.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
passingGrade: PropTypes.number.isRequired,
|
passingGrade: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(GradeBar);
|
export default GradeBar;
|
||||||
|
|||||||
@@ -4,24 +4,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar__base {
|
.grade-bar__base {
|
||||||
fill: $light-300;
|
fill: var(--pgn-color-light-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar__divider {
|
.grade-bar__divider {
|
||||||
fill: $primary-500;
|
fill: var(--pgn-color-primary-500);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar--passing {
|
.grade-bar--passing {
|
||||||
fill: $primary-500;
|
fill: var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar--current-passing {
|
.grade-bar--current-passing {
|
||||||
fill: $success-500;
|
fill: var(--pgn-color-success-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-bar--current-non-passing {
|
.grade-bar--current-non-passing {
|
||||||
fill: $accent-b;
|
fill: var(--pgn-color-accent-b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,22 +31,22 @@
|
|||||||
|
|
||||||
#minimum-grade-tooltip {
|
#minimum-grade-tooltip {
|
||||||
.arrow::after {
|
.arrow::after {
|
||||||
border-bottom-color: $primary-500;
|
border-bottom-color: var(--pgn-color-primary-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#passing-grade-tooltip {
|
#passing-grade-tooltip {
|
||||||
background: $success-500;
|
background: var(--pgn-color-success-500);
|
||||||
|
|
||||||
.arrow::after {
|
.arrow::after {
|
||||||
border-top-color: $success-500;
|
border-top-color: var(--pgn-color-success-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#non-passing-grade-tooltip {
|
#non-passing-grade-tooltip {
|
||||||
background: $accent-b;
|
background: var(--pgn-color-accent-b);
|
||||||
|
|
||||||
.arrow::after {
|
.arrow::after {
|
||||||
border-top-color: $accent-b;
|
border-top-color: var(--pgn-color-accent-b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { InfoOutline } from '@openedx/paragon/icons';
|
import { InfoOutline } from '@openedx/paragon/icons';
|
||||||
import {
|
import {
|
||||||
Icon, IconButton, OverlayTrigger, Popover,
|
Icon, IconButton, OverlayTrigger, Popover,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
|
import { useContextId } from '../../../../data/hooks';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const GradeRangeTooltip = ({ intl, iconButtonClassName, passingGrade }) => {
|
const GradeRangeTooltip = ({ iconButtonClassName, passingGrade }) => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
gradesFeatureIsFullyLocked,
|
gradesFeatureIsFullyLocked,
|
||||||
@@ -80,8 +79,7 @@ GradeRangeTooltip.defaultProps = {
|
|||||||
|
|
||||||
GradeRangeTooltip.propTypes = {
|
GradeRangeTooltip.propTypes = {
|
||||||
iconButtonClassName: PropTypes.string,
|
iconButtonClassName: PropTypes.string,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
passingGrade: PropTypes.number.isRequired,
|
passingGrade: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(GradeRangeTooltip);
|
export default GradeRangeTooltip;
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {
|
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
getLocale, injectIntl, intlShape, isRtl,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
import { OverlayTrigger, Popover } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const PassingGradeTooltip = ({ intl, passingGrade, tooltipClassName }) => {
|
const PassingGradeTooltip = ({ passingGrade, tooltipClassName }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const isLocaleRtl = isRtl(getLocale());
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
let passingGradeDirection = passingGrade < 50 ? '' : '-';
|
let passingGradeDirection = passingGrade < 50 ? '' : '-';
|
||||||
@@ -54,9 +52,8 @@ PassingGradeTooltip.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
PassingGradeTooltip.propTypes = {
|
PassingGradeTooltip.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
passingGrade: PropTypes.number.isRequired,
|
passingGrade: PropTypes.number.isRequired,
|
||||||
tooltipClassName: PropTypes.string,
|
tooltipClassName: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(PassingGradeTooltip);
|
export default PassingGradeTooltip;
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Blocked } from '@openedx/paragon/icons';
|
import { Locked } from '@openedx/paragon/icons';
|
||||||
import { Icon, Hyperlink } from '@openedx/paragon';
|
import { Icon, Hyperlink } from '@openedx/paragon';
|
||||||
|
import { useContextId } from '../../../../data/hooks';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
import { showUngradedAssignments } from '../../utils';
|
||||||
|
|
||||||
import DetailedGradesTable from './DetailedGradesTable';
|
import DetailedGradesTable from './DetailedGradesTable';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const DetailedGrades = ({ intl }) => {
|
const DetailedGrades = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const { administrator } = getAuthenticatedUser();
|
const { administrator } = getAuthenticatedUser();
|
||||||
const {
|
const courseId = useContextId();
|
||||||
courseId,
|
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
const {
|
const {
|
||||||
org,
|
org,
|
||||||
tabs,
|
tabs,
|
||||||
@@ -28,6 +26,8 @@ const DetailedGrades = ({ intl }) => {
|
|||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
const hasSectionScores = sectionScores.length > 0;
|
const hasSectionScores = sectionScores.length > 0;
|
||||||
|
const emptyTableMsg = showUngradedAssignments()
|
||||||
|
? messages.detailedGradesEmpty : messages.detailedGradesEmptyOnlyGraded;
|
||||||
|
|
||||||
const logOutlineLinkClick = () => {
|
const logOutlineLinkClick = () => {
|
||||||
sendTrackEvent('edx.ui.lms.course_progress.detailed_grades.course_outline_link.clicked', {
|
sendTrackEvent('edx.ui.lms.course_progress.detailed_grades.course_outline_link.clicked', {
|
||||||
@@ -54,35 +54,36 @@ const DetailedGrades = ({ intl }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="text-dark-700">
|
<section className="text-dark-700">
|
||||||
<h3 className="h4 mb-3">{intl.formatMessage(messages.detailedGrades)}</h3>
|
<h3 className="h4">{intl.formatMessage(messages.detailedGrades)}</h3>
|
||||||
|
<ul className="micro mb-3 pl-3 text-gray-700">
|
||||||
|
<li>
|
||||||
|
<b>{intl.formatMessage(messages.practiceScoreLabel)} </b>
|
||||||
|
{intl.formatMessage(messages.practiceScoreInfoText)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>{intl.formatMessage(messages.gradedScoreLabel)} </b>
|
||||||
|
{intl.formatMessage(messages.gradedScoreInfoText)}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
{gradesFeatureIsPartiallyLocked && (
|
{gradesFeatureIsPartiallyLocked && (
|
||||||
<div className="mb-3 small ml-0 d-inline">
|
<div className="mb-3 small ml-0 d-inline">
|
||||||
<Icon className="mr-1 mt-1 d-inline-flex" style={{ height: '1rem', width: '1rem' }} src={Blocked} data-testid="blocked-icon" />
|
<Icon className="mr-1 mt-1 d-inline-flex" style={{ height: '1rem', width: '1rem' }} src={Locked} data-testid="locked-icon" />
|
||||||
{intl.formatMessage(messages.gradeSummaryLimitedAccessExplanation)}
|
{intl.formatMessage(messages.gradeSummaryLimitedAccessExplanation, { upgradeLink: '' })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasSectionScores && (
|
{hasSectionScores && (
|
||||||
<DetailedGradesTable />
|
<DetailedGradesTable />
|
||||||
)}
|
)}
|
||||||
{!hasSectionScores && (
|
{!hasSectionScores && (
|
||||||
<p className="small">{intl.formatMessage(messages.detailedGradesEmpty)}</p>
|
<p className="small">{intl.formatMessage(emptyTableMsg)}</p>
|
||||||
)}
|
)}
|
||||||
{overviewTabUrl && (
|
{overviewTabUrl && !showUngradedAssignments() && (
|
||||||
<p className="x-small m-0">
|
<p className="x-small m-0">
|
||||||
<FormattedMessage
|
{intl.formatMessage(messages.ungradedAlert, { outlineLink })}
|
||||||
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>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DetailedGrades.propTypes = {
|
export default DetailedGrades;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(DetailedGrades);
|
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import React from 'react';
|
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getLocale, injectIntl, intlShape, isRtl,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { DataTable } from '@openedx/paragon';
|
import { DataTable } from '@openedx/paragon';
|
||||||
|
import { useContextId } from '../../../../data/hooks';
|
||||||
|
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import SubsectionTitleCell from './SubsectionTitleCell';
|
import SubsectionTitleCell from './SubsectionTitleCell';
|
||||||
|
import { showUngradedAssignments } from '../../utils';
|
||||||
|
|
||||||
const DetailedGradesTable = ({ intl }) => {
|
const DetailedGradesTable = () => {
|
||||||
const {
|
const intl = useIntl();
|
||||||
courseId,
|
const courseId = useContextId();
|
||||||
} = useSelector(state => state.courseHome);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
sectionScores,
|
sectionScores,
|
||||||
@@ -24,9 +20,10 @@ const DetailedGradesTable = ({ intl }) => {
|
|||||||
sectionScores.map((chapter) => {
|
sectionScores.map((chapter) => {
|
||||||
const subsectionScores = chapter.subsections.filter(
|
const subsectionScores = chapter.subsections.filter(
|
||||||
(subsection) => !!(
|
(subsection) => !!(
|
||||||
subsection.hasGradedAssignment
|
(showUngradedAssignments() || subsection.hasGradedAssignment)
|
||||||
&& subsection.showGrades
|
&& subsection.showGrades
|
||||||
&& (subsection.numPointsPossible > 0 || subsection.numPointsEarned > 0)),
|
&& (subsection.numPointsPossible > 0 || subsection.numPointsEarned > 0)
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (subsectionScores.length === 0) {
|
if (subsectionScores.length === 0) {
|
||||||
@@ -66,8 +63,4 @@ const DetailedGradesTable = ({ intl }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DetailedGradesTable.propTypes = {
|
export default DetailedGradesTable;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(DetailedGradesTable);
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {
|
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
getLocale, injectIntl, intlShape, isRtl,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const ProblemScoreDrawer = ({ intl, problemScores, subsection }) => {
|
const ProblemScoreDrawer = ({ problemScores, subsection }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const isLocaleRtl = isRtl(getLocale());
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
|
const scoreLabel = subsection.hasGradedAssignment ? messages.gradedScoreLabel : messages.practiceScoreLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="row w-100 m-0 x-small ml-4 pt-2 pl-1 text-gray-700 flex-nowrap">
|
<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>
|
<span id="problem-score-label" className="col-auto p-0">{intl.formatMessage(scoreLabel)}</span>
|
||||||
<div className={classNames('col', 'p-0', { 'greyed-out': !subsection.learnerHasAccess })}>
|
<div className={classNames('col', 'p-0', { 'greyed-out': !subsection.learnerHasAccess })}>
|
||||||
<ul className="list-unstyled row w-100 m-0" aria-labelledby="problem-score-label">
|
<ul className="list-unstyled row w-100 m-0" aria-labelledby="problem-score-label">
|
||||||
{problemScores.map((problemScore, i) => (
|
{problemScores.map((problemScore, i) => (
|
||||||
@@ -26,12 +27,14 @@ const ProblemScoreDrawer = ({ intl, problemScores, subsection }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ProblemScoreDrawer.propTypes = {
|
ProblemScoreDrawer.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
problemScores: PropTypes.arrayOf(PropTypes.shape({
|
problemScores: PropTypes.arrayOf(PropTypes.shape({
|
||||||
earned: PropTypes.number.isRequired,
|
earned: PropTypes.number.isRequired,
|
||||||
possible: PropTypes.number.isRequired,
|
possible: PropTypes.number.isRequired,
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
subsection: PropTypes.shape({ learnerHasAccess: PropTypes.bool }).isRequired,
|
subsection: PropTypes.shape({
|
||||||
|
learnerHasAccess: PropTypes.bool,
|
||||||
|
hasGradedAssignment: PropTypes.bool,
|
||||||
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(ProblemScoreDrawer);
|
export default ProblemScoreDrawer;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user