Compare commits
260 Commits
open-relea
...
mashal-m/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
891d123f84 | ||
|
|
d9831b4dc7 | ||
|
|
f8215e3e46 | ||
|
|
edba1600dc | ||
|
|
9a07ad1501 | ||
|
|
b343ca7a74 | ||
|
|
b6d272e99d | ||
|
|
0fbb53ae86 | ||
|
|
ba06fd7c98 | ||
|
|
9396fbd9d4 | ||
|
|
57d880de70 | ||
|
|
bfad5cf684 | ||
|
|
b0378e1331 | ||
|
|
19d06d60be | ||
|
|
df91fef82e | ||
|
|
7e53ddb685 | ||
|
|
be72e36a3a | ||
|
|
fa5cf8f204 | ||
|
|
759d154e13 | ||
|
|
7c4200e9d3 | ||
|
|
e5e73e40ba | ||
|
|
1892edaade | ||
|
|
381be9a26b | ||
|
|
b3841ef446 | ||
|
|
5a897e4ea1 | ||
|
|
96ceab8b2f | ||
|
|
f9806d0759 | ||
|
|
a7b584c566 | ||
|
|
193a184142 | ||
|
|
3e76f7ac78 | ||
|
|
36062ff3a6 | ||
|
|
6257cb4b58 | ||
|
|
792d9eb758 | ||
|
|
cd84a15891 | ||
|
|
cafb881a61 | ||
|
|
fd94da0a43 | ||
|
|
1e41547b3e | ||
|
|
bf2f123367 | ||
|
|
0211ecf45e | ||
|
|
36ac129267 | ||
|
|
20d4c35d83 | ||
|
|
bbff8e719e | ||
|
|
5461c08169 | ||
|
|
ee88a12d8f | ||
|
|
9b316bd859 | ||
|
|
7e7eb83596 | ||
|
|
aaa367780d | ||
|
|
6d42ee9c6f | ||
|
|
41047f4c88 | ||
|
|
d83551c809 | ||
|
|
7c3088901d | ||
|
|
518c9ef6c2 | ||
|
|
ae97efaf2b | ||
|
|
361a099ed1 | ||
|
|
7f3757539a | ||
|
|
44f5132e2a | ||
|
|
53b19c9be3 | ||
|
|
abc374b60a | ||
|
|
af837fcac8 | ||
|
|
e328e3d597 | ||
|
|
559160213d | ||
|
|
878a4616f3 | ||
|
|
3028d79597 | ||
|
|
aa0de7663c | ||
|
|
acd91a1c31 | ||
|
|
b32817b3dd | ||
|
|
8b32e5892f | ||
|
|
76cf85f3d7 | ||
|
|
7d86c501a7 | ||
|
|
eeee32c100 | ||
|
|
95d88a054e | ||
|
|
550b15a16c | ||
|
|
715393d6ad | ||
|
|
19b4241020 | ||
|
|
09bd5bd748 | ||
|
|
89771cb56b | ||
|
|
3353ee2f9d | ||
|
|
7c1821382c | ||
|
|
b444d677b7 | ||
|
|
7178f28838 | ||
|
|
b07f22193c | ||
|
|
c6eba42120 | ||
|
|
7bb2266790 | ||
|
|
0a70f9b64e | ||
|
|
cfe4432c6b | ||
|
|
f7219b4f5d | ||
|
|
14a19b2794 | ||
|
|
8a9767cdd3 | ||
|
|
3cba1bbac4 | ||
|
|
9436770620 | ||
|
|
d03dd34009 | ||
|
|
9cdacde4dc | ||
|
|
a22ac3a776 | ||
|
|
7e19af44da | ||
|
|
57c3f3080e | ||
|
|
385635f5d1 | ||
|
|
a7f763cd2a | ||
|
|
c7c9c19771 | ||
|
|
1d3a779ef1 | ||
|
|
4f1a50ec24 | ||
|
|
72d18dc4f9 | ||
|
|
2197ec0c21 | ||
|
|
069ac9c234 | ||
|
|
3edf349969 | ||
|
|
a2516e9fcc | ||
|
|
554806e9ce | ||
|
|
ed13128fc4 | ||
|
|
373a2d88fc | ||
|
|
bcd54a4f4b | ||
|
|
c4cb0e5ac2 | ||
|
|
c77d518d04 | ||
|
|
703250c3d2 | ||
|
|
35ec314505 | ||
|
|
9fc7951576 | ||
|
|
4ed350c9c6 | ||
|
|
ebed27529c | ||
|
|
24ced5dc63 | ||
|
|
f004d0ab3c | ||
|
|
1bbcc6d052 | ||
|
|
3d122e0fb9 | ||
|
|
685d2d5593 | ||
|
|
97bd45cfa8 | ||
|
|
55dac2696e | ||
|
|
4586f8a6ad | ||
|
|
88bc1f6956 | ||
|
|
4f2f17beb3 | ||
|
|
8114750796 | ||
|
|
7b945a9fce | ||
|
|
48aad3951a | ||
|
|
dcf8da2279 | ||
|
|
d8e1124a4c | ||
|
|
e9f0a658d6 | ||
|
|
7049445969 | ||
|
|
f17a635e9d | ||
|
|
cc8ee33dcd | ||
|
|
c25ec8f1ae | ||
|
|
8325851813 | ||
|
|
a66d2cf524 | ||
|
|
628ede3ccc | ||
|
|
c0c51a3028 | ||
|
|
947e5e3cb2 | ||
|
|
93baa10141 | ||
|
|
c02bf1eeed | ||
|
|
b4c90ab506 | ||
|
|
e20bed64fb | ||
|
|
8285d42b7e | ||
|
|
74484b7847 | ||
|
|
45d5141769 | ||
|
|
3c52eb2e8d | ||
|
|
616027df86 | ||
|
|
93790464f8 | ||
|
|
c2cb5744a1 | ||
|
|
5d62cb2f46 | ||
|
|
0f11fd6245 | ||
|
|
2d6e4063ed | ||
|
|
7b6f5ccf86 | ||
|
|
61f0ce2023 | ||
|
|
5706adde4d | ||
|
|
ec1c3da725 | ||
|
|
64b0c03d30 | ||
|
|
3b33aacb3d | ||
|
|
f907c588c9 | ||
|
|
99cf1f9f06 | ||
|
|
7f016e55aa | ||
|
|
f0f8027de4 | ||
|
|
fd3d0f9391 | ||
|
|
3fe5bb1733 | ||
|
|
6db421eade | ||
|
|
b9d1bf0624 | ||
|
|
2789c7415b | ||
|
|
8484d98e26 | ||
|
|
b346b741d5 | ||
|
|
eedaa9f2e9 | ||
|
|
f2f0cb6008 | ||
|
|
b61057f2df | ||
|
|
2d46bacdc7 | ||
|
|
4655b344a7 | ||
|
|
41207e953e | ||
|
|
16a6eeab24 | ||
|
|
907892e7bb | ||
|
|
f5d1b1c897 | ||
|
|
5854afa987 | ||
|
|
2aa2e42595 | ||
|
|
edf9e58d6d | ||
|
|
d344b501ab | ||
|
|
2bf4f2a0b5 | ||
|
|
de49e8b271 | ||
|
|
fb21f88c02 | ||
|
|
1044d2afc6 | ||
|
|
aaf2856573 | ||
|
|
1546c62e7f | ||
|
|
b8875f3cda | ||
|
|
febc0cae0b | ||
|
|
cc0c3c24d9 | ||
|
|
2fa4a837b1 | ||
|
|
32e299e13b | ||
|
|
f92d2e2ecd | ||
|
|
fffc48b41a | ||
|
|
0cc2dcdbc5 | ||
|
|
e9ca92a359 | ||
|
|
439965847a | ||
|
|
436c05487a | ||
|
|
fba300bc5c | ||
|
|
8c43de9fc0 | ||
|
|
c2b46d50a8 | ||
|
|
e2ce54dea8 | ||
|
|
af45d899e3 | ||
|
|
15a4ea42b2 | ||
|
|
555dddf8de | ||
|
|
b03e0fd904 | ||
|
|
a0c2e86a95 | ||
|
|
39682badef | ||
|
|
45afc3fbee | ||
|
|
15d20dd693 | ||
|
|
2d77ad7125 | ||
|
|
33df4d2b7f | ||
|
|
09b16976fd | ||
|
|
1b430f99fe | ||
|
|
982f849f41 | ||
|
|
6ec3a4cb5a | ||
|
|
4d29b202b1 | ||
|
|
99ee1da598 | ||
|
|
b896a64853 | ||
|
|
94ab6d016e | ||
|
|
41006f5cbf | ||
|
|
9c2c1427e1 | ||
|
|
c19f21d257 | ||
|
|
1d98de1e0c | ||
|
|
64eb268cb0 | ||
|
|
f7428db3c3 | ||
|
|
7986db7027 | ||
|
|
7704a8a5d7 | ||
|
|
d88f83311c | ||
|
|
6f0a69b838 | ||
|
|
7ed1be1960 | ||
|
|
663559f8c7 | ||
|
|
4a56673377 | ||
|
|
d1f19a9dc4 | ||
|
|
8a3722a723 | ||
|
|
4abf6ebdce | ||
|
|
d1013802ba | ||
|
|
581e8c4769 | ||
|
|
ea5c7f516a | ||
|
|
ce7cef0c6b | ||
|
|
45a823e6c7 | ||
|
|
0b8cf06c29 | ||
|
|
f93519f675 | ||
|
|
c39b3ae4c5 | ||
|
|
c3ea12225d | ||
|
|
f914d83510 | ||
|
|
67ea30a45a | ||
|
|
eabbb440f0 | ||
|
|
8735f219e9 | ||
|
|
c2414ce1ba | ||
|
|
e6fee7b5b9 | ||
|
|
d8f3c7441e | ||
|
|
765bf2089c | ||
|
|
10fce146fd | ||
|
|
b274cb5137 | ||
|
|
a6e539dad2 |
12
.env
12
.env
@@ -2,14 +2,20 @@
|
|||||||
# If you add a new learning MFE-specific variable, please note it there!
|
# If you add a new learning MFE-specific variable, please note it there!
|
||||||
|
|
||||||
NODE_ENV='production'
|
NODE_ENV='production'
|
||||||
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME=''
|
ACCESS_TOKEN_COOKIE_NAME=''
|
||||||
BASE_URL=''
|
BASE_URL=''
|
||||||
CONTACT_URL=''
|
CONTACT_URL=''
|
||||||
CREDENTIALS_BASE_URL=''
|
CREDENTIALS_BASE_URL=''
|
||||||
|
CREDIT_HELP_LINK_URL=''
|
||||||
CSRF_TOKEN_API_PATH=''
|
CSRF_TOKEN_API_PATH=''
|
||||||
DISCOVERY_API_BASE_URL=''
|
DISCOVERY_API_BASE_URL=''
|
||||||
|
DISCUSSIONS_MFE_BASE_URL=''
|
||||||
ECOMMERCE_BASE_URL=''
|
ECOMMERCE_BASE_URL=''
|
||||||
|
ENABLE_JUMPNAV='true'
|
||||||
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
||||||
|
FAVICON_URL=''
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
INSIGHTS_BASE_URL=''
|
INSIGHTS_BASE_URL=''
|
||||||
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
||||||
@@ -19,12 +25,13 @@ LOGOUT_URL=''
|
|||||||
LOGO_URL=''
|
LOGO_URL=''
|
||||||
LOGO_TRADEMARK_URL=''
|
LOGO_TRADEMARK_URL=''
|
||||||
LOGO_WHITE_URL=''
|
LOGO_WHITE_URL=''
|
||||||
FAVICON_URL=''
|
LEGACY_THEME_NAME=''
|
||||||
MARKETING_SITE_BASE_URL=''
|
MARKETING_SITE_BASE_URL=''
|
||||||
ORDER_HISTORY_URL=''
|
ORDER_HISTORY_URL=''
|
||||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||||
SEARCH_CATALOG_URL=''
|
SEARCH_CATALOG_URL=''
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
|
SESSION_COOKIE_DOMAIN=''
|
||||||
SITE_NAME=''
|
SITE_NAME=''
|
||||||
SOCIAL_UTM_MILESTONE_CAMPAIGN=''
|
SOCIAL_UTM_MILESTONE_CAMPAIGN=''
|
||||||
STUDIO_BASE_URL=''
|
STUDIO_BASE_URL=''
|
||||||
@@ -36,6 +43,3 @@ TERMS_OF_SERVICE_URL=''
|
|||||||
TWITTER_HASHTAG=''
|
TWITTER_HASHTAG=''
|
||||||
TWITTER_URL=''
|
TWITTER_URL=''
|
||||||
USER_INFO_COOKIE_NAME=''
|
USER_INFO_COOKIE_NAME=''
|
||||||
SESSION_COOKIE_DOMAIN=''
|
|
||||||
ENABLE_JUMPNAV='true'
|
|
||||||
ENABLE_NOTICES=''
|
|
||||||
|
|||||||
@@ -2,14 +2,20 @@
|
|||||||
# If you add a new learning MFE-specific variable, please note it there!
|
# If you add a new learning MFE-specific variable, please note it there!
|
||||||
|
|
||||||
NODE_ENV='development'
|
NODE_ENV='development'
|
||||||
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||||
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'
|
||||||
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'
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
|
ENABLE_JUMPNAV='true'
|
||||||
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||||
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||||
LMS_BASE_URL='http://localhost:18000'
|
LMS_BASE_URL='http://localhost:18000'
|
||||||
@@ -18,7 +24,7 @@ LOGOUT_URL='http://localhost:18000/logout'
|
|||||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
LEGACY_THEME_NAME=''
|
||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||||
PORT=2000
|
PORT=2000
|
||||||
@@ -37,5 +43,3 @@ 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'
|
||||||
SESSION_COOKIE_DOMAIN='localhost'
|
SESSION_COOKIE_DOMAIN='localhost'
|
||||||
ENABLE_JUMPNAV='true'
|
|
||||||
ENABLE_NOTICES=''
|
|
||||||
|
|||||||
10
.env.test
10
.env.test
@@ -2,14 +2,20 @@
|
|||||||
# If you add a new learning MFE-specific variable, please note it there!
|
# If you add a new learning MFE-specific variable, please note it there!
|
||||||
|
|
||||||
NODE_ENV='test'
|
NODE_ENV='test'
|
||||||
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||||
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'
|
||||||
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'
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
|
ENABLE_JUMPNAV='true'
|
||||||
|
ENABLE_NOTICES=''
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||||
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
IGNORED_ERROR_REGEX=''
|
IGNORED_ERROR_REGEX=''
|
||||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||||
LMS_BASE_URL='http://localhost:18000'
|
LMS_BASE_URL='http://localhost:18000'
|
||||||
@@ -18,7 +24,7 @@ LOGOUT_URL='http://localhost:18000/logout'
|
|||||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
LEGACY_THEME_NAME=''
|
||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||||
PORT=2000
|
PORT=2000
|
||||||
@@ -36,5 +42,3 @@ TERMS_OF_SERVICE_URL='https://www.edx.org/edx-terms-service'
|
|||||||
TWITTER_HASHTAG='myedxjourney'
|
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'
|
||||||
ENABLE_JUMPNAV='true'
|
|
||||||
ENABLE_NOTICES=''
|
|
||||||
|
|||||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Run the workflow that adds new tickets that are either:
|
||||||
|
# - labelled "DEPR"
|
||||||
|
# - title starts with "[DEPR]"
|
||||||
|
# - body starts with "Proposal Date" (this is the first template field)
|
||||||
|
# to the org-wide DEPR project board
|
||||||
|
|
||||||
|
name: Add newly created DEPR issues to the DEPR project board
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
routeissue:
|
||||||
|
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
|
||||||
|
secrets:
|
||||||
|
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||||
|
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||||
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||||
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
commitlint:
|
commitlint:
|
||||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||||
|
|||||||
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#check package-lock file version
|
||||||
|
|
||||||
|
name: Lockfile Version check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version-check:
|
||||||
|
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||||
21
.github/workflows/validate.yml
vendored
21
.github/workflows/validate.yml
vendored
@@ -1,21 +1,24 @@
|
|||||||
name: validate
|
name: validate
|
||||||
on:
|
on:
|
||||||
- push
|
push:
|
||||||
- pull_request
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version:
|
node: [16]
|
||||||
- 12
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node }}
|
||||||
- run: make validate.ci
|
- run: make validate.ci
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v2
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
|||||||
1
.husky/_/.gitignore
vendored
Normal file
1
.husky/_/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*
|
||||||
31
.husky/_/husky.sh
Normal file
31
.husky/_/husky.sh
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
if [ -z "$husky_skip_init" ]; then
|
||||||
|
debug () {
|
||||||
|
if [ "$HUSKY_DEBUG" = "1" ]; then
|
||||||
|
echo "husky (debug) - $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly hook_name="$(basename "$0")"
|
||||||
|
debug "starting $hook_name..."
|
||||||
|
|
||||||
|
if [ "$HUSKY" = "0" ]; then
|
||||||
|
debug "HUSKY env variable is set to 0, skipping hook"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f ~/.huskyrc ]; then
|
||||||
|
debug "sourcing ~/.huskyrc"
|
||||||
|
. ~/.huskyrc
|
||||||
|
fi
|
||||||
|
|
||||||
|
export readonly husky_skip_init=1
|
||||||
|
sh -e "$0" "$@"
|
||||||
|
exitCode="$?"
|
||||||
|
|
||||||
|
if [ $exitCode != 0 ]; then
|
||||||
|
echo "husky - $hook_name hook exited with code $exitCode (error)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $exitCode
|
||||||
|
fi
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
[main]
|
[main]
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
|
|
||||||
[edx-platform.frontend-app-learning]
|
[o:open-edx:p:edx-platform:r:frontend-app-learning]
|
||||||
file_filter = src/i18n/messages/<lang>.json
|
file_filter = src/i18n/messages/<lang>.json
|
||||||
source_file = src/i18n/transifex_input.json
|
source_file = src/i18n/transifex_input.json
|
||||||
source_lang = en
|
source_lang = en
|
||||||
type = KEYVALUEJSON
|
type = KEYVALUEJSON
|
||||||
|
|
||||||
|
|||||||
15
Makefile
15
Makefile
@@ -1,11 +1,9 @@
|
|||||||
transifex_resource = frontend-app-learning
|
export TRANSIFEX_RESOURCE=frontend-app-learning
|
||||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
|
||||||
|
|
||||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||||
i18n = ./src/i18n
|
i18n = ./src/i18n
|
||||||
transifex_input = $(i18n)/transifex_input.json
|
transifex_input = $(i18n)/transifex_input.json
|
||||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
|
||||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
|
||||||
|
|
||||||
# This directory must match .babelrc .
|
# This directory must match .babelrc .
|
||||||
transifex_temp = ./temp/babel-plugin-react-intl
|
transifex_temp = ./temp/babel-plugin-react-intl
|
||||||
@@ -38,15 +36,15 @@ push_translations:
|
|||||||
# Pushing strings to Transifex...
|
# Pushing strings to Transifex...
|
||||||
tx push -s
|
tx push -s
|
||||||
# Fetching hashes from Transifex...
|
# Fetching hashes from Transifex...
|
||||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||||
# Writing out comments to file...
|
# Writing out comments to file...
|
||||||
$(transifex_utils) $(transifex_temp) --comments
|
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||||
# Pushing comments to Transifex...
|
# Pushing comments to Transifex...
|
||||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||||
|
|
||||||
# Pulls translations from Transifex.
|
# Pulls translations from Transifex.
|
||||||
pull_translations:
|
pull_translations:
|
||||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||||
|
|
||||||
# This target is used by Travis.
|
# This target is used by Travis.
|
||||||
validate-no-uncommitted-package-lock-changes:
|
validate-no-uncommitted-package-lock-changes:
|
||||||
@@ -60,7 +58,6 @@ validate:
|
|||||||
npm run lint -- --max-warnings 0
|
npm run lint -- --max-warnings 0
|
||||||
npm run test
|
npm run test
|
||||||
npm run build
|
npm run build
|
||||||
npm run is-es5
|
|
||||||
|
|
||||||
.PHONY: validate.ci
|
.PHONY: validate.ci
|
||||||
validate.ci:
|
validate.ci:
|
||||||
|
|||||||
20
README.rst
20
README.rst
@@ -15,7 +15,7 @@ Please tag **@edx/engage-squad** on any PRs or issues. Thanks.
|
|||||||
.. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3
|
.. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3
|
||||||
:target: https://codecov.io/gh/edx/frontend-app-learning
|
:target: https://codecov.io/gh/edx/frontend-app-learning
|
||||||
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational
|
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational
|
||||||
:target: https://github.com/edx/frontend-app-account/blob/master/LICENSE
|
:target: https://github.com/openedx/frontend-app-account/blob/master/LICENSE
|
||||||
|
|
||||||
Development
|
Development
|
||||||
-----------
|
-----------
|
||||||
@@ -23,7 +23,7 @@ Development
|
|||||||
Start Devstack
|
Start Devstack
|
||||||
^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
To use this application, `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
|
To use this application, `devstack <https://github.com/openedx/devstack>`__ must be running and you must be logged into it.
|
||||||
|
|
||||||
- Run ``make dev.up.lms``
|
- Run ``make dev.up.lms``
|
||||||
- Visit http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course to view the demo course. You can replace ``course-v1:edX+DemoX+Demo_Course`` with a different course key.
|
- Visit http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course to view the demo course. You can replace ``course-v1:edX+DemoX+Demo_Course`` with a different course key.
|
||||||
@@ -52,7 +52,7 @@ file (which is git-ignored) that defines where to find your local modules, for i
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
See https://github.com/edx/frontend-build#local-module-configuration-for-webpack for more details.
|
See https://github.com/openedx/frontend-build#local-module-configuration-for-webpack for more details.
|
||||||
|
|
||||||
Deployment
|
Deployment
|
||||||
----------
|
----------
|
||||||
@@ -71,6 +71,15 @@ as documented in the Open edX Developer Guide under
|
|||||||
|
|
||||||
The learning micro-frontend also supports the following additional variables:
|
The learning micro-frontend also supports the following additional variables:
|
||||||
|
|
||||||
|
CREDIT_HELP_LINK_URL
|
||||||
|
A link to resources to help explain what course credit is and how to earn it.
|
||||||
|
|
||||||
|
ENABLE_JUMPNAV
|
||||||
|
Enables the new Jump Navigation feature in the course breadcrumbs, defaulted to the string 'true'.
|
||||||
|
Disable to have simple hyperlinks for breadcrumbs. Setting it to any other value but 'true' ('false','I love flags', 'etc' would disable the Jumpnav).
|
||||||
|
This feature flag is slated to be removed as jumpnav becomes default. Follow the progress of this ticket here:
|
||||||
|
https://openedx.atlassian.net/browse/TNL-8678
|
||||||
|
|
||||||
SOCIAL_UTM_MILESTONE_CAMPAIGN
|
SOCIAL_UTM_MILESTONE_CAMPAIGN
|
||||||
This value is passed as the ``utm_campaign`` parameter for social-share
|
This value is passed as the ``utm_campaign`` parameter for social-share
|
||||||
links when celebrating learning milestones in the course. Optional.
|
links when celebrating learning milestones in the course. Optional.
|
||||||
@@ -110,8 +119,3 @@ TWITTER_URL
|
|||||||
|
|
||||||
Example: https://twitter.com/edXOnline
|
Example: https://twitter.com/edXOnline
|
||||||
|
|
||||||
ENABLE_JUMPNAV
|
|
||||||
Enables the new Jump Navigation feature in the course breadcrumbs, defaulted to the string 'true'.
|
|
||||||
Disable to have simple hyperlinks for breadcrumbs. Setting it to any other value but 'true' ('false','I love flags', 'etc' would disable the Jumpnav).
|
|
||||||
This feature flag is slated to be removed as jumpnav becomes default. Follow the progress of this ticket here:
|
|
||||||
https://openedx.atlassian.net/browse/TNL-8678
|
|
||||||
|
|||||||
40983
package-lock.json
generated
40983
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
67
package.json
67
package.json
@@ -4,16 +4,14 @@
|
|||||||
"description": "Frontend learning application.",
|
"description": "Frontend learning application.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/edx/frontend-app-learning.git"
|
"url": "git+https://github.com/openedx/frontend-app-learning.git"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 2 versions",
|
"extends @edx/browserslist-config"
|
||||||
"ie 11"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "fedx-scripts webpack",
|
"build": "fedx-scripts webpack",
|
||||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||||
"is-es5": "es-check es5 ./dist/*.js",
|
|
||||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
@@ -23,60 +21,59 @@
|
|||||||
},
|
},
|
||||||
"author": "edX",
|
"author": "edX",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"homepage": "https://github.com/edx/frontend-app-learning#readme",
|
"homepage": "https://github.com/openedx/frontend-app-learning#readme",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/edx/frontend-app-learning/issues"
|
"url": "https://github.com/openedx/frontend-app-learning/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||||
"@edx/frontend-component-footer": "10.1.6",
|
"@edx/frontend-component-footer": "11.1.0",
|
||||||
"@edx/frontend-enterprise-utils": "1.1.0",
|
"@edx/frontend-component-header": "3.1.0",
|
||||||
"@edx/frontend-lib-special-exams": "1.13.3",
|
"@edx/frontend-lib-special-exams": "2.1.0",
|
||||||
"@edx/frontend-platform": "1.12.7",
|
"@edx/frontend-platform": "2.5.1",
|
||||||
"@edx/paragon": "16.14.9",
|
"@edx/paragon": "^20.20.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
"@fortawesome/fontawesome-svg-core": "1.3.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.15",
|
"@fortawesome/react-fontawesome": "0.1.18",
|
||||||
"@pact-foundation/pact": "9.16.4",
|
"@popperjs/core": "2.11.5",
|
||||||
"@reduxjs/toolkit": "1.6.2",
|
"@reduxjs/toolkit": "1.8.1",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"core-js": "3.18.3",
|
"core-js": "3.22.2",
|
||||||
|
"history": "^5.3.0",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"lodash.camelcase": "4.3.0",
|
"lodash.camelcase": "4.3.0",
|
||||||
"prop-types": "15.7.2",
|
"prop-types": "15.8.1",
|
||||||
"react": "17.0.2",
|
"query-string": "^7.1.1",
|
||||||
"react-break": "1.3.2",
|
"react": "16.14.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "16.14.0",
|
||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-redux": "7.2.5",
|
"react-redux": "7.2.8",
|
||||||
"react-router": "5.2.1",
|
"react-router": "5.2.1",
|
||||||
"react-router-dom": "5.3.0",
|
"react-router-dom": "5.3.0",
|
||||||
"react-share": "4.4.0",
|
"react-share": "4.4.0",
|
||||||
"redux": "4.1.1",
|
"redux": "4.1.2",
|
||||||
"regenerator-runtime": "0.13.9",
|
"regenerator-runtime": "0.13.9",
|
||||||
"reselect": "4.0.0",
|
"reselect": "4.1.5",
|
||||||
"truncate-html": "1.0.4",
|
"truncate-html": "1.0.4",
|
||||||
"util": "0.12.4"
|
"util": "0.12.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/frontend-build": "8.0.4",
|
"@edx/browserslist-config": "1.0.2",
|
||||||
"@testing-library/dom": "7.16.3",
|
"@edx/frontend-build": "9.1.4",
|
||||||
"@testing-library/jest-dom": "5.14.1",
|
"@edx/reactifex": "2.0.1",
|
||||||
"@testing-library/react": "10.3.0",
|
"@pact-foundation/pact": "9.17.3",
|
||||||
"@testing-library/user-event": "13.4.1",
|
"@testing-library/jest-dom": "5.16.4",
|
||||||
|
"@testing-library/react": "12.1.5",
|
||||||
|
"@testing-library/user-event": "13.5.0",
|
||||||
"axios-mock-adapter": "1.20.0",
|
"axios-mock-adapter": "1.20.0",
|
||||||
"codecov": "3.8.3",
|
"es-check": "6.2.1",
|
||||||
"es-check": "6.0.0",
|
"husky": "7.0.4",
|
||||||
"glob": "7.2.0",
|
"jest": "27.5.1",
|
||||||
"husky": "7.0.2",
|
|
||||||
"jest": "27.2.5",
|
|
||||||
"jest-chain": "1.1.5",
|
|
||||||
"reactifex": "1.1.1",
|
|
||||||
"rosie": "2.1.0"
|
"rosie": "2.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,5 +5,12 @@
|
|||||||
"patch": {
|
"patch": {
|
||||||
"automerge": true
|
"automerge": true
|
||||||
},
|
},
|
||||||
"rebaseStalePrs": true
|
"rebaseStalePrs": true,
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchPackagePatterns": ["@edx"],
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"automerge": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ function AccessExpirationAlert({ intl, payload }) {
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="learning.accessExpiration.deadline"
|
id="learning.accessExpiration.deadline"
|
||||||
defaultMessage="Upgrade by {date} to get unlimited access to the course as long as it exists on the site."
|
defaultMessage="Upgrade by {date} to get unlimited access to the course as long as it exists on the site."
|
||||||
|
description="Warning shown to learner to upgrade while they are enrolled on the audit version and it's possible to upgrade"
|
||||||
values={{
|
values={{
|
||||||
date: (
|
date: (
|
||||||
<FormattedDate
|
<FormattedDate
|
||||||
@@ -97,6 +98,7 @@ function AccessExpirationAlert({ intl, payload }) {
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="learning.accessExpiration.header"
|
id="learning.accessExpiration.header"
|
||||||
defaultMessage="Audit Access Expires {date}"
|
defaultMessage="Audit Access Expires {date}"
|
||||||
|
description="Headline for auditing deadline"
|
||||||
values={{
|
values={{
|
||||||
date: (
|
date: (
|
||||||
<FormattedDate
|
<FormattedDate
|
||||||
@@ -115,6 +117,7 @@ function AccessExpirationAlert({ intl, payload }) {
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="learning.accessExpiration.body"
|
id="learning.accessExpiration.body"
|
||||||
defaultMessage="You lose all access to this course, including your progress, on {date}."
|
defaultMessage="You lose all access to this course, including your progress, on {date}."
|
||||||
|
description="Message body to tell learner the consequences of course expiration."
|
||||||
values={{
|
values={{
|
||||||
date: (
|
date: (
|
||||||
<FormattedDate
|
<FormattedDate
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function AccessExpirationMasqueradeBanner({ payload }) {
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="instructorToolbar.pageBanner.courseHasExpired"
|
id="instructorToolbar.pageBanner.courseHasExpired"
|
||||||
defaultMessage="This learner no longer has access to this course. Their access expired on {date}."
|
defaultMessage="This learner no longer has access to this course. Their access expired on {date}."
|
||||||
|
description="It's a warning that is shown to course author when being masqueraded as learner, while the course has expired for the real learner."
|
||||||
values={{
|
values={{
|
||||||
date: <FormattedDate
|
date: <FormattedDate
|
||||||
key="instructorToolbar.pageBanner.accessExpirationDate"
|
key="instructorToolbar.pageBanner.accessExpirationDate"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const messages = defineMessages({
|
|||||||
upgradeNow: {
|
upgradeNow: {
|
||||||
id: 'learning.accessExpiration.upgradeNow',
|
id: 'learning.accessExpiration.upgradeNow',
|
||||||
defaultMessage: 'Upgrade now',
|
defaultMessage: 'Upgrade now',
|
||||||
|
description: 'The anchor text for the upgrading link',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
48
src/alerts/active-enteprise-alert/ActiveEnterpriseAlert.jsx
Normal file
48
src/alerts/active-enteprise-alert/ActiveEnterpriseAlert.jsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Alert, Hyperlink } from '@edx/paragon';
|
||||||
|
import { WarningFilled } from '@edx/paragon/icons';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import genericMessages from './messages';
|
||||||
|
|
||||||
|
function ActiveEnterpriseAlert({ intl, payload }) {
|
||||||
|
const { text, courseId } = payload;
|
||||||
|
const changeActiveEnterprise = (
|
||||||
|
<Hyperlink
|
||||||
|
style={{ textDecoration: 'underline' }}
|
||||||
|
destination={
|
||||||
|
`${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=${encodeURIComponent(
|
||||||
|
`${global.location.origin}/course/${courseId}/home`,
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(genericMessages.changeActiveEnterpriseLowercase)}
|
||||||
|
</Hyperlink>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert variant="warning" icon={WarningFilled}>
|
||||||
|
{text}
|
||||||
|
<FormattedMessage
|
||||||
|
id="learning.activeEnterprise.alert"
|
||||||
|
description="Prompts the user to log-in with the correct enterprise to access the course content."
|
||||||
|
defaultMessage=" {changeActiveEnterprise}."
|
||||||
|
values={{
|
||||||
|
changeActiveEnterprise,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveEnterpriseAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
payload: PropTypes.shape({
|
||||||
|
text: PropTypes.string,
|
||||||
|
courseId: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(ActiveEnterpriseAlert);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import {
|
||||||
|
initializeTestStore, render, screen,
|
||||||
|
} from '../../setupTest';
|
||||||
|
import ActiveEnterpriseAlert from './ActiveEnterpriseAlert';
|
||||||
|
|
||||||
|
describe('ActiveEnterpriseAlert', () => {
|
||||||
|
const mockData = {
|
||||||
|
payload: {
|
||||||
|
text: 'test message',
|
||||||
|
courseId: 'test-course-id',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shows alert message and links', () => {
|
||||||
|
render(<ActiveEnterpriseAlert {...mockData} />);
|
||||||
|
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('test message', { exact: false })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('link', { name: 'change enterprise now' })).toHaveAttribute(
|
||||||
|
'href', `${getConfig().LMS_BASE_URL}/enterprise/select/active/?success_url=http%3A%2F%2Flocalhost%2Fcourse%2Ftest-course-id%2Fhome`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
28
src/alerts/active-enteprise-alert/hooks.js
Normal file
28
src/alerts/active-enteprise-alert/hooks.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { ALERT_TYPES, useAlert } from '../../generic/user-messages';
|
||||||
|
import { useModel } from '../../generic/model-store';
|
||||||
|
|
||||||
|
const ActiveEnterpriseAlert = React.lazy(() => import('./ActiveEnterpriseAlert'));
|
||||||
|
|
||||||
|
export default function useActiveEnterpriseAlert(courseId) {
|
||||||
|
const { courseAccess } = useModel('courseHomeMeta', courseId);
|
||||||
|
/**
|
||||||
|
* This alert should render if
|
||||||
|
* 1. course access code is incorrect_active_enterprise
|
||||||
|
*/
|
||||||
|
const isVisible = courseAccess && !courseAccess.hasAccess && courseAccess.errorCode === 'incorrect_active_enterprise';
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
text: courseAccess && courseAccess.userMessage,
|
||||||
|
courseId,
|
||||||
|
};
|
||||||
|
useAlert(isVisible, {
|
||||||
|
code: 'clientActiveEnterpriseAlert',
|
||||||
|
topic: 'outline',
|
||||||
|
dismissible: false,
|
||||||
|
type: ALERT_TYPES.ERROR,
|
||||||
|
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { clientActiveEnterpriseAlert: ActiveEnterpriseAlert };
|
||||||
|
}
|
||||||
3
src/alerts/active-enteprise-alert/index.js
Normal file
3
src/alerts/active-enteprise-alert/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import useActiveEnterpriseAlert from './hooks';
|
||||||
|
|
||||||
|
export default useActiveEnterpriseAlert;
|
||||||
11
src/alerts/active-enteprise-alert/messages.js
Normal file
11
src/alerts/active-enteprise-alert/messages.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
changeActiveEnterpriseLowercase: {
|
||||||
|
id: 'learning.activeEnterprise.change.alert',
|
||||||
|
defaultMessage: 'change enterprise now',
|
||||||
|
description: 'Text in a link, prompting the user to change active enterprise. Used in learning.activeEnterprise.change.alert"',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import {
|
import {
|
||||||
FormattedDate,
|
FormattedDate,
|
||||||
FormattedMessage,
|
FormattedMessage,
|
||||||
FormattedRelative,
|
FormattedRelativeTime,
|
||||||
FormattedTime,
|
FormattedTime,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Alert } from '@edx/paragon';
|
import { Alert } from '@edx/paragon';
|
||||||
@@ -26,7 +26,7 @@ function CourseStartAlert({ payload }) {
|
|||||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
|
|
||||||
const timeRemaining = (
|
const timeRemaining = (
|
||||||
<FormattedRelative
|
<FormattedRelativeTime
|
||||||
key="timeRemaining"
|
key="timeRemaining"
|
||||||
value={startDate}
|
value={startDate}
|
||||||
{...timezoneFormatArgs}
|
{...timezoneFormatArgs}
|
||||||
@@ -86,6 +86,7 @@ function CourseStartAlert({ payload }) {
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="learning.outline.alert.end.calendar"
|
id="learning.outline.alert.end.calendar"
|
||||||
defaultMessage="Don’t forget to add a calendar reminder!"
|
defaultMessage="Don’t forget to add a calendar reminder!"
|
||||||
|
description="It's just a recommendation for learners to set a reminder for the course starting date and is shown when the course starting date is more than a day. "
|
||||||
/>
|
/>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function CourseStartMasqueradeBanner({ payload }) {
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="instructorToolbar.pageBanner.courseHasNotStarted"
|
id="instructorToolbar.pageBanner.courseHasNotStarted"
|
||||||
defaultMessage="This learner does not yet have access to this course. The course starts on {date}."
|
defaultMessage="This learner does not yet have access to this course. The course starts on {date}."
|
||||||
|
description="It's a warning that is shown to course author when being masqueraded as learner, while the course hasn't started for the real learner yet."
|
||||||
values={{
|
values={{
|
||||||
date: <FormattedDate
|
date: <FormattedDate
|
||||||
key="instructorToolbar.pageBanner.courseStartDate"
|
key="instructorToolbar.pageBanner.courseStartDate"
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import { Check, ArrowForward } from '@edx/paragon/icons';
|
import { Check, ArrowForward } from '@edx/paragon/icons';
|
||||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { sendActivationEmail } from '../../courseware/data';
|
import { sendActivationEmail } from '../../courseware/data';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
function AccountActivationAlert() {
|
function AccountActivationAlert({
|
||||||
|
intl,
|
||||||
|
}) {
|
||||||
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);
|
||||||
@@ -29,22 +32,12 @@ function AccountActivationAlert() {
|
|||||||
if (showAccountActivationAlert !== undefined) {
|
if (showAccountActivationAlert !== undefined) {
|
||||||
Cookies.remove('show-account-activation-popup', { path: '/', domain: process.env.SESSION_COOKIE_DOMAIN });
|
Cookies.remove('show-account-activation-popup', { path: '/', domain: process.env.SESSION_COOKIE_DOMAIN });
|
||||||
// extra check to make sure cookie was removed before updating the state. Updating the state without removal
|
// extra check to make sure cookie was removed before updating the state. Updating the state without removal
|
||||||
// of cookie would make it infinit rendering
|
// of cookie would make it infinite rendering
|
||||||
if (Cookies.get('show-account-activation-popup') === undefined) {
|
if (Cookies.get('show-account-activation-popup') === undefined) {
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = (
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage
|
|
||||||
id="account-activation.alert.title"
|
|
||||||
defaultMessage="Activate your account so you can log back in"
|
|
||||||
description="Title for account activation alert which is shown after the registration"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -64,7 +57,7 @@ function AccountActivationAlert() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const children = () => {
|
const children = () => {
|
||||||
let bodyContent = null;
|
let bodyContent;
|
||||||
const message = (
|
const message = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="account-activation.alert.message"
|
id="account-activation.alert.message"
|
||||||
@@ -123,7 +116,7 @@ function AccountActivationAlert() {
|
|||||||
return (
|
return (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={showModal}
|
isOpen={showModal}
|
||||||
title={title}
|
title={intl.formatMessage(messages.accountActivationAlertTitle)}
|
||||||
footerNode={button}
|
footerNode={button}
|
||||||
onClose={() => ({})}
|
onClose={() => ({})}
|
||||||
>
|
>
|
||||||
@@ -132,4 +125,8 @@ function AccountActivationAlert() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountActivationAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default injectIntl(AccountActivationAlert);
|
export default injectIntl(AccountActivationAlert);
|
||||||
|
|||||||
11
src/alerts/logistration-alert/messages.js
Normal file
11
src/alerts/logistration-alert/messages.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
accountActivationAlertTitle: {
|
||||||
|
id: 'account-activation.alert.title',
|
||||||
|
defaultMessage: 'Activate your account so you can log back in',
|
||||||
|
description: 'Title for account activation alert which is shown after the registration',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
56
src/alerts/sequence-alerts/hooks.js
Normal file
56
src/alerts/sequence-alerts/hooks.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { useModel } from '../../generic/model-store';
|
||||||
|
import { ALERT_TYPES, useAlert } from '../../generic/user-messages';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
function useSequenceBannerTextAlert(sequenceId) {
|
||||||
|
const sequence = useModel('sequences', sequenceId);
|
||||||
|
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||||
|
|
||||||
|
// Show Alert that comes along with the sequence
|
||||||
|
useAlert(sequenceStatus === 'loaded' && sequence.bannerText, {
|
||||||
|
code: null,
|
||||||
|
dismissible: false,
|
||||||
|
text: sequence.bannerText,
|
||||||
|
type: ALERT_TYPES.INFO,
|
||||||
|
topic: 'sequence',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function useSequenceEntranceExamAlert(courseId, sequenceId, intl) {
|
||||||
|
const course = useModel('coursewareMeta', courseId);
|
||||||
|
const sequence = useModel('sequences', sequenceId);
|
||||||
|
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||||
|
const {
|
||||||
|
entranceExamCurrentScore,
|
||||||
|
entranceExamEnabled,
|
||||||
|
entranceExamId,
|
||||||
|
entranceExamMinimumScorePct,
|
||||||
|
entranceExamPassed,
|
||||||
|
} = course.entranceExamData || {};
|
||||||
|
const entranceExamAlertVisible = sequenceStatus === 'loaded' && entranceExamEnabled && entranceExamId === sequence.sectionId;
|
||||||
|
let entranceExamText;
|
||||||
|
|
||||||
|
if (entranceExamPassed) {
|
||||||
|
entranceExamText = intl.formatMessage(
|
||||||
|
messages.entranceExamTextPassed, { entranceExamCurrentScore: entranceExamCurrentScore * 100 },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
entranceExamText = intl.formatMessage(messages.entranceExamTextNotPassing, {
|
||||||
|
entranceExamCurrentScore: entranceExamCurrentScore * 100,
|
||||||
|
entranceExamMinimumScorePct: entranceExamMinimumScorePct * 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useAlert(entranceExamAlertVisible, {
|
||||||
|
code: null,
|
||||||
|
dismissible: false,
|
||||||
|
text: entranceExamText,
|
||||||
|
type: ALERT_TYPES.INFO,
|
||||||
|
topic: 'sequence',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useSequenceBannerTextAlert, useSequenceEntranceExamAlert };
|
||||||
14
src/alerts/sequence-alerts/messages.js
Normal file
14
src/alerts/sequence-alerts/messages.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
entranceExamTextNotPassing: {
|
||||||
|
id: 'learn.sequence.entranceExamTextNotPassing',
|
||||||
|
defaultMessage: 'To access course materials, you must score {entranceExamMinimumScorePct}% or higher on this exam. Your current score is {entranceExamCurrentScore}%.',
|
||||||
|
},
|
||||||
|
entranceExamTextPassed: {
|
||||||
|
id: 'learn.sequence.entranceExamTextPassed',
|
||||||
|
defaultMessage: 'Your score is {entranceExamCurrentScore}%. You have passed the entrance exam.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Button } from '@edx/paragon';
|
|
||||||
|
|
||||||
import genericMessages from '../generic/messages';
|
|
||||||
|
|
||||||
function AnonymousUserMenu({ intl }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="mr-3"
|
|
||||||
variant="outline-primary"
|
|
||||||
href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
|
|
||||||
>
|
|
||||||
{intl.formatMessage(genericMessages.registerSentenceCase)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
href={`${getLoginRedirectUrl(global.location.href)}`}
|
|
||||||
>
|
|
||||||
{intl.formatMessage(genericMessages.signInSentenceCase)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AnonymousUserMenu.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(AnonymousUserMenu);
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Dropdown } from '@edx/paragon';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
function AuthenticatedUserDropdown({ enterpriseLearnerPortalLink, intl, username }) {
|
|
||||||
let dashboardMenuItem = (
|
|
||||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
|
||||||
{intl.formatMessage(messages.dashboard)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
);
|
|
||||||
if (enterpriseLearnerPortalLink && Object.keys(enterpriseLearnerPortalLink).length > 0) {
|
|
||||||
dashboardMenuItem = (
|
|
||||||
<Dropdown.Item href={enterpriseLearnerPortalLink.href}>
|
|
||||||
{enterpriseLearnerPortalLink.content}
|
|
||||||
</Dropdown.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
|
||||||
<Dropdown className="user-dropdown">
|
|
||||||
<Dropdown.Toggle variant="outline-primary">
|
|
||||||
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
|
||||||
<span data-hj-suppress className="d-none d-md-inline">
|
|
||||||
{username}
|
|
||||||
</span>
|
|
||||||
</Dropdown.Toggle>
|
|
||||||
<Dropdown.Menu className="dropdown-menu-right">
|
|
||||||
{dashboardMenuItem}
|
|
||||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}>
|
|
||||||
{intl.formatMessage(messages.profile)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
|
||||||
{intl.formatMessage(messages.account)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
{!enterpriseLearnerPortalLink && (
|
|
||||||
// Users should only see Order History if they do not have an available
|
|
||||||
// learner portal, because an available learner portal currently means
|
|
||||||
// that they access content via Subscriptions, in which context an "order"
|
|
||||||
// is not relevant.
|
|
||||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
|
||||||
{intl.formatMessage(messages.orderHistory)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
)}
|
|
||||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
|
||||||
{intl.formatMessage(messages.signOut)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticatedUserDropdown.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
enterpriseLearnerPortalLink: PropTypes.shape({
|
|
||||||
type: PropTypes.string,
|
|
||||||
href: PropTypes.string,
|
|
||||||
content: PropTypes.string,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
AuthenticatedUserDropdown.defaultProps = {
|
|
||||||
enterpriseLearnerPortalLink: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(AuthenticatedUserDropdown);
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import React, { useContext } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useEnterpriseConfig } from '@edx/frontend-enterprise-utils';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
|
||||||
|
|
||||||
import AnonymousUserMenu from './AnonymousUserMenu';
|
|
||||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
function LinkedLogo({
|
|
||||||
href,
|
|
||||||
src,
|
|
||||||
alt,
|
|
||||||
...attributes
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<a href={href} {...attributes}>
|
|
||||||
<img className="d-block" src={src} alt={alt} />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LinkedLogo.propTypes = {
|
|
||||||
href: PropTypes.string.isRequired,
|
|
||||||
src: PropTypes.string.isRequired,
|
|
||||||
alt: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function Header({
|
|
||||||
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
|
|
||||||
}) {
|
|
||||||
const { authenticatedUser } = useContext(AppContext);
|
|
||||||
|
|
||||||
const { enterpriseLearnerPortalLink, enterpriseCustomerBrandingConfig } = useEnterpriseConfig(
|
|
||||||
authenticatedUser,
|
|
||||||
getConfig().ENTERPRISE_LEARNER_PORTAL_HOSTNAME,
|
|
||||||
getConfig().LMS_BASE_URL,
|
|
||||||
);
|
|
||||||
|
|
||||||
let headerLogo = (
|
|
||||||
<LinkedLogo
|
|
||||||
className="logo"
|
|
||||||
href={`${getConfig().LMS_BASE_URL}/dashboard`}
|
|
||||||
src={getConfig().LOGO_URL}
|
|
||||||
alt={getConfig().SITE_NAME}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
if (enterpriseCustomerBrandingConfig && Object.keys(enterpriseCustomerBrandingConfig).length > 0) {
|
|
||||||
headerLogo = (
|
|
||||||
<LinkedLogo
|
|
||||||
className="logo"
|
|
||||||
href={enterpriseCustomerBrandingConfig.logoDestination}
|
|
||||||
src={enterpriseCustomerBrandingConfig.logo}
|
|
||||||
alt={enterpriseCustomerBrandingConfig.logoAltText}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="course-header">
|
|
||||||
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
|
|
||||||
<div className="container-xl py-2 d-flex align-items-center">
|
|
||||||
{headerLogo}
|
|
||||||
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
|
|
||||||
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
|
||||||
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
|
||||||
</div>
|
|
||||||
{showUserDropdown && authenticatedUser && (
|
|
||||||
<AuthenticatedUserDropdown
|
|
||||||
enterpriseLearnerPortalLink={enterpriseLearnerPortalLink}
|
|
||||||
username={authenticatedUser.username}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showUserDropdown && !authenticatedUser && (
|
|
||||||
<AnonymousUserMenu />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Header.propTypes = {
|
|
||||||
courseOrg: PropTypes.string,
|
|
||||||
courseNumber: PropTypes.string,
|
|
||||||
courseTitle: PropTypes.string,
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
showUserDropdown: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
Header.defaultProps = {
|
|
||||||
courseOrg: null,
|
|
||||||
courseNumber: null,
|
|
||||||
courseTitle: null,
|
|
||||||
showUserDropdown: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(Header);
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
authenticatedUser, initializeMockApp, render, screen,
|
|
||||||
} from '../setupTest';
|
|
||||||
import { Header } from './index';
|
|
||||||
|
|
||||||
describe('Header', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
// We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`.
|
|
||||||
await initializeMockApp();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays user button', () => {
|
|
||||||
render(<Header />);
|
|
||||||
expect(screen.getByRole('button')).toHaveTextContent(authenticatedUser.username);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays course data', () => {
|
|
||||||
const courseData = {
|
|
||||||
courseOrg: 'course-org',
|
|
||||||
courseNumber: 'course-number',
|
|
||||||
courseTitle: 'course-title',
|
|
||||||
};
|
|
||||||
render(<Header {...courseData} />);
|
|
||||||
|
|
||||||
expect(screen.getByText(`${courseData.courseOrg} ${courseData.courseNumber}`)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(courseData.courseTitle)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
courseMaterial: {
|
|
||||||
id: 'learn.navigation.course.tabs.label',
|
|
||||||
defaultMessage: 'Course Material',
|
|
||||||
description: 'The accessible label for course tabs navigation',
|
|
||||||
},
|
|
||||||
dashboard: {
|
|
||||||
id: 'header.menu.dashboard.label',
|
|
||||||
defaultMessage: 'Dashboard',
|
|
||||||
description: 'The text for the user menu Dashboard navigation link.',
|
|
||||||
},
|
|
||||||
help: {
|
|
||||||
id: 'header.help.label',
|
|
||||||
defaultMessage: 'Help',
|
|
||||||
description: 'The text for the link to the Help Center',
|
|
||||||
},
|
|
||||||
profile: {
|
|
||||||
id: 'header.menu.profile.label',
|
|
||||||
defaultMessage: 'Profile',
|
|
||||||
description: 'The text for the user menu Profile navigation link.',
|
|
||||||
},
|
|
||||||
account: {
|
|
||||||
id: 'header.menu.account.label',
|
|
||||||
defaultMessage: 'Account',
|
|
||||||
description: 'The text for the user menu Account navigation link.',
|
|
||||||
},
|
|
||||||
orderHistory: {
|
|
||||||
id: 'header.menu.orderHistory.label',
|
|
||||||
defaultMessage: 'Order History',
|
|
||||||
description: 'The text for the user menu Order History navigation link.',
|
|
||||||
},
|
|
||||||
skipNavLink: {
|
|
||||||
id: 'header.navigation.skipNavLink',
|
|
||||||
defaultMessage: 'Skip to main content.',
|
|
||||||
description: 'A link used by screen readers to allow users to skip to the main content of the page.',
|
|
||||||
},
|
|
||||||
signOut: {
|
|
||||||
id: 'header.menu.signOut.label',
|
|
||||||
defaultMessage: 'Sign Out',
|
|
||||||
description: 'The label for the user menu Sign Out action.',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default messages;
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
|
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
|
||||||
|
|
||||||
import courseMetadataBase from '../../../shared/data/__factories__/courseMetadataBase.factory';
|
import courseMetadataBase from '../../../shared/data/__factories__/courseMetadataBase.factory';
|
||||||
|
|
||||||
Factory.define('courseHomeMetadata')
|
Factory.define('courseHomeMetadata')
|
||||||
@@ -9,7 +8,9 @@ Factory.define('courseHomeMetadata')
|
|||||||
title: 'Demonstration Course',
|
title: 'Demonstration Course',
|
||||||
is_self_paced: false,
|
is_self_paced: false,
|
||||||
is_enrolled: false,
|
is_enrolled: false,
|
||||||
can_load_courseware: false,
|
is_staff: false,
|
||||||
|
can_view_certificate: true,
|
||||||
|
celebrations: null,
|
||||||
course_access: {
|
course_access: {
|
||||||
additional_context_user_message: null,
|
additional_context_user_message: null,
|
||||||
developer_message: null,
|
developer_message: null,
|
||||||
@@ -18,6 +19,106 @@ Factory.define('courseHomeMetadata')
|
|||||||
user_fragment: null,
|
user_fragment: null,
|
||||||
user_message: null,
|
user_message: null,
|
||||||
},
|
},
|
||||||
|
number: 'DemoX',
|
||||||
|
original_user_is_staff: false,
|
||||||
|
org: 'edX',
|
||||||
start: '2013-02-05T05:00:00Z',
|
start: '2013-02-05T05:00:00Z',
|
||||||
user_timezone: 'UTC',
|
user_timezone: 'UTC',
|
||||||
});
|
username: 'MockUser',
|
||||||
|
verified_mode: {
|
||||||
|
access_expiration_date: null,
|
||||||
|
currency: 'USD',
|
||||||
|
upgrade_url: 'http://localhost:18130/basket/add/?sku=8CF08E5',
|
||||||
|
sku: '8CF08E5',
|
||||||
|
price: 149,
|
||||||
|
currency_symbol: '$',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.attr(
|
||||||
|
'tabs', ['id', 'host'], (id, host) => [
|
||||||
|
Factory.build(
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
title: 'Course',
|
||||||
|
priority: 0,
|
||||||
|
slug: 'courseware',
|
||||||
|
type: 'courseware',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
courseId: id,
|
||||||
|
host,
|
||||||
|
path: 'course/',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Factory.build(
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
title: 'Discussion',
|
||||||
|
priority: 1,
|
||||||
|
slug: 'discussion',
|
||||||
|
type: 'discussion',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
courseId: id,
|
||||||
|
host,
|
||||||
|
path: 'discussion/forum/',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Factory.build(
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
title: 'Wiki',
|
||||||
|
priority: 2,
|
||||||
|
slug: 'wiki',
|
||||||
|
type: 'wiki',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
courseId: id,
|
||||||
|
host,
|
||||||
|
path: 'course_wiki',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Factory.build(
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
title: 'Progress',
|
||||||
|
priority: 3,
|
||||||
|
slug: 'progress',
|
||||||
|
type: 'progress',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
courseId: id,
|
||||||
|
host,
|
||||||
|
path: 'progress',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Factory.build(
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
title: 'Instructor',
|
||||||
|
priority: 4,
|
||||||
|
slug: 'instructor',
|
||||||
|
type: 'instructor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
courseId: id,
|
||||||
|
host,
|
||||||
|
path: 'instructor',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Factory.build(
|
||||||
|
'tab',
|
||||||
|
{
|
||||||
|
title: 'Dates',
|
||||||
|
priority: 5,
|
||||||
|
slug: 'dates',
|
||||||
|
type: 'dates',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
courseId: id,
|
||||||
|
host,
|
||||||
|
path: 'dates',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ Factory.define('outlineTabData')
|
|||||||
cert_status: null,
|
cert_status: null,
|
||||||
cert_web_view_url: null,
|
cert_web_view_url: null,
|
||||||
certificate_available_date: null,
|
certificate_available_date: null,
|
||||||
download_url: null,
|
|
||||||
},
|
},
|
||||||
course_goals: {
|
course_goals: {
|
||||||
goal_options: [],
|
goal_options: [],
|
||||||
selected_goal: null,
|
selected_goal: null,
|
||||||
|
weekly_learning_goal_enabled: false,
|
||||||
|
days_per_week: null,
|
||||||
|
subscribed_to_reminders: null,
|
||||||
},
|
},
|
||||||
course_tools: [
|
course_tools: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Factory.define('progressTabData')
|
|||||||
percent: 1,
|
percent: 1,
|
||||||
is_passing: true,
|
is_passing: true,
|
||||||
},
|
},
|
||||||
|
credit_course_requirements: null,
|
||||||
section_scores: [
|
section_scores: [
|
||||||
{
|
{
|
||||||
display_name: 'First section',
|
display_name: 'First section',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Factory.define('upgradeNotificationData')
|
|||||||
.option('dateBlocks', [])
|
.option('dateBlocks', [])
|
||||||
.option('offer', null)
|
.option('offer', null)
|
||||||
.option('userTimezone', null)
|
.option('userTimezone', null)
|
||||||
.option('accessExpiration', null)
|
|
||||||
.option('contentTypeGatingEnabled', false)
|
.option('contentTypeGatingEnabled', false)
|
||||||
.attr('courseId', 'course-v1:edX+DemoX+Demo_Course')
|
.attr('courseId', 'course-v1:edX+DemoX+Demo_Course')
|
||||||
.attr('upsellPageName', 'test')
|
.attr('upsellPageName', 'test')
|
||||||
@@ -18,4 +17,9 @@ Factory.define('upgradeNotificationData')
|
|||||||
upgradeUrl: `${host}/dashboard`,
|
upgradeUrl: `${host}/dashboard`,
|
||||||
}))
|
}))
|
||||||
.attr('org', 'edX')
|
.attr('org', 'edX')
|
||||||
|
.attrs({
|
||||||
|
accessExpiration: {
|
||||||
|
expiration_date: '1950-07-13T02:04:49.040006Z',
|
||||||
|
},
|
||||||
|
})
|
||||||
.attr('timeOffsetMillis', 0);
|
.attr('timeOffsetMillis', 0);
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
|
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"courseHome": Object {
|
"courseHome": Object {
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"courseStatus": "loaded",
|
"courseStatus": "loaded",
|
||||||
|
"proctoringPanelStatus": "loading",
|
||||||
"targetUserId": undefined,
|
"targetUserId": undefined,
|
||||||
"toastBodyLink": null,
|
"toastBodyLink": null,
|
||||||
"toastBodyText": null,
|
"toastBodyText": null,
|
||||||
@@ -14,12 +15,14 @@ Object {
|
|||||||
"courseId": null,
|
"courseId": null,
|
||||||
"courseStatus": "loading",
|
"courseStatus": "loading",
|
||||||
"sequenceId": null,
|
"sequenceId": null,
|
||||||
|
"sequenceMightBeUnit": false,
|
||||||
"sequenceStatus": "loading",
|
"sequenceStatus": "loading",
|
||||||
},
|
},
|
||||||
"models": Object {
|
"models": Object {
|
||||||
"courseHomeMeta": Object {
|
"courseHomeMeta": Object {
|
||||||
"course-v1:edX+DemoX+Demo_Course_1": Object {
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
"canLoadCourseware": false,
|
"canViewCertificate": true,
|
||||||
|
"celebrations": null,
|
||||||
"courseAccess": Object {
|
"courseAccess": Object {
|
||||||
"additionalContextUserMessage": null,
|
"additionalContextUserMessage": null,
|
||||||
"developerMessage": null,
|
"developerMessage": null,
|
||||||
@@ -28,7 +31,7 @@ Object {
|
|||||||
"userFragment": null,
|
"userFragment": null,
|
||||||
"userMessage": null,
|
"userMessage": null,
|
||||||
},
|
},
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"isEnrolled": false,
|
"isEnrolled": false,
|
||||||
"isMasquerading": false,
|
"isMasquerading": false,
|
||||||
"isSelfPaced": false,
|
"isSelfPaced": false,
|
||||||
@@ -41,45 +44,49 @@ Object {
|
|||||||
Object {
|
Object {
|
||||||
"slug": "outline",
|
"slug": "outline",
|
||||||
"title": "Course",
|
"title": "Course",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "discussion",
|
"slug": "discussion",
|
||||||
"title": "Discussion",
|
"title": "Discussion",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/discussion/forum/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "wiki",
|
"slug": "wiki",
|
||||||
"title": "Wiki",
|
"title": "Wiki",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course_wiki",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "progress",
|
"slug": "progress",
|
||||||
"title": "Progress",
|
"title": "Progress",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/progress",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "instructor",
|
"slug": "instructor",
|
||||||
"title": "Instructor",
|
"title": "Instructor",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "dates",
|
"slug": "dates",
|
||||||
"title": "Dates",
|
"title": "Dates",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"title": "Demonstration Course",
|
"title": "Demonstration Course",
|
||||||
"userTimezone": "UTC",
|
"userTimezone": "UTC",
|
||||||
|
"username": "MockUser",
|
||||||
"verifiedMode": Object {
|
"verifiedMode": Object {
|
||||||
|
"accessExpirationDate": null,
|
||||||
|
"currency": "USD",
|
||||||
"currencySymbol": "$",
|
"currencySymbol": "$",
|
||||||
"price": 10,
|
"price": 149,
|
||||||
"upgradeUrl": "test",
|
"sku": "8CF08E5",
|
||||||
|
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"dates": Object {
|
"dates": Object {
|
||||||
"course-v1:edX+DemoX+Demo_Course_1": Object {
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
"courseDateBlocks": Array [
|
"courseDateBlocks": Array [
|
||||||
Object {
|
Object {
|
||||||
"date": "2020-05-01T17:59:41Z",
|
"date": "2020-05-01T17:59:41Z",
|
||||||
@@ -293,7 +300,7 @@ Object {
|
|||||||
"verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
"verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||||
},
|
},
|
||||||
"hasEnded": false,
|
"hasEnded": false,
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"learnerIsFullAccess": true,
|
"learnerIsFullAccess": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -301,14 +308,22 @@ Object {
|
|||||||
"recommendations": Object {
|
"recommendations": Object {
|
||||||
"recommendationsStatus": "loading",
|
"recommendationsStatus": "loading",
|
||||||
},
|
},
|
||||||
|
"tours": Object {
|
||||||
|
"showCoursewareTour": false,
|
||||||
|
"showExistingUserCourseHomeTour": false,
|
||||||
|
"showNewUserCourseHomeModal": false,
|
||||||
|
"showNewUserCourseHomeTour": false,
|
||||||
|
"toursEnabled": false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
|
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"courseHome": Object {
|
"courseHome": Object {
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"courseStatus": "loaded",
|
"courseStatus": "loaded",
|
||||||
|
"proctoringPanelStatus": "loading",
|
||||||
"targetUserId": undefined,
|
"targetUserId": undefined,
|
||||||
"toastBodyLink": null,
|
"toastBodyLink": null,
|
||||||
"toastBodyText": null,
|
"toastBodyText": null,
|
||||||
@@ -318,12 +333,14 @@ Object {
|
|||||||
"courseId": null,
|
"courseId": null,
|
||||||
"courseStatus": "loading",
|
"courseStatus": "loading",
|
||||||
"sequenceId": null,
|
"sequenceId": null,
|
||||||
|
"sequenceMightBeUnit": false,
|
||||||
"sequenceStatus": "loading",
|
"sequenceStatus": "loading",
|
||||||
},
|
},
|
||||||
"models": Object {
|
"models": Object {
|
||||||
"courseHomeMeta": Object {
|
"courseHomeMeta": Object {
|
||||||
"course-v1:edX+DemoX+Demo_Course_1": Object {
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
"canLoadCourseware": false,
|
"canViewCertificate": true,
|
||||||
|
"celebrations": null,
|
||||||
"courseAccess": Object {
|
"courseAccess": Object {
|
||||||
"additionalContextUserMessage": null,
|
"additionalContextUserMessage": null,
|
||||||
"developerMessage": null,
|
"developerMessage": null,
|
||||||
@@ -332,7 +349,7 @@ Object {
|
|||||||
"userFragment": null,
|
"userFragment": null,
|
||||||
"userMessage": null,
|
"userMessage": null,
|
||||||
},
|
},
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"isEnrolled": false,
|
"isEnrolled": false,
|
||||||
"isMasquerading": false,
|
"isMasquerading": false,
|
||||||
"isSelfPaced": false,
|
"isSelfPaced": false,
|
||||||
@@ -345,58 +362,61 @@ Object {
|
|||||||
Object {
|
Object {
|
||||||
"slug": "outline",
|
"slug": "outline",
|
||||||
"title": "Course",
|
"title": "Course",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "discussion",
|
"slug": "discussion",
|
||||||
"title": "Discussion",
|
"title": "Discussion",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/discussion/forum/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "wiki",
|
"slug": "wiki",
|
||||||
"title": "Wiki",
|
"title": "Wiki",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course_wiki",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "progress",
|
"slug": "progress",
|
||||||
"title": "Progress",
|
"title": "Progress",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/progress",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "instructor",
|
"slug": "instructor",
|
||||||
"title": "Instructor",
|
"title": "Instructor",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "dates",
|
"slug": "dates",
|
||||||
"title": "Dates",
|
"title": "Dates",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"title": "Demonstration Course",
|
"title": "Demonstration Course",
|
||||||
"userTimezone": "UTC",
|
"userTimezone": "UTC",
|
||||||
|
"username": "MockUser",
|
||||||
"verifiedMode": Object {
|
"verifiedMode": Object {
|
||||||
|
"accessExpirationDate": null,
|
||||||
|
"currency": "USD",
|
||||||
"currencySymbol": "$",
|
"currencySymbol": "$",
|
||||||
"price": 10,
|
"price": 149,
|
||||||
"upgradeUrl": "test",
|
"sku": "8CF08E5",
|
||||||
|
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"outline": Object {
|
"outline": Object {
|
||||||
"course-v1:edX+DemoX+Demo_Course_1": Object {
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
"accessExpiration": null,
|
"accessExpiration": null,
|
||||||
"canShowUpgradeSock": false,
|
"canShowUpgradeSock": false,
|
||||||
"certData": Object {
|
"certData": Object {
|
||||||
"certStatus": null,
|
"certStatus": null,
|
||||||
"certWebViewUrl": null,
|
"certWebViewUrl": null,
|
||||||
"certificateAvailableDate": null,
|
"certificateAvailableDate": null,
|
||||||
"downloadUrl": null,
|
|
||||||
},
|
},
|
||||||
"courseBlocks": Object {
|
"courseBlocks": Object {
|
||||||
"courses": Object {
|
"courses": Object {
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
|
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
|
||||||
"hasScheduledContent": false,
|
"hasScheduledContent": false,
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"sectionIds": Array [
|
"sectionIds": Array [
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||||
],
|
],
|
||||||
@@ -406,7 +426,7 @@ Object {
|
|||||||
"sections": Object {
|
"sections": Object {
|
||||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object {
|
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object {
|
||||||
"complete": false,
|
"complete": false,
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||||
"resumeBlock": false,
|
"resumeBlock": false,
|
||||||
"sequenceIds": Array [
|
"sequenceIds": Array [
|
||||||
@@ -424,7 +444,6 @@ Object {
|
|||||||
"effortTime": 15,
|
"effortTime": 15,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||||
"legacyWebUrl": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1?experience=legacy",
|
|
||||||
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||||
"showLink": true,
|
"showLink": true,
|
||||||
"title": "Title of Sequence",
|
"title": "Title of Sequence",
|
||||||
@@ -432,8 +451,11 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"courseGoals": Object {
|
"courseGoals": Object {
|
||||||
|
"daysPerWeek": null,
|
||||||
"goalOptions": Array [],
|
"goalOptions": Array [],
|
||||||
"selectedGoal": null,
|
"selectedGoal": null,
|
||||||
|
"subscribedToReminders": null,
|
||||||
|
"weeklyLearningGoalEnabled": false,
|
||||||
},
|
},
|
||||||
"courseTools": Array [
|
"courseTools": Array [
|
||||||
Object {
|
Object {
|
||||||
@@ -450,6 +472,7 @@ Object {
|
|||||||
"datesWidget": Object {
|
"datesWidget": Object {
|
||||||
"courseDateBlocks": Array [],
|
"courseDateBlocks": Array [],
|
||||||
},
|
},
|
||||||
|
"enableProctoredExams": undefined,
|
||||||
"enrollAlert": Object {
|
"enrollAlert": Object {
|
||||||
"canEnroll": true,
|
"canEnroll": true,
|
||||||
"extraText": "Contact the administrator.",
|
"extraText": "Contact the administrator.",
|
||||||
@@ -458,7 +481,7 @@ Object {
|
|||||||
"handoutsHtml": "<ul><li>Handout 1</li></ul>",
|
"handoutsHtml": "<ul><li>Handout 1</li></ul>",
|
||||||
"hasEnded": undefined,
|
"hasEnded": undefined,
|
||||||
"hasScheduledContent": null,
|
"hasScheduledContent": null,
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"offer": null,
|
"offer": null,
|
||||||
"resumeCourse": Object {
|
"resumeCourse": Object {
|
||||||
"hasVisitedCourse": false,
|
"hasVisitedCourse": false,
|
||||||
@@ -481,14 +504,22 @@ Object {
|
|||||||
"recommendations": Object {
|
"recommendations": Object {
|
||||||
"recommendationsStatus": "loading",
|
"recommendationsStatus": "loading",
|
||||||
},
|
},
|
||||||
|
"tours": Object {
|
||||||
|
"showCoursewareTour": false,
|
||||||
|
"showExistingUserCourseHomeTour": false,
|
||||||
|
"showNewUserCourseHomeModal": false,
|
||||||
|
"showNewUserCourseHomeTour": false,
|
||||||
|
"toursEnabled": false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
|
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"courseHome": Object {
|
"courseHome": Object {
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"courseStatus": "loaded",
|
"courseStatus": "loaded",
|
||||||
|
"proctoringPanelStatus": "loading",
|
||||||
"targetUserId": undefined,
|
"targetUserId": undefined,
|
||||||
"toastBodyLink": null,
|
"toastBodyLink": null,
|
||||||
"toastBodyText": null,
|
"toastBodyText": null,
|
||||||
@@ -498,12 +529,14 @@ Object {
|
|||||||
"courseId": null,
|
"courseId": null,
|
||||||
"courseStatus": "loading",
|
"courseStatus": "loading",
|
||||||
"sequenceId": null,
|
"sequenceId": null,
|
||||||
|
"sequenceMightBeUnit": false,
|
||||||
"sequenceStatus": "loading",
|
"sequenceStatus": "loading",
|
||||||
},
|
},
|
||||||
"models": Object {
|
"models": Object {
|
||||||
"courseHomeMeta": Object {
|
"courseHomeMeta": Object {
|
||||||
"course-v1:edX+DemoX+Demo_Course_1": Object {
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
"canLoadCourseware": false,
|
"canViewCertificate": true,
|
||||||
|
"celebrations": null,
|
||||||
"courseAccess": Object {
|
"courseAccess": Object {
|
||||||
"additionalContextUserMessage": null,
|
"additionalContextUserMessage": null,
|
||||||
"developerMessage": null,
|
"developerMessage": null,
|
||||||
@@ -512,7 +545,7 @@ Object {
|
|||||||
"userFragment": null,
|
"userFragment": null,
|
||||||
"userMessage": null,
|
"userMessage": null,
|
||||||
},
|
},
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"isEnrolled": false,
|
"isEnrolled": false,
|
||||||
"isMasquerading": false,
|
"isMasquerading": false,
|
||||||
"isSelfPaced": false,
|
"isSelfPaced": false,
|
||||||
@@ -525,45 +558,49 @@ Object {
|
|||||||
Object {
|
Object {
|
||||||
"slug": "outline",
|
"slug": "outline",
|
||||||
"title": "Course",
|
"title": "Course",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "discussion",
|
"slug": "discussion",
|
||||||
"title": "Discussion",
|
"title": "Discussion",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/discussion/forum/",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "wiki",
|
"slug": "wiki",
|
||||||
"title": "Wiki",
|
"title": "Wiki",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/course_wiki",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "progress",
|
"slug": "progress",
|
||||||
"title": "Progress",
|
"title": "Progress",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/progress",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "instructor",
|
"slug": "instructor",
|
||||||
"title": "Instructor",
|
"title": "Instructor",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/instructor",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"slug": "dates",
|
"slug": "dates",
|
||||||
"title": "Dates",
|
"title": "Dates",
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course_1/dates",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"title": "Demonstration Course",
|
"title": "Demonstration Course",
|
||||||
"userTimezone": "UTC",
|
"userTimezone": "UTC",
|
||||||
|
"username": "MockUser",
|
||||||
"verifiedMode": Object {
|
"verifiedMode": Object {
|
||||||
|
"accessExpirationDate": null,
|
||||||
|
"currency": "USD",
|
||||||
"currencySymbol": "$",
|
"currencySymbol": "$",
|
||||||
"price": 10,
|
"price": 149,
|
||||||
"upgradeUrl": "test",
|
"sku": "8CF08E5",
|
||||||
|
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"progress": Object {
|
"progress": Object {
|
||||||
"course-v1:edX+DemoX+Demo_Course_1": Object {
|
"course-v1:edX+DemoX+Demo_Course": Object {
|
||||||
"accessExpiration": null,
|
"accessExpiration": null,
|
||||||
"certificateData": Object {},
|
"certificateData": Object {},
|
||||||
"completionSummary": Object {
|
"completionSummary": Object {
|
||||||
@@ -575,9 +612,9 @@ Object {
|
|||||||
"isPassing": true,
|
"isPassing": true,
|
||||||
"letterGrade": "pass",
|
"letterGrade": "pass",
|
||||||
"percent": 1,
|
"percent": 1,
|
||||||
"visiblePercent": 1,
|
|
||||||
},
|
},
|
||||||
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
|
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||||
|
"creditCourseRequirements": null,
|
||||||
"end": "3027-03-31T00:00:00Z",
|
"end": "3027-03-31T00:00:00Z",
|
||||||
"enrollmentMode": "audit",
|
"enrollmentMode": "audit",
|
||||||
"gradesFeatureIsFullyLocked": false,
|
"gradesFeatureIsFullyLocked": false,
|
||||||
@@ -585,7 +622,7 @@ Object {
|
|||||||
"gradingPolicy": Object {
|
"gradingPolicy": Object {
|
||||||
"assignmentPolicies": Array [
|
"assignmentPolicies": Array [
|
||||||
Object {
|
Object {
|
||||||
"averageGrade": 1,
|
"averageGrade": "1.00",
|
||||||
"numDroppable": 1,
|
"numDroppable": 1,
|
||||||
"shortLabel": "HW",
|
"shortLabel": "HW",
|
||||||
"type": "Homework",
|
"type": "Homework",
|
||||||
@@ -598,7 +635,7 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"hasScheduledContent": false,
|
"hasScheduledContent": false,
|
||||||
"id": "course-v1:edX+DemoX+Demo_Course_1",
|
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||||
"sectionScores": Array [
|
"sectionScores": Array [
|
||||||
Object {
|
Object {
|
||||||
"displayName": "First section",
|
"displayName": "First section",
|
||||||
@@ -669,5 +706,12 @@ Object {
|
|||||||
"recommendations": Object {
|
"recommendations": Object {
|
||||||
"recommendationsStatus": "loading",
|
"recommendationsStatus": "loading",
|
||||||
},
|
},
|
||||||
|
"tours": Object {
|
||||||
|
"showCoursewareTour": false,
|
||||||
|
"showExistingUserCourseHomeTour": false,
|
||||||
|
"showNewUserCourseHomeModal": false,
|
||||||
|
"showNewUserCourseHomeTour": false,
|
||||||
|
"toursEnabled": false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) =
|
|||||||
let averageGrade = 0;
|
let averageGrade = 0;
|
||||||
let weightedGrade = 0;
|
let weightedGrade = 0;
|
||||||
if (points.length) {
|
if (points.length) {
|
||||||
averageGrade = points.reduce((a, b) => a + b, 0) / 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(2);
|
||||||
weightedGrade = averageGrade * assignmentWeight;
|
weightedGrade = averageGrade * assignmentWeight;
|
||||||
}
|
}
|
||||||
return { averageGrade, weightedGrade };
|
return { averageGrade, weightedGrade };
|
||||||
@@ -87,14 +90,21 @@ function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCourseHomeCourseMetadata(metadata) {
|
/**
|
||||||
|
* Tweak the metadata for consistency
|
||||||
|
* @param metadata the data to normalize
|
||||||
|
* @param rootSlug either 'courseware' or 'outline' depending on the context
|
||||||
|
* @returns {Object} The normalized metadata
|
||||||
|
*/
|
||||||
|
function normalizeCourseHomeCourseMetadata(metadata, rootSlug) {
|
||||||
const data = camelCaseObject(metadata);
|
const data = camelCaseObject(metadata);
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
tabs: data.tabs.map(tab => ({
|
tabs: data.tabs.map(tab => ({
|
||||||
// The API uses "courseware" as a slug for both courseware and the outline tab. We switch it to "outline" here for
|
// The API uses "courseware" as a slug for both courseware and the outline tab.
|
||||||
|
// If needed, we switch it to "outline" here for
|
||||||
// use within the MFE to differentiate between course home and courseware.
|
// use within the MFE to differentiate between course home and courseware.
|
||||||
slug: tab.tabId === 'courseware' ? 'outline' : tab.tabId,
|
slug: tab.tabId === 'courseware' ? rootSlug : tab.tabId,
|
||||||
title: tab.title,
|
title: tab.title,
|
||||||
url: tab.url,
|
url: tab.url,
|
||||||
})),
|
})),
|
||||||
@@ -138,12 +148,9 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
|||||||
effortTime: block.effort_time,
|
effortTime: block.effort_time,
|
||||||
icon: block.icon,
|
icon: block.icon,
|
||||||
id: block.id,
|
id: block.id,
|
||||||
legacyWebUrl: block.legacy_web_url,
|
// The presence of a URL for the sequence indicates that we want this sequence to be a clickable
|
||||||
// The presence of an legacy URL for the sequence indicates that we want this
|
// link in the outline (even though we ignore the given url and use an internal <Link> to ourselves).
|
||||||
// sequence to be a clickable link in the outline (even though, if the new
|
showLink: !!block.lms_web_url,
|
||||||
// courseware experience is active, we will ignore `legacyWebUrl` and build a
|
|
||||||
// link to the MFE ourselves).
|
|
||||||
showLink: !!block.legacy_web_url,
|
|
||||||
title: block.display_name,
|
title: block.display_name,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@@ -179,11 +186,11 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
|||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCourseHomeCourseMetadata(courseId) {
|
export async function getCourseHomeCourseMetadata(courseId, rootSlug) {
|
||||||
let url = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
let url = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
||||||
url = appendBrowserTimezoneToUrl(url);
|
url = appendBrowserTimezoneToUrl(url);
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
return normalizeCourseHomeCourseMetadata(data);
|
return normalizeCourseHomeCourseMetadata(data, rootSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For debugging purposes, you might like to see a fully loaded dates tab.
|
// For debugging purposes, you might like to see a fully loaded dates tab.
|
||||||
@@ -198,10 +205,6 @@ export async function getDatesTabData(courseId) {
|
|||||||
return camelCaseObject(data);
|
return camelCaseObject(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { httpErrorStatus } = error && error.customAttributes;
|
const { httpErrorStatus } = error && error.customAttributes;
|
||||||
if (httpErrorStatus === 404) {
|
|
||||||
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/dates`);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (httpErrorStatus === 401) {
|
if (httpErrorStatus === 401) {
|
||||||
// The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
|
// The backend sends this for unenrolled and unauthenticated learners, but we handle those cases by examining
|
||||||
// courseAccess in the metadata call, so just ignore this status for now.
|
// courseAccess in the metadata call, so just ignore this status for now.
|
||||||
@@ -229,16 +232,6 @@ export async function getProgressTabData(courseId, targetUserId) {
|
|||||||
camelCasedData.sectionScores,
|
camelCasedData.sectionScores,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Accumulate the weighted grades by assignment type to calculate the learner facing grade. The grades within
|
|
||||||
// assignmentPolicies have been filtered by what's visible to the learner.
|
|
||||||
camelCasedData.courseGrade.visiblePercent = camelCasedData.gradingPolicy.assignmentPolicies
|
|
||||||
? camelCasedData.gradingPolicy.assignmentPolicies.reduce(
|
|
||||||
(accumulator, assignment) => accumulator + assignment.weightedGrade, 0,
|
|
||||||
) : camelCasedData.courseGrade.percent;
|
|
||||||
|
|
||||||
camelCasedData.courseGrade.isPassing = camelCasedData.courseGrade.visiblePercent
|
|
||||||
>= Math.min(...Object.values(data.grading_policy.grade_range));
|
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -297,6 +290,20 @@ export async function getProctoringInfoData(courseId, username) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getLiveTabIframe(courseId) {
|
||||||
|
const url = `${getConfig().LMS_BASE_URL}/api/course_live/iframe/${courseId}/`;
|
||||||
|
try {
|
||||||
|
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
const { httpErrorStatus } = error && error.customAttributes;
|
||||||
|
if (httpErrorStatus === 404) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getTimeOffsetMillis(headerDate, requestTime, responseTime) {
|
export function getTimeOffsetMillis(headerDate, requestTime, responseTime) {
|
||||||
// Time offset computation should move down into the HttpClient wrapper to maintain a global time correction reference
|
// Time offset computation should move down into the HttpClient wrapper to maintain a global time correction reference
|
||||||
// Requires 'Access-Control-Expose-Headers: Date' on the server response per https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-expose-headers
|
// Requires 'Access-Control-Expose-Headers: Date' on the server response per https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-expose-headers
|
||||||
@@ -314,21 +321,9 @@ export function getTimeOffsetMillis(headerDate, requestTime, responseTime) {
|
|||||||
|
|
||||||
export async function getOutlineTabData(courseId) {
|
export async function getOutlineTabData(courseId) {
|
||||||
const url = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
|
const url = `${getConfig().LMS_BASE_URL}/api/course_home/outline/${courseId}`;
|
||||||
let { tabData } = {};
|
const requestTime = Date.now();
|
||||||
let requestTime = Date.now();
|
const tabData = await getAuthenticatedHttpClient().get(url);
|
||||||
let responseTime = requestTime;
|
const responseTime = Date.now();
|
||||||
try {
|
|
||||||
requestTime = Date.now();
|
|
||||||
tabData = await getAuthenticatedHttpClient().get(url);
|
|
||||||
responseTime = Date.now();
|
|
||||||
} catch (error) {
|
|
||||||
const { httpErrorStatus } = error && error.customAttributes;
|
|
||||||
if (httpErrorStatus === 404) {
|
|
||||||
global.location.replace(`${getConfig().LMS_BASE_URL}/courses/${courseId}/course/`);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -343,6 +338,7 @@ export async function getOutlineTabData(courseId) {
|
|||||||
const courseTools = camelCaseObject(data.course_tools);
|
const courseTools = camelCaseObject(data.course_tools);
|
||||||
const datesBannerInfo = camelCaseObject(data.dates_banner_info);
|
const datesBannerInfo = camelCaseObject(data.dates_banner_info);
|
||||||
const datesWidget = camelCaseObject(data.dates_widget);
|
const datesWidget = camelCaseObject(data.dates_widget);
|
||||||
|
const enableProctoredExams = data.enable_proctored_exams;
|
||||||
const enrollAlert = camelCaseObject(data.enroll_alert);
|
const enrollAlert = camelCaseObject(data.enroll_alert);
|
||||||
const enrollmentMode = data.enrollment_mode;
|
const enrollmentMode = data.enrollment_mode;
|
||||||
const handoutsHtml = data.handouts_html;
|
const handoutsHtml = data.handouts_html;
|
||||||
@@ -366,6 +362,7 @@ export async function getOutlineTabData(courseId) {
|
|||||||
datesWidget,
|
datesWidget,
|
||||||
enrollAlert,
|
enrollAlert,
|
||||||
enrollmentMode,
|
enrollmentMode,
|
||||||
|
enableProctoredExams,
|
||||||
handoutsHtml,
|
handoutsHtml,
|
||||||
hasScheduledContent,
|
hasScheduledContent,
|
||||||
hasEnded,
|
hasEnded,
|
||||||
@@ -386,11 +383,20 @@ export async function postCourseDeadlines(courseId, model) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postCourseGoals(courseId, goalKey) {
|
export async function deprecatedPostCourseGoals(courseId, goalKey) {
|
||||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`);
|
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`);
|
||||||
return getAuthenticatedHttpClient().post(url.href, { course_id: courseId, goal_key: goalKey });
|
return getAuthenticatedHttpClient().post(url.href, { course_id: courseId, goal_key: goalKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postWeeklyLearningGoal(courseId, daysPerWeek, subscribedToReminders) {
|
||||||
|
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`);
|
||||||
|
return getAuthenticatedHttpClient().post(url.href, {
|
||||||
|
course_id: courseId,
|
||||||
|
days_per_week: daysPerWeek,
|
||||||
|
subscribed_to_reminders: subscribedToReminders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function postDismissWelcomeMessage(courseId) {
|
export async function postDismissWelcomeMessage(courseId) {
|
||||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`);
|
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`);
|
||||||
await getAuthenticatedHttpClient().post(url.href, { course_id: courseId });
|
await getAuthenticatedHttpClient().post(url.href, { course_id: courseId });
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ export {
|
|||||||
fetchOutlineTab,
|
fetchOutlineTab,
|
||||||
fetchProgressTab,
|
fetchProgressTab,
|
||||||
resetDeadlines,
|
resetDeadlines,
|
||||||
saveCourseGoal,
|
deprecatedSaveCourseGoal,
|
||||||
|
saveWeeklyLearningGoal,
|
||||||
} from './thunks';
|
} from './thunks';
|
||||||
|
|
||||||
export { reducer } from './slice';
|
export { reducer } from './slice';
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ describe('Course Home Service', () => {
|
|||||||
sku: '8CF08E5',
|
sku: '8CF08E5',
|
||||||
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||||
}),
|
}),
|
||||||
can_load_courseware: boolean(true),
|
|
||||||
celebrations: like({
|
celebrations: like({
|
||||||
first_section: false,
|
first_section: false,
|
||||||
streak_length_to_celebrate: null,
|
streak_length_to_celebrate: null,
|
||||||
@@ -106,7 +105,6 @@ describe('Course Home Service', () => {
|
|||||||
sku: '8CF08E5',
|
sku: '8CF08E5',
|
||||||
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||||
},
|
},
|
||||||
canLoadCourseware: true,
|
|
||||||
celebrations: {
|
celebrations: {
|
||||||
firstSection: false,
|
firstSection: false,
|
||||||
streakLengthToCelebrate: null,
|
streakLengthToCelebrate: null,
|
||||||
@@ -139,7 +137,7 @@ describe('Course Home Service', () => {
|
|||||||
title: 'Demonstration Course',
|
title: 'Demonstration Course',
|
||||||
username: 'edx',
|
username: 'edx',
|
||||||
};
|
};
|
||||||
const response = await getCourseHomeCourseMetadata(courseId);
|
const response = await getCourseHomeCourseMetadata(courseId, 'outline');
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
expect(response).toEqual(normalizedTabData);
|
expect(response).toEqual(normalizedTabData);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ describe('Data layer integration tests', () => {
|
|||||||
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
const goalUrl = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
||||||
axiosMock.onPost(goalUrl).reply(200, {});
|
axiosMock.onPost(goalUrl).reply(200, {});
|
||||||
|
|
||||||
await thunks.saveCourseGoal(courseId, 'unsure');
|
await thunks.deprecatedSaveCourseGoal(courseId, 'unsure');
|
||||||
|
|
||||||
expect(axiosMock.history.post[0].url).toEqual(goalUrl);
|
expect(axiosMock.history.post[0].url).toEqual(goalUrl);
|
||||||
expect(axiosMock.history.post[0].data).toEqual(`{"course_id":"${courseId}","goal_key":"unsure"}`);
|
expect(axiosMock.history.post[0].data).toEqual(`{"course_id":"${courseId}","goal_key":"unsure"}`);
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ const slice = createSlice({
|
|||||||
initialState: {
|
initialState: {
|
||||||
courseStatus: 'loading',
|
courseStatus: 'loading',
|
||||||
courseId: null,
|
courseId: null,
|
||||||
|
proctoringPanelStatus: 'loading',
|
||||||
toastBodyText: null,
|
toastBodyText: null,
|
||||||
toastBodyLink: null,
|
toastBodyLink: null,
|
||||||
toastHeader: '',
|
toastHeader: '',
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
|
fetchProctoringInfoResolved: (state) => {
|
||||||
|
state.proctoringPanelStatus = LOADED;
|
||||||
|
},
|
||||||
fetchTabDenied: (state, { payload }) => {
|
fetchTabDenied: (state, { payload }) => {
|
||||||
state.courseId = payload.courseId;
|
state.courseId = payload.courseId;
|
||||||
state.courseStatus = DENIED;
|
state.courseStatus = DENIED;
|
||||||
@@ -47,6 +51,7 @@ const slice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
fetchProctoringInfoResolved,
|
||||||
fetchTabDenied,
|
fetchTabDenied,
|
||||||
fetchTabFailure,
|
fetchTabFailure,
|
||||||
fetchTabRequest,
|
fetchTabRequest,
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import {
|
|||||||
getOutlineTabData,
|
getOutlineTabData,
|
||||||
getProgressTabData,
|
getProgressTabData,
|
||||||
postCourseDeadlines,
|
postCourseDeadlines,
|
||||||
postCourseGoals,
|
deprecatedPostCourseGoals,
|
||||||
|
postWeeklyLearningGoal,
|
||||||
postDismissWelcomeMessage,
|
postDismissWelcomeMessage,
|
||||||
postRequestCert,
|
postRequestCert,
|
||||||
|
getLiveTabIframe,
|
||||||
} from './api';
|
} from './api';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -31,46 +33,38 @@ const eventTypes = {
|
|||||||
export function fetchTab(courseId, tab, getTabData, targetUserId) {
|
export function fetchTab(courseId, tab, getTabData, targetUserId) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
dispatch(fetchTabRequest({ courseId }));
|
dispatch(fetchTabRequest({ courseId }));
|
||||||
Promise.allSettled([
|
try {
|
||||||
getCourseHomeCourseMetadata(courseId),
|
const courseHomeCourseMetadata = await getCourseHomeCourseMetadata(courseId, 'outline');
|
||||||
getTabData(courseId, targetUserId),
|
dispatch(addModel({
|
||||||
]).then(([courseHomeCourseMetadataResult, tabDataResult]) => {
|
modelType: 'courseHomeMeta',
|
||||||
const fetchedCourseHomeCourseMetadata = courseHomeCourseMetadataResult.status === 'fulfilled';
|
model: {
|
||||||
const fetchedTabData = tabDataResult.status === 'fulfilled';
|
id: courseId,
|
||||||
|
...courseHomeCourseMetadata,
|
||||||
if (fetchedCourseHomeCourseMetadata) {
|
},
|
||||||
dispatch(addModel({
|
}));
|
||||||
modelType: 'courseHomeMeta',
|
const tabDataResult = getTabData && await getTabData(courseId, targetUserId);
|
||||||
model: {
|
if (tabDataResult) {
|
||||||
id: courseId,
|
|
||||||
...courseHomeCourseMetadataResult.value,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
logError(courseHomeCourseMetadataResult.reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fetchedTabData) {
|
|
||||||
dispatch(addModel({
|
dispatch(addModel({
|
||||||
modelType: tab,
|
modelType: tab,
|
||||||
model: {
|
model: {
|
||||||
id: courseId,
|
id: courseId,
|
||||||
...tabDataResult.value,
|
...tabDataResult,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else {
|
|
||||||
logError(tabDataResult.reason);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable the access-denied path for now - it caused a regression
|
// Disable the access-denied path for now - it caused a regression
|
||||||
if (fetchedCourseHomeCourseMetadata && !courseHomeCourseMetadataResult.value.courseAccess.hasAccess) {
|
if (!courseHomeCourseMetadata.courseAccess.hasAccess) {
|
||||||
dispatch(fetchTabDenied({ courseId }));
|
dispatch(fetchTabDenied({ courseId }));
|
||||||
} else if (fetchedCourseHomeCourseMetadata && fetchedTabData) {
|
} else if (tabDataResult || !getTabData) {
|
||||||
dispatch(fetchTabSuccess({ courseId, targetUserId }));
|
dispatch(fetchTabSuccess({
|
||||||
} else {
|
courseId,
|
||||||
dispatch(fetchTabFailure({ courseId }));
|
targetUserId,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
});
|
} catch (e) {
|
||||||
|
dispatch(fetchTabFailure({ courseId }));
|
||||||
|
logError(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +80,14 @@ export function fetchOutlineTab(courseId) {
|
|||||||
return fetchTab(courseId, 'outline', getOutlineTabData);
|
return fetchTab(courseId, 'outline', getOutlineTabData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchLiveTab(courseId) {
|
||||||
|
return fetchTab(courseId, 'live', getLiveTabIframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchDiscussionTab(courseId) {
|
||||||
|
return fetchTab(courseId, 'discussion');
|
||||||
|
}
|
||||||
|
|
||||||
export function dismissWelcomeMessage(courseId) {
|
export function dismissWelcomeMessage(courseId) {
|
||||||
return async () => postDismissWelcomeMessage(courseId);
|
return async () => postDismissWelcomeMessage(courseId);
|
||||||
}
|
}
|
||||||
@@ -109,8 +111,12 @@ export function resetDeadlines(courseId, model, getTabData) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveCourseGoal(courseId, goalKey) {
|
export async function deprecatedSaveCourseGoal(courseId, goalKey) {
|
||||||
return postCourseGoals(courseId, goalKey);
|
return deprecatedPostCourseGoals(courseId, goalKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveWeeklyLearningGoal(courseId, daysPerWeek, subscribedToReminders) {
|
||||||
|
return postWeeklyLearningGoal(courseId, daysPerWeek, subscribedToReminders);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processEvent(eventData, getTabData) {
|
export function processEvent(eventData, getTabData) {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe('DatesTab', () => {
|
|||||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||||
|
|
||||||
function setMetadata(attributes, options) {
|
function setMetadata(attributes, options) {
|
||||||
courseMetadata = Factory.build('courseHomeMetadata', { id: courseId, ...attributes }, options);
|
courseMetadata = Factory.build('courseHomeMetadata', attributes, options);
|
||||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ describe('DatesTab', () => {
|
|||||||
userEvent.hover(tipIcon);
|
userEvent.hover(tipIcon);
|
||||||
const tooltip = screen.getByText(tipText); // now it's there
|
const tooltip = screen.getByText(tipText); // now it's there
|
||||||
userEvent.unhover(tipIcon);
|
userEvent.unhover(tipIcon);
|
||||||
waitForElementToBeRemoved(tooltip); // and it's gone again
|
await waitForElementToBeRemoved(tooltip); // and it's gone again
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -341,12 +341,12 @@ describe('DatesTab', () => {
|
|||||||
|
|
||||||
it('redirects to the home page when unauthenticated', async () => {
|
it('redirects to the home page when unauthenticated', async () => {
|
||||||
await renderDenied('authentication_required');
|
await renderDenied('authentication_required');
|
||||||
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
|
expect(global.location.href).toEqual(`http://localhost/course/${courseMetadata.id}/home`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redirects to the home page when unenrolled', async () => {
|
it('redirects to the home page when unenrolled', async () => {
|
||||||
await renderDenied('enrollment_required');
|
await renderDenied('enrollment_required');
|
||||||
expect(global.location.href).toEqual(`http://localhost/redirect/course-home/${courseMetadata.id}`);
|
expect(global.location.href).toEqual(`http://localhost/course/${courseMetadata.id}/home`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,30 +4,37 @@ const messages = defineMessages({
|
|||||||
completed: {
|
completed: {
|
||||||
id: 'learning.dates.badge.completed',
|
id: 'learning.dates.badge.completed',
|
||||||
defaultMessage: 'Completed',
|
defaultMessage: 'Completed',
|
||||||
|
description: 'shown as label for the assignments which learner has completed.',
|
||||||
},
|
},
|
||||||
dueNext: {
|
dueNext: {
|
||||||
id: 'learning.dates.badge.dueNext',
|
id: 'learning.dates.badge.dueNext',
|
||||||
defaultMessage: 'Due next',
|
defaultMessage: 'Due next',
|
||||||
|
description: 'Shown as label for the assignment which date is in the future',
|
||||||
},
|
},
|
||||||
pastDue: {
|
pastDue: {
|
||||||
id: 'learning.dates.badge.pastDue',
|
id: 'learning.dates.badge.pastDue',
|
||||||
defaultMessage: 'Past due',
|
defaultMessage: 'Past due',
|
||||||
|
description: 'Shown as label for the assignments which deadline has passed',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
id: 'learning.dates.title',
|
id: 'learning.dates.title',
|
||||||
defaultMessage: 'Important dates',
|
defaultMessage: 'Important dates',
|
||||||
|
description: 'The title of dates tab (course timeline).',
|
||||||
},
|
},
|
||||||
today: {
|
today: {
|
||||||
id: 'learning.dates.badge.today',
|
id: 'learning.dates.badge.today',
|
||||||
defaultMessage: 'Today',
|
defaultMessage: 'Today',
|
||||||
|
description: 'Label used when the scheduled date for the assignment matches the current day',
|
||||||
},
|
},
|
||||||
unreleased: {
|
unreleased: {
|
||||||
id: 'learning.dates.badge.unreleased',
|
id: 'learning.dates.badge.unreleased',
|
||||||
defaultMessage: 'Not yet released',
|
defaultMessage: 'Not yet released',
|
||||||
|
description: 'Shown as label for assignments which date is unknown yet',
|
||||||
},
|
},
|
||||||
verifiedOnly: {
|
verifiedOnly: {
|
||||||
id: 'learning.dates.badge.verifiedOnly',
|
id: 'learning.dates.badge.verifiedOnly',
|
||||||
defaultMessage: 'Verified only',
|
defaultMessage: 'Verified only',
|
||||||
|
description: 'Shown as label for assignments which learner has no access to.',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
36
src/course-home/discussion-tab/DiscussionTab.jsx
Normal file
36
src/course-home/discussion-tab/DiscussionTab.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { generatePath, useHistory } from 'react-router';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useIFrameHeight, useIFramePluginEvents } from '../../generic/hooks';
|
||||||
|
|
||||||
|
function DiscussionTab() {
|
||||||
|
const { courseId } = useSelector(state => state.courseHome);
|
||||||
|
const { path } = useParams();
|
||||||
|
const [originalPath] = useState(path);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [, iFrameHeight] = useIFrameHeight();
|
||||||
|
useIFramePluginEvents({
|
||||||
|
'discussions.navigate': (payload) => {
|
||||||
|
const basePath = generatePath('/course/:courseId/discussion', { courseId });
|
||||||
|
history.push(`${basePath}/${payload.path}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const discussionsUrl = `${getConfig().DISCUSSIONS_MFE_BASE_URL}/${courseId}/${originalPath}`;
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={discussionsUrl}
|
||||||
|
className="d-flex w-100 border-0"
|
||||||
|
height={iFrameHeight}
|
||||||
|
style={{ minHeight: '60rem' }}
|
||||||
|
title="discussion"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscussionTab.propTypes = {};
|
||||||
|
|
||||||
|
export default injectIntl(DiscussionTab);
|
||||||
61
src/course-home/discussion-tab/DiscussionTab.test.jsx
Normal file
61
src/course-home/discussion-tab/DiscussionTab.test.jsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { getConfig, history } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import React from 'react';
|
||||||
|
import { Route } from 'react-router';
|
||||||
|
import { Factory } from 'rosie';
|
||||||
|
import { UserMessagesProvider } from '../../generic/user-messages';
|
||||||
|
import {
|
||||||
|
initializeMockApp, messageEvent, screen, waitFor,
|
||||||
|
} from '../../setupTest';
|
||||||
|
import initializeStore from '../../store';
|
||||||
|
import { TabContainer } from '../../tab-page';
|
||||||
|
import { appendBrowserTimezoneToUrl } from '../../utils';
|
||||||
|
import { fetchDiscussionTab } from '../data/thunks';
|
||||||
|
import DiscussionTab from './DiscussionTab';
|
||||||
|
|
||||||
|
initializeMockApp();
|
||||||
|
jest.mock('@edx/frontend-platform/analytics');
|
||||||
|
|
||||||
|
describe('DiscussionTab', () => {
|
||||||
|
let axiosMock;
|
||||||
|
let store;
|
||||||
|
let component;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
store = initializeStore();
|
||||||
|
component = (
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<UserMessagesProvider>
|
||||||
|
<Route path="/course/:courseId/discussion">
|
||||||
|
<TabContainer tab="discussion" fetch={fetchDiscussionTab} slice="courseHome">
|
||||||
|
<DiscussionTab />
|
||||||
|
</TabContainer>
|
||||||
|
</Route>
|
||||||
|
</UserMessagesProvider>
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const courseMetadata = Factory.build('courseHomeMetadata', { user_timezone: 'America/New_York' });
|
||||||
|
const { id: courseId } = courseMetadata;
|
||||||
|
|
||||||
|
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
||||||
|
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||||
|
history.push(`/course/${courseId}/discussion`); // so tab can pull course id from url
|
||||||
|
|
||||||
|
render(component);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resizes when it gets a size hint from iframe', async () => {
|
||||||
|
window.postMessage({ ...messageEvent, payload: { height: 1234 } }, '*');
|
||||||
|
await waitFor(() => expect(screen.getByTitle('discussion'))
|
||||||
|
.toHaveAttribute('height', String(1234)));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
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 { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { Header } from '../../course-header';
|
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||||
import PageLoading from '../../generic/PageLoading';
|
import PageLoading from '../../generic/PageLoading';
|
||||||
import { unsubscribeFromCourseGoal } from '../data/api';
|
import { unsubscribeFromCourseGoal } from '../data/api';
|
||||||
|
|
||||||
@@ -28,6 +29,10 @@ function GoalUnsubscribe({ intl }) {
|
|||||||
setError(true);
|
setError(true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// We unfortunately have no information about the user, course, org, or really anything
|
||||||
|
// as visiting this page is allowed to be done anonymously and without the context of the course.
|
||||||
|
// The token can be used to connect a user and course, it will just require some post-processing
|
||||||
|
sendTrackEvent('edx.ui.lms.goal.unsubscribe', { token });
|
||||||
}, []); // deps=[] to only run once
|
}, []); // deps=[] to only run once
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ function ResultPage({ courseTitle, error, intl }) {
|
|||||||
<>
|
<>
|
||||||
<UnsubscribeIcon className="text-primary" alt="" />
|
<UnsubscribeIcon className="text-primary" alt="" />
|
||||||
<div role="heading" aria-level="1" className="h2">{header}</div>
|
<div role="heading" aria-level="1" className="h2">{header}</div>
|
||||||
<div>{description}</div>
|
<div className="row justify-content-center">
|
||||||
|
<div className="col-xl-7 col-12 p-0">{description}</div>
|
||||||
|
</div>
|
||||||
<Button variant="brand" href={`${getConfig().LMS_BASE_URL}/dashboard`} className="mt-4">
|
<Button variant="brand" href={`${getConfig().LMS_BASE_URL}/dashboard`} className="mt-4">
|
||||||
{intl.formatMessage(messages.goToDashboard)}
|
{intl.formatMessage(messages.goToDashboard)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -4,26 +4,32 @@ const messages = defineMessages({
|
|||||||
contactSupport: {
|
contactSupport: {
|
||||||
id: 'learning.goals.unsubscribe.contact',
|
id: 'learning.goals.unsubscribe.contact',
|
||||||
defaultMessage: 'contact support',
|
defaultMessage: 'contact support',
|
||||||
|
description: 'Its shown as a suggestion or recommendation for learner when their unsubscribing request has failed',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
id: 'learning.goals.unsubscribe.description',
|
id: 'learning.goals.unsubscribe.description',
|
||||||
defaultMessage: 'You will no longer receive email reminders about your goal for {courseTitle}.',
|
defaultMessage: 'You will no longer receive email reminders about your goal for {courseTitle}.',
|
||||||
|
description: 'It describes the consequences to learner when they unsubscribe of goal reminder service',
|
||||||
},
|
},
|
||||||
errorHeader: {
|
errorHeader: {
|
||||||
id: 'learning.goals.unsubscribe.errorHeader',
|
id: 'learning.goals.unsubscribe.errorHeader',
|
||||||
defaultMessage: 'Something went wrong',
|
defaultMessage: 'Something went wrong',
|
||||||
|
description: 'It indicate that the unsubscribing request has failed',
|
||||||
},
|
},
|
||||||
goToDashboard: {
|
goToDashboard: {
|
||||||
id: 'learning.goals.unsubscribe.goToDashboard',
|
id: 'learning.goals.unsubscribe.goToDashboard',
|
||||||
defaultMessage: 'Go to dashboard',
|
defaultMessage: 'Go to dashboard',
|
||||||
|
description: 'Anchor text for button that redirects to dashboard page',
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
id: 'learning.goals.unsubscribe.header',
|
id: 'learning.goals.unsubscribe.header',
|
||||||
defaultMessage: 'You’ve unsubscribed from goal reminders',
|
defaultMessage: 'You’ve unsubscribed from goal reminders',
|
||||||
|
description: 'It indicate that the unsubscribing request was successful',
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
id: 'learning.goals.unsubscribe.loading',
|
id: 'learning.goals.unsubscribe.loading',
|
||||||
defaultMessage: 'Unsubscribing…',
|
defaultMessage: 'Unsubscribing…',
|
||||||
|
description: 'Message shown when the unsubscribing request is processing',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
22
src/course-home/live-tab/LiveTab.jsx
Normal file
22
src/course-home/live-tab/LiveTab.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
function LiveTab() {
|
||||||
|
const { courseId } = useSelector(state => state.courseHome);
|
||||||
|
const liveModel = useSelector(state => state.models.live);
|
||||||
|
useEffect(() => {
|
||||||
|
const iframe = document.getElementById('lti-tab-embed');
|
||||||
|
if (iframe) {
|
||||||
|
iframe.className += ' vh-100 w-100 border-0';
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="live_tab"
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{ __html: liveModel[courseId]?.iframe }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LiveTab;
|
||||||
@@ -40,7 +40,7 @@ export default function DateSummary({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="container p-0 mb-3 small text-dark-500">
|
<li className="p-0 mb-3 small text-dark-500">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<FontAwesomeIcon icon={faCalendarAlt} className="ml-3 mt-1 mr-1" fixedWidth />
|
<FontAwesomeIcon icon={faCalendarAlt} className="ml-3 mt-1 mr-1" fixedWidth />
|
||||||
<div className="ml-1 font-weight-bold">
|
<div className="ml-1 font-weight-bold">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
@@ -13,22 +13,40 @@ export default function LmsHtmlFragment({
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
|
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
|
||||||
<link rel="stylesheet" href="/static/css/bootstrap/lms-main.css">
|
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">
|
||||||
<link rel="stylesheet" type="text/css" href="${getConfig().BASE_URL}/src/course-home/outline-tab/LmsHtmlFragment.css">
|
<link rel="stylesheet" type="text/css" href="${getConfig().BASE_URL}/src/course-home/outline-tab/LmsHtmlFragment.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="${className}">${html}</body>
|
<body class="${className}">${html}</body>
|
||||||
|
<script>
|
||||||
|
const resizer = new ResizeObserver(() => {
|
||||||
|
window.parent.postMessage({type: 'lmshtmlfragment.resize'}, '*');
|
||||||
|
});
|
||||||
|
resizer.observe(document.body);
|
||||||
|
</script>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const iframe = useRef(null);
|
const iframe = useRef(null);
|
||||||
function handleLoad() {
|
function resetIframeHeight() {
|
||||||
iframe.current.height = iframe.current.contentWindow.document.body.scrollHeight;
|
if (iframe?.current?.contentWindow?.document?.body) {
|
||||||
|
iframe.current.height = iframe.current.contentWindow.document.body.scrollHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function receiveMessage(event) {
|
||||||
|
const { type } = event.data;
|
||||||
|
if (type === 'lmshtmlfragment.resize') {
|
||||||
|
resetIframeHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global.addEventListener('message', receiveMessage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
className="w-100 border-0"
|
className="w-100 border-0"
|
||||||
onLoad={handleLoad}
|
onLoad={resetIframeHeight}
|
||||||
ref={iframe}
|
ref={iframe}
|
||||||
referrerPolicy="origin"
|
referrerPolicy="origin"
|
||||||
scrolling="no"
|
scrolling="no"
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { sendTrackEvent, sendTrackingLogEvent } 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 { history } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Button } from '@edx/paragon';
|
||||||
import { Button, Toast } from '@edx/paragon';
|
|
||||||
import { AlertList } from '../../generic/user-messages';
|
import { AlertList } from '../../generic/user-messages';
|
||||||
|
|
||||||
import CourseDates from './widgets/CourseDates';
|
import CourseDates from './widgets/CourseDates';
|
||||||
import CourseGoalCard from './widgets/CourseGoalCard';
|
|
||||||
import CourseHandouts from './widgets/CourseHandouts';
|
import CourseHandouts from './widgets/CourseHandouts';
|
||||||
|
import StartOrResumeCourseCard from './widgets/StartOrResumeCourseCard';
|
||||||
|
import WeeklyLearningGoalCard from './widgets/WeeklyLearningGoalCard';
|
||||||
import CourseTools from './widgets/CourseTools';
|
import CourseTools from './widgets/CourseTools';
|
||||||
import { fetchOutlineTab } from '../data';
|
import { fetchOutlineTab } from '../data';
|
||||||
import genericMessages from '../../generic/messages';
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import Section from './Section';
|
import Section from './Section';
|
||||||
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||||
import UpdateGoalSelector from './widgets/UpdateGoalSelector';
|
|
||||||
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
|
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';
|
||||||
@@ -35,13 +35,13 @@ import { initHomeMMP2P, MMP2PFlyover } from '../../experiments/mm-p2p';
|
|||||||
function OutlineTab({ intl }) {
|
function OutlineTab({ intl }) {
|
||||||
const {
|
const {
|
||||||
courseId,
|
courseId,
|
||||||
|
proctoringPanelStatus,
|
||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isSelfPaced,
|
isSelfPaced,
|
||||||
org,
|
org,
|
||||||
title,
|
title,
|
||||||
username,
|
|
||||||
userTimezone,
|
userTimezone,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
@@ -52,24 +52,23 @@ function OutlineTab({ intl }) {
|
|||||||
sections,
|
sections,
|
||||||
},
|
},
|
||||||
courseGoals: {
|
courseGoals: {
|
||||||
goalOptions,
|
|
||||||
selectedGoal,
|
selectedGoal,
|
||||||
|
weeklyLearningGoalEnabled,
|
||||||
} = {},
|
} = {},
|
||||||
datesBannerInfo,
|
datesBannerInfo,
|
||||||
datesWidget: {
|
datesWidget: {
|
||||||
courseDateBlocks,
|
courseDateBlocks,
|
||||||
},
|
},
|
||||||
resumeCourse: {
|
enableProctoredExams,
|
||||||
hasVisitedCourse,
|
|
||||||
url: resumeCourseUrl,
|
|
||||||
},
|
|
||||||
offer,
|
offer,
|
||||||
timeOffsetMillis,
|
timeOffsetMillis,
|
||||||
verifiedMode,
|
verifiedMode,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
const [courseGoalToDisplay, setCourseGoalToDisplay] = useState(selectedGoal);
|
const {
|
||||||
const [goalToastHeader, setGoalToastHeader] = useState('');
|
marketingUrl,
|
||||||
|
} = useModel('coursewareMeta', courseId);
|
||||||
|
|
||||||
const [expandAll, setExpandAll] = useState(false);
|
const [expandAll, setExpandAll] = useState(false);
|
||||||
|
|
||||||
const eventProperties = {
|
const eventProperties = {
|
||||||
@@ -77,14 +76,6 @@ function OutlineTab({ intl }) {
|
|||||||
courserun_key: courseId,
|
courserun_key: courseId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const logResumeCourseClick = () => {
|
|
||||||
sendTrackingLogEvent('edx.course.home.resume_course.clicked', {
|
|
||||||
...eventProperties,
|
|
||||||
event_type: hasVisitedCourse ? 'resume' : 'start',
|
|
||||||
url: resumeCourseUrl,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Below the course title alerts (appearing in the order listed here)
|
// Below the course title alerts (appearing in the order listed here)
|
||||||
const courseStartAlert = useCourseStartAlert(courseId);
|
const courseStartAlert = useCourseStartAlert(courseId);
|
||||||
const courseEndAlert = useCourseEndAlert(courseId);
|
const courseEndAlert = useCourseEndAlert(courseId);
|
||||||
@@ -119,26 +110,29 @@ function OutlineTab({ intl }) {
|
|||||||
/** show post enrolment survey to only B2C learners */
|
/** show post enrolment survey to only B2C learners */
|
||||||
const learnerType = isEnterpriseUser() ? 'enterprise_learner' : 'b2c_learner';
|
const learnerType = isEnterpriseUser() ? 'enterprise_learner' : 'b2c_learner';
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentParams = new URLSearchParams(location.search);
|
||||||
|
const startCourse = currentParams.get('start_course');
|
||||||
|
if (startCourse === '1') {
|
||||||
|
sendTrackEvent('enrollment.email.clicked.startcourse', {});
|
||||||
|
|
||||||
|
// Deleting the course_start query param as it only needs to be set once
|
||||||
|
// whenever passed in query params.
|
||||||
|
currentParams.delete('start_course');
|
||||||
|
history.replace({
|
||||||
|
search: currentParams.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toast
|
|
||||||
closeLabel={intl.formatMessage(genericMessages.close)}
|
|
||||||
onClose={() => setGoalToastHeader('')}
|
|
||||||
show={!!(goalToastHeader)}
|
|
||||||
>
|
|
||||||
{goalToastHeader}
|
|
||||||
</Toast>
|
|
||||||
<div data-learner-type={learnerType} className="row w-100 mx-0 my-3 justify-content-between">
|
<div data-learner-type={learnerType} className="row w-100 mx-0 my-3 justify-content-between">
|
||||||
<div className="col-12 col-sm-auto p-0">
|
<div className="col-12 col-sm-auto p-0">
|
||||||
<div role="heading" aria-level="1" className="h2">{title}</div>
|
<div role="heading" aria-level="1" className="h2">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
{resumeCourseUrl && (
|
|
||||||
<div className="col-12 col-sm-auto p-0">
|
|
||||||
<Button variant="brand" block href={resumeCourseUrl} onClick={() => logResumeCourseClick()}>
|
|
||||||
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/** [MM-P2P] Experiment (className for optimizely trigger) */}
|
{/** [MM-P2P] Experiment (className for optimizely trigger) */}
|
||||||
<div className="row course-outline-tab">
|
<div className="row course-outline-tab">
|
||||||
@@ -172,26 +166,18 @@ function OutlineTab({ intl }) {
|
|||||||
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
|
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!courseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
|
<StartOrResumeCourseCard />
|
||||||
<CourseGoalCard
|
|
||||||
courseId={courseId}
|
|
||||||
goalOptions={goalOptions}
|
|
||||||
title={title}
|
|
||||||
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }}
|
|
||||||
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<WelcomeMessage courseId={courseId} />
|
<WelcomeMessage courseId={courseId} />
|
||||||
{rootCourseId && (
|
{rootCourseId && (
|
||||||
<>
|
<>
|
||||||
<div className="row w-100 m-0 mb-3 justify-content-end">
|
<div className="row w-100 m-0 mb-3 justify-content-end">
|
||||||
<div className="col-12 col-sm-auto p-0">
|
<div className="col-12 col-md-auto p-0">
|
||||||
<Button variant="outline-primary" block onClick={() => { setExpandAll(!expandAll); }}>
|
<Button 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 className="list-unstyled">
|
<ol id="courseHome-outline" className="list-unstyled">
|
||||||
{courses[rootCourseId].sectionIds.map((sectionId) => (
|
{courses[rootCourseId].sectionIds.map((sectionId) => (
|
||||||
<Section
|
<Section
|
||||||
key={sectionId}
|
key={sectionId}
|
||||||
@@ -207,22 +193,16 @@ function OutlineTab({ intl }) {
|
|||||||
</div>
|
</div>
|
||||||
{rootCourseId && (
|
{rootCourseId && (
|
||||||
<div className="col col-12 col-md-4">
|
<div className="col col-12 col-md-4">
|
||||||
<ProctoringInfoPanel
|
<ProctoringInfoPanel />
|
||||||
courseId={courseId}
|
{ /** Defer showing the goal widget until the ProctoringInfoPanel has resolved or has been determined as
|
||||||
username={username}
|
disabled to avoid components bouncing around too much as screen is rendered */ }
|
||||||
/>
|
{(!enableProctoredExams || proctoringPanelStatus === 'loaded') && weeklyLearningGoalEnabled && (
|
||||||
{courseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
|
<WeeklyLearningGoalCard
|
||||||
<UpdateGoalSelector
|
daysPerWeek={selectedGoal && 'daysPerWeek' in selectedGoal ? selectedGoal.daysPerWeek : null}
|
||||||
courseId={courseId}
|
subscribedToReminders={selectedGoal && 'subscribedToReminders' in selectedGoal ? selectedGoal.subscribedToReminders : false}
|
||||||
goalOptions={goalOptions}
|
|
||||||
selectedGoal={courseGoalToDisplay}
|
|
||||||
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }}
|
|
||||||
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CourseTools
|
<CourseTools />
|
||||||
courseId={courseId}
|
|
||||||
/>
|
|
||||||
{ /** [MM-P2P] Experiment (conditional) */ }
|
{ /** [MM-P2P] Experiment (conditional) */ }
|
||||||
{ MMP2P.state.isEnabled
|
{ MMP2P.state.isEnabled
|
||||||
? <MMP2PFlyover isStatic options={MMP2P} />
|
? <MMP2PFlyover isStatic options={MMP2P} />
|
||||||
@@ -232,6 +212,7 @@ function OutlineTab({ intl }) {
|
|||||||
verifiedMode={verifiedMode}
|
verifiedMode={verifiedMode}
|
||||||
accessExpiration={accessExpiration}
|
accessExpiration={accessExpiration}
|
||||||
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
||||||
|
marketingUrl={marketingUrl}
|
||||||
upsellPageName="course_home"
|
upsellPageName="course_home"
|
||||||
userTimezone={userTimezone}
|
userTimezone={userTimezone}
|
||||||
shouldDisplayBorder
|
shouldDisplayBorder
|
||||||
@@ -241,13 +222,10 @@ function OutlineTab({ intl }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CourseDates
|
<CourseDates
|
||||||
courseId={courseId}
|
|
||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
mmp2p={MMP2P}
|
mmp2p={MMP2P}
|
||||||
/>
|
/>
|
||||||
<CourseHandouts
|
<CourseHandouts />
|
||||||
courseId={courseId}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
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, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||||
@@ -6,6 +10,7 @@ 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';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
import { buildMinimalCourseBlocks } from '../../shared/data/__factories__/courseBlocks.factory';
|
import { buildMinimalCourseBlocks } from '../../shared/data/__factories__/courseBlocks.factory';
|
||||||
import {
|
import {
|
||||||
@@ -24,21 +29,21 @@ jest.mock('@edx/frontend-platform/analytics');
|
|||||||
describe('Outline Tab', () => {
|
describe('Outline Tab', () => {
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
|
|
||||||
const courseId = 'course-v1:edX+Test+run';
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
||||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||||
const enrollmentUrl = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`;
|
const enrollmentUrl = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`;
|
||||||
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)}`;
|
const proctoringInfoUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status?is_learning_mfe=true&course_id=${encodeURIComponent(courseId)}&username=MockUser`;
|
||||||
|
|
||||||
const store = initializeStore();
|
const store = initializeStore();
|
||||||
const defaultMetadata = Factory.build('courseHomeMetadata', { id: courseId });
|
const defaultMetadata = Factory.build('courseHomeMetadata');
|
||||||
const defaultTabData = Factory.build('outlineTabData');
|
const defaultTabData = Factory.build('outlineTabData');
|
||||||
|
|
||||||
function setMetadata(attributes, options) {
|
function setMetadata(attributes, options) {
|
||||||
const courseMetadata = Factory.build('courseHomeMetadata', { id: courseId, ...attributes }, options);
|
const courseMetadata = Factory.build('courseHomeMetadata', attributes, options);
|
||||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +52,14 @@ describe('Outline Tab', () => {
|
|||||||
axiosMock.onGet(outlineUrl).reply(200, outlineTabData);
|
axiosMock.onGet(outlineUrl).reply(200, outlineTabData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAndRender() {
|
async function fetchAndRender(path = '') {
|
||||||
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
|
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
|
||||||
await act(async () => render(<OutlineTab />, { store }));
|
await act(async () => render(
|
||||||
|
<MemoryRouter initialEntries={[path]}>
|
||||||
|
<OutlineTab />
|
||||||
|
</MemoryRouter>,
|
||||||
|
{ store },
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -73,7 +83,7 @@ describe('Outline Tab', () => {
|
|||||||
describe('Course Outline', () => {
|
describe('Course Outline', () => {
|
||||||
it('displays link to start course', async () => {
|
it('displays link to start course', async () => {
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByRole('link', { name: 'Start Course' })).toBeInTheDocument();
|
expect(screen.getByRole('link', { name: messages.start.defaultMessage })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays link to resume course', async () => {
|
it('displays link to resume course', async () => {
|
||||||
@@ -134,25 +144,8 @@ describe('Outline Tab', () => {
|
|||||||
expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
|
expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('SequenceLink displays points to legacy courseware', async () => {
|
it('SequenceLink displays link', async () => {
|
||||||
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
||||||
setMetadata({
|
|
||||||
can_load_courseware: false,
|
|
||||||
});
|
|
||||||
setTabData({
|
|
||||||
course_blocks: { blocks: courseBlocks.blocks },
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
|
|
||||||
const sequenceLink = screen.getByText('Title of Sequence');
|
|
||||||
expect(sequenceLink.getAttribute('href')).toContain(`/courses/${courseId}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('SequenceLink displays points to courseware MFE', async () => {
|
|
||||||
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
|
|
||||||
setMetadata({
|
|
||||||
can_load_courseware: true,
|
|
||||||
});
|
|
||||||
setTabData({
|
setTabData({
|
||||||
course_blocks: { blocks: courseBlocks.blocks },
|
course_blocks: { blocks: courseBlocks.blocks },
|
||||||
});
|
});
|
||||||
@@ -328,89 +321,165 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Course Goals', () => {
|
describe('Start or Resume Course Card', () => {
|
||||||
const goalOptions = [
|
it('renders startOrResumeCourseCard', async () => {
|
||||||
['certify', 'Earn a certificate'],
|
|
||||||
['complete', 'Complete the course'],
|
|
||||||
['explore', 'Explore the course'],
|
|
||||||
['unsure', 'Not sure yet'],
|
|
||||||
];
|
|
||||||
|
|
||||||
it('does not render goal widgets if no goals available', async () => {
|
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.queryByTestId('course-goal-card')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('start-resume-card')).toBeInTheDocument();
|
||||||
expect(screen.queryByLabelText('Goal')).not.toBeInTheDocument();
|
});
|
||||||
expect(screen.queryByTestId('edit-goal-selector')).not.toBeInTheDocument();
|
});
|
||||||
|
|
||||||
|
describe('Weekly Learning Goal', () => {
|
||||||
|
it('does not post goals while masquerading', async () => {
|
||||||
|
setMetadata({ is_enrolled: true, original_user_is_staff: true });
|
||||||
|
setTabData({
|
||||||
|
course_goals: {
|
||||||
|
weekly_learning_goal_enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const spy = jest.spyOn(thunks, 'saveWeeklyLearningGoal');
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
const button = await screen.getByTestId('weekly-learning-goal-input-Regular');
|
||||||
|
fireEvent.click(button);
|
||||||
|
expect(spy).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('goal is not set', () => {
|
it('post goal via query param', async () => {
|
||||||
|
setTabData({
|
||||||
|
course_goals: {
|
||||||
|
weekly_learning_goal_enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const spy = jest.spyOn(thunks, 'saveWeeklyLearningGoal');
|
||||||
|
sendTrackEvent.mockClear();
|
||||||
|
|
||||||
|
await fetchAndRender('http://localhost/?weekly_goal=3');
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendTrackEvent).toHaveBeenCalledWith('enrollment.email.clicked.setgoal', {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emit start course event via query param', async () => {
|
||||||
|
sendTrackEvent.mockClear();
|
||||||
|
await fetchAndRender('http://localhost/?start_course=1');
|
||||||
|
expect(sendTrackEvent).toHaveBeenCalledWith('enrollment.email.clicked.startcourse', {});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('weekly learning goal is not set', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
setTabData({
|
setTabData({
|
||||||
course_goals: {
|
course_goals: {
|
||||||
goal_options: goalOptions,
|
weekly_learning_goal_enabled: true,
|
||||||
selected_goal: null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders goal card', () => {
|
it('renders weekly learning goal card', async () => {
|
||||||
expect(screen.queryByLabelText('Goal')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('weekly-learning-goal-card')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('course-goal-card')).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('button', { name: 'Earn a certificate' })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('button', { name: 'Complete the course' })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('button', { name: 'Explore the course' })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('button', { name: 'Not sure yet' })).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders goal selector on goal selection', async () => {
|
it('disables the subscribe button if no goal is set', async () => {
|
||||||
const certifyGoalButton = screen.getByRole('button', { name: 'Earn a certificate' });
|
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeDisabled();
|
||||||
fireEvent.click(certifyGoalButton);
|
|
||||||
|
|
||||||
const goalSelector = await screen.findByTestId('edit-goal-selector');
|
|
||||||
expect(goalSelector).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('goal is set', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
setTabData({
|
|
||||||
course_goals: {
|
|
||||||
goal_options: goalOptions,
|
|
||||||
selected_goal: { text: 'Earn a certificate', key: 'certify' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders edit goal selector', () => {
|
it.each`
|
||||||
expect(screen.getByLabelText('Goal')).toBeInTheDocument();
|
level | days
|
||||||
expect(screen.getByTestId('edit-goal-selector')).toBeInTheDocument();
|
${'Casual'} | ${1}
|
||||||
expect(screen.getByRole('button', { name: 'Earn a certificate' })).toBeInTheDocument();
|
${'Regular'} | ${3}
|
||||||
});
|
${'Intense'} | ${5}
|
||||||
|
`('calls the API with a goal of $days when $level goal is clicked', async ({ level, days }) => {
|
||||||
it('updates goal on click', async () => {
|
// click on Casual goal
|
||||||
// Open dropdown
|
const button = await screen.queryByTestId(`weekly-learning-goal-input-${level}`);
|
||||||
const dropdownButtonNode = screen.getByRole('button', { name: 'Earn a certificate' });
|
fireEvent.click(button);
|
||||||
await waitFor(() => {
|
// Verify the request was made
|
||||||
expect(dropdownButtonNode).toBeInTheDocument();
|
await waitFor(() => {
|
||||||
});
|
expect(axiosMock.history.post[0].url).toMatch(goalUrl);
|
||||||
fireEvent.click(dropdownButtonNode);
|
// subscribe is turned on automatically
|
||||||
|
expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","days_per_week":${days},"subscribed_to_reminders":true}`);
|
||||||
// Select a new goal
|
// verify that the additional info about subscriptions shows up
|
||||||
const unsureButtonNode = screen.getByRole('button', { name: 'Not sure yet' });
|
expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).toBeInTheDocument();
|
||||||
await waitFor(() => {
|
});
|
||||||
expect(unsureButtonNode).toBeInTheDocument();
|
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeEnabled();
|
||||||
});
|
});
|
||||||
fireEvent.click(unsureButtonNode);
|
it('shows and hides subscribe to reminders additional text', async () => {
|
||||||
|
const button = await screen.getByTestId('weekly-learning-goal-input-Regular');
|
||||||
|
fireEvent.click(button);
|
||||||
// Verify the request was made
|
// Verify the request was made
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(axiosMock.history.post[0].url).toMatch(goalUrl);
|
expect(axiosMock.history.post[0].url).toMatch(goalUrl);
|
||||||
expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","goal_key":"unsure"}`);
|
// subscribe is turned on automatically
|
||||||
|
expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","days_per_week":3,"subscribed_to_reminders":true}`);
|
||||||
|
// verify that the additional info about subscriptions shows up
|
||||||
|
expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeEnabled();
|
||||||
|
|
||||||
|
// Click on subscribe to reminders toggle
|
||||||
|
const subscriptionSwitch = await screen.getByRole('switch', { name: messages.setGoalReminder.defaultMessage });
|
||||||
|
expect(subscriptionSwitch).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(subscriptionSwitch);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(axiosMock.history.post[1].url).toMatch(goalUrl);
|
||||||
|
expect(axiosMock.history.post[1].data)
|
||||||
|
.toMatch(`{"course_id":"${courseId}","days_per_week":3,"subscribed_to_reminders":false}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify that the additional info about subscriptions gets hidden
|
||||||
|
expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has button for weekly learning goal selected', async () => {
|
||||||
|
setTabData({
|
||||||
|
course_goals: {
|
||||||
|
weekly_learning_goal_enabled: true,
|
||||||
|
selected_goal: {
|
||||||
|
subscribed_to_reminders: true,
|
||||||
|
days_per_week: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
|
||||||
|
const button = await screen.queryByTestId('weekly-learning-goal-input-Regular');
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
expect(button).toHaveClass('flag-button-selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders weekly learning goal card if ProctoringInfoPanel is not shown', async () => {
|
||||||
|
setTabData({
|
||||||
|
course_goals: {
|
||||||
|
weekly_learning_goal_enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
axiosMock.onGet(proctoringInfoUrl).reply(404);
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.queryByTestId('weekly-learning-goal-card')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders weekly learning goal card if ProctoringInfoPanel is not enabled', async () => {
|
||||||
|
setTabData({
|
||||||
|
course_goals: {
|
||||||
|
weekly_learning_goal_enabled: true,
|
||||||
|
enableProctoredExams: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.queryByTestId('weekly-learning-goal-card')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders weekly learning goal card if ProctoringInfoPanel is enabled', async () => {
|
||||||
|
setTabData({
|
||||||
|
course_goals: {
|
||||||
|
weekly_learning_goal_enabled: true,
|
||||||
|
enableProctoredExams: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.queryByTestId('weekly-learning-goal-card')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Course Handouts', () => {
|
describe('Course Handouts', () => {
|
||||||
@@ -508,7 +577,7 @@ describe('Outline Tab', () => {
|
|||||||
const instructorToolbar = await screen.getByTestId('instructor-toolbar');
|
const instructorToolbar = await screen.getByTestId('instructor-toolbar');
|
||||||
expect(instructorToolbar).toBeInTheDocument();
|
expect(instructorToolbar).toBeInTheDocument();
|
||||||
expect(screen.getByText('This learner no longer has access to this course. Their access expired on', { exact: false })).toBeInTheDocument();
|
expect(screen.getByText('This learner no longer has access to this course. Their access expired on', { exact: false })).toBeInTheDocument();
|
||||||
expect(screen.getByText('1/1/2020')).toBeInTheDocument();
|
expect(screen.getByText('1/1/2020', { exact: false })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render banner when not masquerading', async () => {
|
it('does not render banner when not masquerading', async () => {
|
||||||
@@ -599,7 +668,6 @@ describe('Outline Tab', () => {
|
|||||||
cert_status: CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE,
|
cert_status: CERT_STATUS_TYPE.EARNED_NOT_AVAILABLE,
|
||||||
cert_web_view_url: null,
|
cert_web_view_url: null,
|
||||||
certificate_available_date: tomorrow.toISOString(),
|
certificate_available_date: tomorrow.toISOString(),
|
||||||
download_url: null,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
date_blocks: [
|
date_blocks: [
|
||||||
@@ -616,7 +684,7 @@ describe('Outline Tab', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.queryByText('Your grade and certificate will be ready soon!')).toBeInTheDocument();
|
expect(screen.queryByText('Your grade and certificate status will be available soon.')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('renders verification alert', async () => {
|
it('renders verification alert', async () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -627,7 +695,6 @@ describe('Outline Tab', () => {
|
|||||||
cert_data: {
|
cert_data: {
|
||||||
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
|
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
|
||||||
cert_web_view_url: null,
|
cert_web_view_url: null,
|
||||||
download_url: null,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
date_blocks: [
|
date_blocks: [
|
||||||
@@ -650,7 +717,7 @@ describe('Outline Tab', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.queryByText('Verify your identity to earn a certificate!')).toBeInTheDocument();
|
expect(screen.queryByText('Verify your identity to qualify for a certificate.')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('renders non passing grade', async () => {
|
it('renders non passing grade', async () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -683,8 +750,8 @@ describe('Outline Tab', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
screen.getAllByText('You are not eligible for a certificate');
|
screen.getAllByText('You are not yet eligible for a certificate');
|
||||||
expect(screen.queryByText('You are not eligible for a certificate')).toBeInTheDocument();
|
expect(screen.queryByText('You are not yet eligible for a certificate')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('tracks request cert button', async () => {
|
it('tracks request cert button', async () => {
|
||||||
sendTrackEvent.mockClear();
|
sendTrackEvent.mockClear();
|
||||||
@@ -696,7 +763,6 @@ describe('Outline Tab', () => {
|
|||||||
cert_data: {
|
cert_data: {
|
||||||
cert_status: CERT_STATUS_TYPE.REQUESTING,
|
cert_status: CERT_STATUS_TYPE.REQUESTING,
|
||||||
cert_web_view_url: null,
|
cert_web_view_url: null,
|
||||||
download_url: null,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
date_blocks: [
|
date_blocks: [
|
||||||
@@ -725,55 +791,12 @@ describe('Outline Tab', () => {
|
|||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_request_cert_button.clicked',
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_request_cert_button.clicked',
|
||||||
{
|
{
|
||||||
courserun_key: 'course-v1:edX+Test+run',
|
courserun_key: courseId,
|
||||||
is_staff: false,
|
|
||||||
org_key: 'edX',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('tracks download cert button', async () => {
|
|
||||||
sendTrackEvent.mockClear();
|
|
||||||
const now = new Date();
|
|
||||||
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
||||||
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
||||||
setMetadata({ is_enrolled: true });
|
|
||||||
setTabData({
|
|
||||||
cert_data: {
|
|
||||||
cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
|
|
||||||
cert_web_view_url: null,
|
|
||||||
download_url: null,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
date_blocks: [
|
|
||||||
{
|
|
||||||
date_type: 'course-end-date',
|
|
||||||
date: yesterday.toISOString(),
|
|
||||||
title: 'End',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date_type: 'certificate-available-date',
|
|
||||||
date: tomorrow.toISOString(),
|
|
||||||
title: 'Cert Available',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date_type: 'verification-deadline-date',
|
|
||||||
date: tomorrow.toISOString(),
|
|
||||||
link_text: 'Verify',
|
|
||||||
title: 'Verification Upgrade Deadline',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
sendTrackEvent.mockClear();
|
|
||||||
const requestingButton = screen.getByRole('button', { name: 'View my certificate' });
|
|
||||||
fireEvent.click(requestingButton);
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_downloadable_button.clicked',
|
|
||||||
{
|
|
||||||
courserun_key: 'course-v1:edX+Test+run',
|
|
||||||
is_staff: false,
|
is_staff: false,
|
||||||
org_key: 'edX',
|
org_key: 'edX',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tracks unverified cert button', async () => {
|
it('tracks unverified cert button', async () => {
|
||||||
sendTrackEvent.mockClear();
|
sendTrackEvent.mockClear();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -784,7 +807,6 @@ describe('Outline Tab', () => {
|
|||||||
cert_data: {
|
cert_data: {
|
||||||
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
|
cert_status: CERT_STATUS_TYPE.UNVERIFIED,
|
||||||
cert_web_view_url: null,
|
cert_web_view_url: null,
|
||||||
download_url: null,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
date_blocks: [
|
date_blocks: [
|
||||||
@@ -813,7 +835,7 @@ describe('Outline Tab', () => {
|
|||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_unverified_button.clicked',
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_outline.certificate_alert_unverified_button.clicked',
|
||||||
{
|
{
|
||||||
courserun_key: 'course-v1:edX+Test+run',
|
courserun_key: courseId,
|
||||||
is_staff: false,
|
is_staff: false,
|
||||||
org_key: 'edX',
|
org_key: 'edX',
|
||||||
});
|
});
|
||||||
@@ -856,7 +878,7 @@ describe('Outline Tab', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByRole('link', { name: 'Start Course' })).toBeInTheDocument();
|
expect(screen.getByRole('link', { name: messages.start.defaultMessage })).toBeInTheDocument();
|
||||||
expect(screen.queryByText('More content is coming soon!')).not.toBeInTheDocument();
|
expect(screen.queryByText('More content is coming soon!')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -872,7 +894,6 @@ describe('Outline Tab', () => {
|
|||||||
cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
|
cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
|
||||||
cert_web_view_url: 'certificate/testuuid',
|
cert_web_view_url: 'certificate/testuuid',
|
||||||
certificate_available_date: null,
|
certificate_available_date: null,
|
||||||
download_url: null,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
date_blocks: [
|
date_blocks: [
|
||||||
@@ -898,7 +919,6 @@ describe('Outline Tab', () => {
|
|||||||
cert_status: CERT_STATUS_TYPE.REQUESTING,
|
cert_status: CERT_STATUS_TYPE.REQUESTING,
|
||||||
cert_web_view_url: null,
|
cert_web_view_url: null,
|
||||||
certificate_available_date: null,
|
certificate_available_date: null,
|
||||||
download_url: null,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
date_blocks: [
|
date_blocks: [
|
||||||
@@ -915,33 +935,6 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Certificate (pdf) Complete Alert', () => {
|
|
||||||
it('appears', async () => {
|
|
||||||
const now = new Date();
|
|
||||||
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
||||||
setMetadata({ is_enrolled: true });
|
|
||||||
setTabData({
|
|
||||||
cert_data: {
|
|
||||||
cert_status: CERT_STATUS_TYPE.DOWNLOADABLE,
|
|
||||||
cert_web_view_url: null,
|
|
||||||
certificate_available_date: null,
|
|
||||||
download_url: 'download/url',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
date_blocks: [
|
|
||||||
{
|
|
||||||
date_type: 'course-end-date',
|
|
||||||
date: yesterday.toISOString(),
|
|
||||||
title: 'End',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.queryByText('Congratulations! Your certificate is ready.')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole('link', { name: 'Download my certificate' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Proctoring Info Panel', () => {
|
describe('Proctoring Info Panel', () => {
|
||||||
const onboardingReleaseDate = new Date();
|
const onboardingReleaseDate = new Date();
|
||||||
onboardingReleaseDate.setDate(new Date().getDate() - 7);
|
onboardingReleaseDate.setDate(new Date().getDate() - 7);
|
||||||
@@ -1024,6 +1017,22 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays expiration warning', async () => {
|
it('displays expiration warning', async () => {
|
||||||
|
const expirationDate = new Date();
|
||||||
|
// This message will render if the expiration date is within 28 days; set the date 10 days in future
|
||||||
|
expirationDate.setTime(expirationDate.getTime() + 864800000);
|
||||||
|
axiosMock.onGet(proctoringInfoUrl).reply(200, {
|
||||||
|
onboarding_status: 'verified',
|
||||||
|
onboarding_link: 'test',
|
||||||
|
expiration_date: expirationDate.toString(),
|
||||||
|
onboarding_release_date: onboardingReleaseDate.toISOString(),
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
await screen.findByText('This course contains proctored exams');
|
||||||
|
expect(screen.queryByText('Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays expiration warning for other course', async () => {
|
||||||
const expirationDate = new Date();
|
const expirationDate = new Date();
|
||||||
// This message will render if the expiration date is within 28 days; set the date 10 days in future
|
// This message will render if the expiration date is within 28 days; set the date 10 days in future
|
||||||
expirationDate.setTime(expirationDate.getTime() + 864800000);
|
expirationDate.setTime(expirationDate.getTime() + 864800000);
|
||||||
@@ -1035,7 +1044,23 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
await screen.findByText('This course contains proctored exams');
|
await screen.findByText('This course contains proctored exams');
|
||||||
expect(screen.queryByText('Your onboarding profile has been approved in another course. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.')).toBeInTheDocument();
|
expect(screen.queryByText('Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays expired', async () => {
|
||||||
|
const expirationDate = new Date();
|
||||||
|
// This message appears after expiration, set the date 10 days in the past
|
||||||
|
expirationDate.setTime(expirationDate.getTime() - 864800000);
|
||||||
|
axiosMock.onGet(proctoringInfoUrl).reply(200, {
|
||||||
|
onboarding_status: 'verified',
|
||||||
|
onboarding_link: 'test',
|
||||||
|
expiration_date: expirationDate.toString(),
|
||||||
|
onboarding_release_date: onboardingReleaseDate.toISOString(),
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
await screen.findByText('This course contains proctored exams');
|
||||||
|
expect(screen.queryByText('Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1056,6 +1081,7 @@ describe('Outline Tab', () => {
|
|||||||
|
|
||||||
it('does not appear for 404', async () => {
|
it('does not appear for 404', async () => {
|
||||||
axiosMock.onGet(proctoringInfoUrl).reply(404);
|
axiosMock.onGet(proctoringInfoUrl).reply(404);
|
||||||
|
await fetchAndRender();
|
||||||
expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('link', { name: 'Review instructions and system requirements' })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1169,7 +1195,7 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accont Activation Alert', () => {
|
describe('Account Activation Alert', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const intersectionObserverMock = () => ({
|
const intersectionObserverMock = () => ({
|
||||||
observe: () => null,
|
observe: () => null,
|
||||||
@@ -1197,7 +1223,7 @@ describe('Outline Tab', () => {
|
|||||||
expect(screen.queryByRole('button', { name: 'resend the email' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: 'resend the email' })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends account activation email on clicking the resened email in account activation alert', async () => {
|
it('sends account activation email on clicking the re-send email in account activation alert', async () => {
|
||||||
Cookies.set = jest.fn();
|
Cookies.set = jest.fn();
|
||||||
Cookies.get = jest.fn().mockImplementation(() => 'true');
|
Cookies.get = jest.fn().mockImplementation(() => 'true');
|
||||||
Cookies.remove = jest.fn().mockImplementation(() => { Cookies.get = jest.fn(); });
|
Cookies.remove = jest.fn().mockImplementation(() => { Cookies.get = jest.fn(); });
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Hyperlink } from '@edx/paragon';
|
|
||||||
import {
|
import {
|
||||||
FormattedMessage,
|
FormattedMessage,
|
||||||
FormattedTime,
|
FormattedTime,
|
||||||
@@ -28,25 +27,16 @@ function SequenceLink({
|
|||||||
complete,
|
complete,
|
||||||
description,
|
description,
|
||||||
due,
|
due,
|
||||||
legacyWebUrl,
|
|
||||||
showLink,
|
showLink,
|
||||||
title,
|
title,
|
||||||
} = sequence;
|
} = sequence;
|
||||||
const {
|
const {
|
||||||
userTimezone,
|
userTimezone,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
const {
|
|
||||||
canLoadCourseware,
|
|
||||||
} = useModel('courseHomeMeta', courseId);
|
|
||||||
|
|
||||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
|
|
||||||
// canLoadCourseware is true if the Courseware MFE is enabled, false otherwise
|
const coursewareUrl = <Link to={`/course/${courseId}/${id}`}>{title}</Link>;
|
||||||
const coursewareUrl = (
|
|
||||||
canLoadCourseware
|
|
||||||
? <Link to={`/course/${courseId}/${id}`}>{title}</Link>
|
|
||||||
: <Hyperlink destination={legacyWebUrl}>{title}</Hyperlink>
|
|
||||||
);
|
|
||||||
const displayTitle = showLink ? coursewareUrl : title;
|
const displayTitle = showLink ? coursewareUrl : title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ function CertificateStatusAlert({ intl, payload }) {
|
|||||||
courseEndDate,
|
courseEndDate,
|
||||||
courseId,
|
courseId,
|
||||||
certURL,
|
certURL,
|
||||||
isWebCert,
|
|
||||||
userTimezone,
|
userTimezone,
|
||||||
org,
|
org,
|
||||||
notPassingCourseEnded,
|
notPassingCourseEnded,
|
||||||
@@ -66,8 +65,8 @@ function CertificateStatusAlert({ intl, payload }) {
|
|||||||
alertProps.body = (
|
alertProps.body = (
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="learning.outline.alert.cert.when"
|
id="learning.outline.alert.cert.earnedNotAvailable"
|
||||||
defaultMessage="This course ends on {courseEndDateFormatted}. Final grades and certificates are
|
defaultMessage="This course ends on {courseEndDateFormatted}. Final grades and any earned certificates are
|
||||||
scheduled to be available after {certificateAvailableDate}."
|
scheduled to be available after {certificateAvailableDate}."
|
||||||
values={{
|
values={{
|
||||||
courseEndDateFormatted,
|
courseEndDateFormatted,
|
||||||
@@ -79,11 +78,7 @@ function CertificateStatusAlert({ intl, payload }) {
|
|||||||
);
|
);
|
||||||
} else if (certStatus === CERT_STATUS_TYPE.DOWNLOADABLE) {
|
} else if (certStatus === CERT_STATUS_TYPE.DOWNLOADABLE) {
|
||||||
alertProps.header = intl.formatMessage(certMessages.certStatusDownloadableHeader);
|
alertProps.header = intl.formatMessage(certMessages.certStatusDownloadableHeader);
|
||||||
if (isWebCert) {
|
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.viewableButton);
|
||||||
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.viewableButton);
|
|
||||||
} else {
|
|
||||||
alertProps.buttonMessage = intl.formatMessage(certStatusMessages.downloadableButton);
|
|
||||||
}
|
|
||||||
alertProps.buttonVisible = true;
|
alertProps.buttonVisible = true;
|
||||||
alertProps.buttonLink = certURL;
|
alertProps.buttonLink = certURL;
|
||||||
alertProps.buttonAction = () => {
|
alertProps.buttonAction = () => {
|
||||||
@@ -204,7 +199,6 @@ CertificateStatusAlert.propTypes = {
|
|||||||
courseEndDate: PropTypes.string,
|
courseEndDate: PropTypes.string,
|
||||||
courseId: PropTypes.string,
|
courseId: PropTypes.string,
|
||||||
certURL: PropTypes.string,
|
certURL: PropTypes.string,
|
||||||
isWebCert: PropTypes.bool,
|
|
||||||
userTimezone: PropTypes.string,
|
userTimezone: PropTypes.string,
|
||||||
org: PropTypes.string,
|
org: PropTypes.string,
|
||||||
notPassingCourseEnded: PropTypes.bool,
|
notPassingCourseEnded: PropTypes.bool,
|
||||||
|
|||||||
@@ -51,10 +51,8 @@ function useCertificateStatusAlert(courseId) {
|
|||||||
certStatus,
|
certStatus,
|
||||||
certWebViewUrl,
|
certWebViewUrl,
|
||||||
certificateAvailableDate,
|
certificateAvailableDate,
|
||||||
downloadUrl,
|
|
||||||
} = certData || {};
|
} = certData || {};
|
||||||
const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
|
const endBlock = courseDateBlocks.find(b => b.dateType === 'course-end-date');
|
||||||
const isWebCert = downloadUrl === null;
|
|
||||||
const isVerifiedEnrollmentMode = (
|
const isVerifiedEnrollmentMode = (
|
||||||
enrollmentMode !== null
|
enrollmentMode !== null
|
||||||
&& enrollmentMode !== undefined
|
&& enrollmentMode !== undefined
|
||||||
@@ -63,9 +61,6 @@ function useCertificateStatusAlert(courseId) {
|
|||||||
let certURL = '';
|
let certURL = '';
|
||||||
if (certWebViewUrl) {
|
if (certWebViewUrl) {
|
||||||
certURL = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
|
certURL = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
|
||||||
} else if (downloadUrl) {
|
|
||||||
// PDF Certificate
|
|
||||||
certURL = downloadUrl;
|
|
||||||
}
|
}
|
||||||
const hasAlertingCertStatus = verifyCertStatusType(certStatus);
|
const hasAlertingCertStatus = verifyCertStatusType(certStatus);
|
||||||
|
|
||||||
@@ -87,7 +82,6 @@ function useCertificateStatusAlert(courseId) {
|
|||||||
courseId,
|
courseId,
|
||||||
courseEndDate: endBlock && endBlock.date,
|
courseEndDate: endBlock && endBlock.date,
|
||||||
userTimezone,
|
userTimezone,
|
||||||
isWebCert,
|
|
||||||
org,
|
org,
|
||||||
notPassingCourseEnded,
|
notPassingCourseEnded,
|
||||||
tabs,
|
tabs,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
certStatusEarnedNotAvailableHeader: {
|
certStatusEarnedNotAvailableHeader: {
|
||||||
id: 'cert.alert.earned.unavailable.header',
|
id: 'cert.alert.earned.unavailable.header.v2',
|
||||||
defaultMessage: 'Your grade and certificate will be ready soon!',
|
defaultMessage: 'Your grade and certificate status will be available soon.',
|
||||||
description: 'Header alerting the user that their certificate will be available soon.',
|
description: 'Header alerting the user that their certificate will be available soon.',
|
||||||
},
|
},
|
||||||
certStatusDownloadableHeader: {
|
certStatusDownloadableHeader: {
|
||||||
@@ -13,7 +13,7 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
certStatusNotPassingHeader: {
|
certStatusNotPassingHeader: {
|
||||||
id: 'cert.alert.notPassing.header',
|
id: 'cert.alert.notPassing.header',
|
||||||
defaultMessage: 'You are not eligible for a certificate',
|
defaultMessage: 'You are not yet eligible for a certificate',
|
||||||
},
|
},
|
||||||
certStatusNotPassingButton: {
|
certStatusNotPassingButton: {
|
||||||
id: 'cert.alert.notPassing.button',
|
id: 'cert.alert.notPassing.button',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import {
|
import {
|
||||||
FormattedDate,
|
FormattedDate,
|
||||||
FormattedMessage,
|
FormattedMessage,
|
||||||
FormattedRelative,
|
FormattedRelativeTime,
|
||||||
FormattedTime,
|
FormattedTime,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Alert } from '@edx/paragon';
|
import { Alert } from '@edx/paragon';
|
||||||
@@ -21,7 +21,7 @@ function CourseEndAlert({ payload }) {
|
|||||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
|
|
||||||
const timeRemaining = (
|
const timeRemaining = (
|
||||||
<FormattedRelative
|
<FormattedRelativeTime
|
||||||
key="timeRemaining"
|
key="timeRemaining"
|
||||||
value={endDate}
|
value={endDate}
|
||||||
{...timezoneFormatArgs}
|
{...timezoneFormatArgs}
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ const messages = defineMessages({
|
|||||||
allDates: {
|
allDates: {
|
||||||
id: 'learning.outline.dates.all',
|
id: 'learning.outline.dates.all',
|
||||||
defaultMessage: 'View all course dates',
|
defaultMessage: 'View all course dates',
|
||||||
|
description: 'Text anchor for link that redirects to dates or course timeline tab',
|
||||||
|
},
|
||||||
|
casualGoalButtonText: {
|
||||||
|
id: 'learning.outline.goalButton.casual.text',
|
||||||
|
defaultMessage: '1 day a week',
|
||||||
|
description: 'Text shown for casual goal button',
|
||||||
|
},
|
||||||
|
casualGoalButtonTitle: {
|
||||||
|
id: 'learning.outline.goalButton.screenReader.text',
|
||||||
|
defaultMessage: 'Casual',
|
||||||
|
description: 'A very short description of the least intense of three learning goals',
|
||||||
|
},
|
||||||
|
certAlt: {
|
||||||
|
id: 'learning.outline.certificateAlt',
|
||||||
|
defaultMessage: 'Example Certificate',
|
||||||
|
description: 'Alternate text displayed when the example certificate image cannot be displayed.',
|
||||||
},
|
},
|
||||||
collapseAll: {
|
collapseAll: {
|
||||||
id: 'learning.outline.collapseAll',
|
id: 'learning.outline.collapseAll',
|
||||||
@@ -23,6 +39,7 @@ const messages = defineMessages({
|
|||||||
dates: {
|
dates: {
|
||||||
id: 'learning.outline.dates',
|
id: 'learning.outline.dates',
|
||||||
defaultMessage: 'Important dates',
|
defaultMessage: 'Important dates',
|
||||||
|
description: 'Headline for the (summary of dates) section of the outline page',
|
||||||
},
|
},
|
||||||
editGoal: {
|
editGoal: {
|
||||||
id: 'learning.outline.editGoal',
|
id: 'learning.outline.editGoal',
|
||||||
@@ -39,6 +56,11 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Goal',
|
defaultMessage: 'Goal',
|
||||||
description: 'Label for the selected course goal',
|
description: 'Label for the selected course goal',
|
||||||
},
|
},
|
||||||
|
goalReminderDetail: {
|
||||||
|
id: 'learning.outline.goalReminderDetail',
|
||||||
|
defaultMessage: 'If we notice you’re not quite at your goal, we’ll send you an email reminder.',
|
||||||
|
description: 'It describe to learner what is goal reminder service',
|
||||||
|
},
|
||||||
goalUnsure: {
|
goalUnsure: {
|
||||||
id: 'learning.outline.goalUnsure',
|
id: 'learning.outline.goalUnsure',
|
||||||
defaultMessage: 'Not sure yet',
|
defaultMessage: 'Not sure yet',
|
||||||
@@ -46,6 +68,7 @@ const messages = defineMessages({
|
|||||||
handouts: {
|
handouts: {
|
||||||
id: 'learning.outline.handouts',
|
id: 'learning.outline.handouts',
|
||||||
defaultMessage: 'Course Handouts',
|
defaultMessage: 'Course Handouts',
|
||||||
|
description: 'Header for (Course Handouts) section in course outline',
|
||||||
},
|
},
|
||||||
incompleteAssignment: {
|
incompleteAssignment: {
|
||||||
id: 'learning.outline.incompleteAssignment',
|
id: 'learning.outline.incompleteAssignment',
|
||||||
@@ -57,6 +80,16 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Incomplete section',
|
defaultMessage: 'Incomplete section',
|
||||||
description: 'Text used to describe the gray checkmark icon in front of a section title',
|
description: 'Text used to describe the gray checkmark icon in front of a section title',
|
||||||
},
|
},
|
||||||
|
intenseGoalButtonText: {
|
||||||
|
id: 'learning.outline.goalButton.intense.text',
|
||||||
|
defaultMessage: '5 days a week',
|
||||||
|
description: 'Text shown for intense goal button',
|
||||||
|
},
|
||||||
|
intenseGoalButtonTitle: {
|
||||||
|
id: 'learning.outline.goalButton.intense.title',
|
||||||
|
defaultMessage: 'Intense',
|
||||||
|
description: 'A very short description of the most intensive option of three learning goals, Casual, Regular and Intense',
|
||||||
|
},
|
||||||
learnMore: {
|
learnMore: {
|
||||||
id: 'learning.outline.learnMore',
|
id: 'learning.outline.learnMore',
|
||||||
defaultMessage: 'Learn More',
|
defaultMessage: 'Learn More',
|
||||||
@@ -66,34 +99,80 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Open',
|
defaultMessage: 'Open',
|
||||||
description: 'A button to open the given section of the course outline',
|
description: 'A button to open the given section of the course outline',
|
||||||
},
|
},
|
||||||
|
proctoringInfoPanel: {
|
||||||
|
id: 'learning.proctoringPanel.header',
|
||||||
|
defaultMessage: 'This course contains proctored exams',
|
||||||
|
description: 'Used as a label to indicate that course has proctored exams',
|
||||||
|
},
|
||||||
|
regularGoalButtonText: {
|
||||||
|
id: 'learning.outline.goalButton.regular.text',
|
||||||
|
defaultMessage: '3 days a week',
|
||||||
|
description: 'Text shown for regular goal button',
|
||||||
|
|
||||||
|
},
|
||||||
|
regularGoalButtonTitle: {
|
||||||
|
id: 'learning.outline.goalButton.regular.title',
|
||||||
|
defaultMessage: 'Regular',
|
||||||
|
description: 'A very short description of the middle option of three learning goals, Casual, Regular and Intense',
|
||||||
|
},
|
||||||
|
resumeBlurb: {
|
||||||
|
id: 'learning.outline.resumeBlurb',
|
||||||
|
defaultMessage: 'Pick up where you left off',
|
||||||
|
description: 'Text describing to the learner that they can return to the last section they visited within the course.',
|
||||||
|
},
|
||||||
resume: {
|
resume: {
|
||||||
id: 'learning.outline.resume',
|
id: 'learning.outline.resume',
|
||||||
defaultMessage: 'Resume course',
|
defaultMessage: 'Resume course',
|
||||||
|
description: 'Anchor text for button that would resume course',
|
||||||
},
|
},
|
||||||
setGoal: {
|
setGoal: {
|
||||||
id: 'learning.outline.setGoal',
|
id: 'learning.outline.setGoal',
|
||||||
defaultMessage: 'To start, set a course goal by selecting the option below that best describes your learning plan.',
|
defaultMessage: 'To start, set a course goal by selecting the option below that best describes your learning plan.',
|
||||||
|
description: 'In indicate to learner how to set or use the goal reminder service',
|
||||||
|
},
|
||||||
|
setGoalReminder: {
|
||||||
|
id: 'learning.outline.setGoalReminder',
|
||||||
|
defaultMessage: 'Set a goal reminder',
|
||||||
|
description: 'The text for the radio button which activate or deactivate the goal reminder service',
|
||||||
|
},
|
||||||
|
setLearningGoalButtonScreenReaderText: {
|
||||||
|
id: 'learning.outline.goalButton.casual.title',
|
||||||
|
defaultMessage: 'Set a learning goal style.',
|
||||||
|
description: 'screen reader text informing learner they can select an intensity of learning goal',
|
||||||
|
},
|
||||||
|
setWeeklyGoal: {
|
||||||
|
id: 'learning.outline.setWeeklyGoal',
|
||||||
|
defaultMessage: 'Set a weekly learning goal',
|
||||||
|
description: 'The headline for (goal reminder service) section in course outline',
|
||||||
|
},
|
||||||
|
setWeeklyGoalDetail: {
|
||||||
|
id: 'learning.outline.setWeeklyGoalDetail',
|
||||||
|
defaultMessage: 'Setting a goal motivates you to finish the course. You can always change it later.',
|
||||||
|
description: 'It indiacate the gaol or the purpose of the goal reminder service to learners',
|
||||||
},
|
},
|
||||||
start: {
|
start: {
|
||||||
id: 'learning.outline.start',
|
id: 'learning.outline.start',
|
||||||
defaultMessage: 'Start Course',
|
defaultMessage: 'Start course',
|
||||||
|
description: 'The text for button which starts the course',
|
||||||
|
},
|
||||||
|
startBlurb: {
|
||||||
|
id: 'learning.outline.startBlurb',
|
||||||
|
defaultMessage: 'Begin your course today',
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
id: 'learning.outline.tools',
|
id: 'learning.outline.tools',
|
||||||
defaultMessage: 'Course Tools',
|
defaultMessage: 'Course Tools',
|
||||||
|
description: 'Headline for the (course tools) section in course outline. course tool might include links to course bookmarks, financial assistance...etc',
|
||||||
},
|
},
|
||||||
upgradeButton: {
|
upgradeButton: {
|
||||||
id: 'learning.outline.upgradeButton',
|
id: 'learning.outline.upgradeButton',
|
||||||
defaultMessage: 'Upgrade ({symbol}{price})',
|
defaultMessage: 'Upgrade ({symbol}{price})',
|
||||||
|
description: 'Text for the button which redirects to the upgrading page',
|
||||||
},
|
},
|
||||||
upgradeTitle: {
|
upgradeTitle: {
|
||||||
id: 'learning.outline.upgradeTitle',
|
id: 'learning.outline.upgradeTitle',
|
||||||
defaultMessage: 'Pursue a verified certificate',
|
defaultMessage: 'Pursue a verified certificate',
|
||||||
},
|
description: 'Upgrade title',
|
||||||
certAlt: {
|
|
||||||
id: 'learning.outline.certificateAlt',
|
|
||||||
defaultMessage: 'Example Certificate',
|
|
||||||
description: 'Alternate text displayed when the example certificate image cannot be displayed.',
|
|
||||||
},
|
},
|
||||||
welcomeMessage: {
|
welcomeMessage: {
|
||||||
id: 'learning.outline.welcomeMessage',
|
id: 'learning.outline.welcomeMessage',
|
||||||
@@ -112,113 +191,145 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Welcome to',
|
defaultMessage: 'Welcome to',
|
||||||
description: 'This precedes the title of the course',
|
description: 'This precedes the title of the course',
|
||||||
},
|
},
|
||||||
proctoringInfoPanel: {
|
|
||||||
id: 'learning.proctoringPanel.header',
|
|
||||||
defaultMessage: 'This course contains proctored exams',
|
|
||||||
},
|
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
|
},
|
||||||
|
expiredProctoringStatus: {
|
||||||
|
id: 'learning.proctoringPanel.status.expired',
|
||||||
|
defaultMessage: 'Expired',
|
||||||
|
description: 'A label to indicate that proctortrack 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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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)',
|
||||||
},
|
},
|
||||||
expiringSoonProctoringMessage: {
|
expiringSoonProctoringMessage: {
|
||||||
id: 'learning.proctoringPanel.message.expiringSoon',
|
id: 'learning.proctoringPanel.message.expiringSoon',
|
||||||
defaultMessage: 'Your onboarding profile has been approved in another course. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.',
|
defaultMessage: 'Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.',
|
||||||
|
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (expiring soon)',
|
||||||
|
},
|
||||||
|
expiredProctoringMessage: {
|
||||||
|
id: 'learning.proctoringPanel.message.expired',
|
||||||
|
defaultMessage: 'Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.',
|
||||||
|
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (expired)',
|
||||||
},
|
},
|
||||||
proctoringPanelGeneralInfo: {
|
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
proctoringOnboardingPracticeButton: {
|
proctoringOnboardingPracticeButton: {
|
||||||
id: 'learning.proctoringPanel.onboardingPracticeButton',
|
id: 'learning.proctoringPanel.onboardingPracticeButton',
|
||||||
defaultMessage: 'View Onboarding Exam',
|
defaultMessage: 'View Onboarding Exam',
|
||||||
|
description: 'The text that appears on onboarding exam while its not released, so learners can take or view it as a practice',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
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',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
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 { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
@@ -8,11 +9,13 @@ import messages from '../messages';
|
|||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
function CourseDates({
|
function CourseDates({
|
||||||
courseId,
|
|
||||||
intl,
|
intl,
|
||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
mmp2p,
|
mmp2p,
|
||||||
}) {
|
}) {
|
||||||
|
const {
|
||||||
|
courseId,
|
||||||
|
} = useSelector(state => state.courseHome);
|
||||||
const {
|
const {
|
||||||
userTimezone,
|
userTimezone,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
@@ -29,34 +32,34 @@ function CourseDates({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mb-4">
|
<section className="mb-4">
|
||||||
<h2 className="h4">{intl.formatMessage(messages.dates)}</h2>
|
<div id="courseHome-dates">
|
||||||
<ol className="list-unstyled">
|
<h2 className="h4">{intl.formatMessage(messages.dates)}</h2>
|
||||||
{courseDateBlocks.map((courseDateBlock) => (
|
<ol className="list-unstyled">
|
||||||
<DateSummary
|
{courseDateBlocks.map((courseDateBlock) => (
|
||||||
key={courseDateBlock.title + courseDateBlock.date}
|
<DateSummary
|
||||||
dateBlock={courseDateBlock}
|
key={courseDateBlock.title + courseDateBlock.date}
|
||||||
userTimezone={userTimezone}
|
dateBlock={courseDateBlock}
|
||||||
/** [MM-P2P] Experiment */
|
userTimezone={userTimezone}
|
||||||
mmp2p={mmp2p}
|
/** [MM-P2P] Experiment */
|
||||||
/>
|
mmp2p={mmp2p}
|
||||||
))}
|
/>
|
||||||
</ol>
|
))}
|
||||||
<a className="font-weight-bold ml-4 pl-1 small" href={datesTabLink}>
|
</ol>
|
||||||
{intl.formatMessage(messages.allDates)}
|
<a className="font-weight-bold ml-4 pl-1 small" href={datesTabLink}>
|
||||||
</a>
|
{intl.formatMessage(messages.allDates)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CourseDates.propTypes = {
|
CourseDates.propTypes = {
|
||||||
courseId: PropTypes.string,
|
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
mmp2p: PropTypes.shape({}),
|
mmp2p: PropTypes.shape({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseDates.defaultProps = {
|
CourseDates.defaultProps = {
|
||||||
courseId: null,
|
|
||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
mmp2p: {},
|
mmp2p: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { Button, Card } from '@edx/paragon';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import messages from '../messages';
|
|
||||||
|
|
||||||
import { saveCourseGoal } from '../../data';
|
|
||||||
|
|
||||||
function CourseGoalCard({
|
|
||||||
courseId,
|
|
||||||
goalOptions,
|
|
||||||
intl,
|
|
||||||
title,
|
|
||||||
setGoalToDisplay,
|
|
||||||
setGoalToastHeader,
|
|
||||||
}) {
|
|
||||||
function selectGoalHandler(event) {
|
|
||||||
const selectedGoal = {
|
|
||||||
key: event.currentTarget.getAttribute('data-goal-key'),
|
|
||||||
text: event.currentTarget.getAttribute('data-goal-text'),
|
|
||||||
};
|
|
||||||
|
|
||||||
saveCourseGoal(courseId, selectedGoal.key).then((response) => {
|
|
||||||
const { data } = response;
|
|
||||||
const {
|
|
||||||
header,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
setGoalToDisplay(selectedGoal);
|
|
||||||
setGoalToastHeader(header);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="mb-3" data-testid="course-goal-card">
|
|
||||||
<Card.Body>
|
|
||||||
<div className="row w-100 m-0 justify-content-between align-items-center">
|
|
||||||
<div className="col col-8 p-0">
|
|
||||||
<h2 className="h4 m-0">{intl.formatMessage(messages.welcomeTo)} {title}</h2>
|
|
||||||
</div>
|
|
||||||
<div className="col col-auto p-0">
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
className="p-0"
|
|
||||||
size="sm"
|
|
||||||
block
|
|
||||||
data-goal-key="unsure"
|
|
||||||
data-goal-text={`${intl.formatMessage(messages.goalUnsure)}`}
|
|
||||||
onClick={(event) => { selectGoalHandler(event); }}
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages.goalUnsure)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Card.Text className="my-2 mx-1 text-dark-500">{intl.formatMessage(messages.setGoal)}</Card.Text>
|
|
||||||
<div className="row w-100 m-0">
|
|
||||||
{goalOptions.map((goal) => {
|
|
||||||
const [goalKey, goalText] = goal;
|
|
||||||
return (
|
|
||||||
(goalKey !== 'unsure') && (
|
|
||||||
<div key={`goal-${goalKey}`} className="col-auto flex-grow-1 mx-1 my-2 p-0">
|
|
||||||
<Button
|
|
||||||
variant="outline-primary"
|
|
||||||
block
|
|
||||||
data-goal-key={goalKey}
|
|
||||||
data-goal-text={goalText}
|
|
||||||
onClick={(event) => { selectGoalHandler(event); }}
|
|
||||||
>
|
|
||||||
{goalText}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Card.Body>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CourseGoalCard.propTypes = {
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
goalOptions: PropTypes.arrayOf(
|
|
||||||
PropTypes.arrayOf(PropTypes.string),
|
|
||||||
).isRequired,
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
setGoalToDisplay: PropTypes.func.isRequired,
|
|
||||||
setGoalToastHeader: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CourseGoalCard);
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
@@ -7,7 +7,10 @@ import LmsHtmlFragment from '../LmsHtmlFragment';
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
function CourseHandouts({ courseId, intl }) {
|
function CourseHandouts({ intl }) {
|
||||||
|
const {
|
||||||
|
courseId,
|
||||||
|
} = useSelector(state => state.courseHome);
|
||||||
const {
|
const {
|
||||||
handoutsHtml,
|
handoutsHtml,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
@@ -29,7 +32,6 @@ function CourseHandouts({ courseId, intl }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CourseHandouts.propTypes = {
|
CourseHandouts.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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';
|
||||||
@@ -12,8 +12,12 @@ import { faNewspaper } from '@fortawesome/free-regular-svg-icons';
|
|||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton';
|
||||||
|
|
||||||
function CourseTools({ courseId, intl }) {
|
function CourseTools({ intl }) {
|
||||||
|
const {
|
||||||
|
courseId,
|
||||||
|
} = useSelector(state => state.courseHome);
|
||||||
const { org } = useModel('courseHomeMeta', courseId);
|
const { org } = useModel('courseHomeMeta', courseId);
|
||||||
const {
|
const {
|
||||||
courseTools,
|
courseTools,
|
||||||
@@ -69,18 +73,16 @@ function CourseTools({ courseId, intl }) {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
<li className="small" id="courseHome-launchTourLink">
|
||||||
|
<LaunchCourseHomeTourButton />
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CourseTools.propTypes = {
|
CourseTools.propTypes = {
|
||||||
courseId: PropTypes.string,
|
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseTools.defaultProps = {
|
|
||||||
courseId: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(CourseTools);
|
export default injectIntl(CourseTools);
|
||||||
|
|||||||
43
src/course-home/outline-tab/widgets/FlagButton.jsx
Normal file
43
src/course-home/outline-tab/widgets/FlagButton.jsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
function FlagButton({
|
||||||
|
buttonIcon,
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
handleSelect,
|
||||||
|
isSelected,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classnames('flag-button row w-100 align-content-between m-1.5 py-3.5',
|
||||||
|
isSelected ? 'flag-button-selected' : '')}
|
||||||
|
aria-checked={isSelected}
|
||||||
|
role="radio"
|
||||||
|
onClick={() => handleSelect()}
|
||||||
|
data-testid={`weekly-learning-goal-input-${title}`}
|
||||||
|
>
|
||||||
|
<div className="row w-100 m-0 justify-content-center pb-1">
|
||||||
|
{buttonIcon}
|
||||||
|
</div>
|
||||||
|
<div className={classnames('row w-100 m-0 justify-content-center small text-gray-700 pb-1', isSelected ? 'font-weight-bold' : '')}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className={classnames('row w-100 m-0 justify-content-center micro text-gray-500', isSelected ? 'font-weight-bold' : '')}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlagButton.propTypes = {
|
||||||
|
buttonIcon: PropTypes.element.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
handleSelect: PropTypes.func.isRequired,
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlagButton;
|
||||||
37
src/course-home/outline-tab/widgets/FlagButton.scss
Normal file
37
src/course-home/outline-tab/widgets/FlagButton.scss
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
@import "~@edx/brand/paragon/variables";
|
||||||
|
@import "~@edx/paragon/scss/core/core";
|
||||||
|
@import "~@edx/brand/paragon/overrides";
|
||||||
|
|
||||||
|
.flag-button {
|
||||||
|
background-color: $white;
|
||||||
|
border: 1px solid $light-400;
|
||||||
|
border-radius: .2rem;
|
||||||
|
box-shadow: 0 0 0 2px $light-400;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid $primary-300;
|
||||||
|
box-shadow: 0 0 0 2px $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-button-selected {
|
||||||
|
border: 1px solid $primary-300;
|
||||||
|
box-shadow: 0 0 0 2px $primary-300;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://heydonworks.com/article/the-flexbox-holy-albatross-reincarnated/
|
||||||
|
// use the container size for layout instead of device media query
|
||||||
|
.flag-button-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
--margin: 1rem;
|
||||||
|
--modifier: calc(20rem - 100%);
|
||||||
|
margin: calc(var(--margin) * -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-button-container > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: calc(var(--modifier) * 999);
|
||||||
|
margin: var(--margin);
|
||||||
|
}
|
||||||
59
src/course-home/outline-tab/widgets/LearningGoalButton.jsx
Normal file
59
src/course-home/outline-tab/widgets/LearningGoalButton.jsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
// These flag svgs are derivatives of the Flag icon from paragon
|
||||||
|
import { ReactComponent as FlagIntenseIcon } from './flag_black.svg';
|
||||||
|
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
|
||||||
|
import { ReactComponent as FlagRegularIcon } from './flag_gray.svg';
|
||||||
|
import FlagButton from './FlagButton';
|
||||||
|
import messages from '../messages';
|
||||||
|
|
||||||
|
function LearningGoalButton({
|
||||||
|
level,
|
||||||
|
isSelected,
|
||||||
|
handleSelect,
|
||||||
|
intl,
|
||||||
|
}) {
|
||||||
|
const buttonDetails = {
|
||||||
|
casual: {
|
||||||
|
daysPerWeek: 1,
|
||||||
|
title: messages.casualGoalButtonTitle,
|
||||||
|
text: messages.casualGoalButtonText,
|
||||||
|
icon: <FlagCasualIcon />,
|
||||||
|
},
|
||||||
|
regular: {
|
||||||
|
daysPerWeek: 3,
|
||||||
|
title: messages.regularGoalButtonTitle,
|
||||||
|
text: messages.regularGoalButtonText,
|
||||||
|
icon: <FlagRegularIcon />,
|
||||||
|
},
|
||||||
|
intense: {
|
||||||
|
daysPerWeek: 5,
|
||||||
|
title: messages.intenseGoalButtonTitle,
|
||||||
|
text: messages.intenseGoalButtonText,
|
||||||
|
icon: <FlagIntenseIcon />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const values = buttonDetails[level];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlagButton
|
||||||
|
buttonIcon={values.icon}
|
||||||
|
title={intl.formatMessage(values.title)}
|
||||||
|
text={intl.formatMessage(values.text)}
|
||||||
|
handleSelect={() => handleSelect(values.daysPerWeek)}
|
||||||
|
isSelected={isSelected}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LearningGoalButton.propTypes = {
|
||||||
|
level: PropTypes.string.isRequired,
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
handleSelect: PropTypes.func.isRequired,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(LearningGoalButton);
|
||||||
@@ -1,14 +1,24 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import camelCase from 'lodash.camelcase';
|
import camelCase from 'lodash.camelcase';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@edx/paragon';
|
import { Button } from '@edx/paragon';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { getProctoringInfoData } from '../../data/api';
|
import { getProctoringInfoData } from '../../data/api';
|
||||||
|
import { fetchProctoringInfoResolved } from '../../data/slice';
|
||||||
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
|
function ProctoringInfoPanel({ intl }) {
|
||||||
|
const {
|
||||||
|
courseId,
|
||||||
|
} = useSelector(state => state.courseHome);
|
||||||
|
const {
|
||||||
|
username,
|
||||||
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
function ProctoringInfoPanel({ courseId, username, intl }) {
|
|
||||||
const [link, setLink] = useState('');
|
const [link, setLink] = useState('');
|
||||||
const [onboardingPastDue, setOnboardingPastDue] = useState(false);
|
const [onboardingPastDue, setOnboardingPastDue] = useState(false);
|
||||||
const [showInfoPanel, setShowInfoPanel] = useState(false);
|
const [showInfoPanel, setShowInfoPanel] = useState(false);
|
||||||
@@ -25,6 +35,7 @@ function ProctoringInfoPanel({ courseId, username, intl }) {
|
|||||||
error: 'error',
|
error: 'error',
|
||||||
otherCourseApproved: 'otherCourseApproved',
|
otherCourseApproved: 'otherCourseApproved',
|
||||||
expiringSoon: 'expiringSoon',
|
expiringSoon: 'expiringSoon',
|
||||||
|
expired: 'expired',
|
||||||
};
|
};
|
||||||
|
|
||||||
function getReadableStatusClass(examStatus) {
|
function getReadableStatusClass(examStatus) {
|
||||||
@@ -44,9 +55,14 @@ function ProctoringInfoPanel({ courseId, username, intl }) {
|
|||||||
return readableClass;
|
return readableClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNotYetSubmitted(examStatus) {
|
function isCurrentlySubmitted(examStatus) {
|
||||||
const NO_SHOW_STATES = ['submitted', 'second_review_required', 'verified'];
|
const SUBMITTED_STATES = ['submitted', 'second_review_required'];
|
||||||
return !NO_SHOW_STATES.includes(examStatus);
|
return SUBMITTED_STATES.includes(examStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSubmissionRequired(examStatus) {
|
||||||
|
const OK_STATES = [readableStatuses.submitted, readableStatuses.verified];
|
||||||
|
return !OK_STATES.includes(examStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNotYetReleased(examReleaseDate) {
|
function isNotYetReleased(examReleaseDate) {
|
||||||
@@ -67,11 +83,19 @@ function ProctoringInfoPanel({ courseId, username, intl }) {
|
|||||||
return borderClass;
|
return borderClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isExpiringSoon(dateString) {
|
function isExpired(dateString) {
|
||||||
// Returns true if the expiration date is within 28 days
|
// Returns true if the expiration date has passed
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const expirationDateObject = new Date(dateString);
|
const expirationDateObject = new Date(dateString);
|
||||||
return today > expirationDateObject.getTime() - 2419200000;
|
return today >= expirationDateObject.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpiringSoon(dateString) {
|
||||||
|
// Returns true if the expiration date is within 28 days
|
||||||
|
const twentyeightDays = 28 * 24 * 60 * 60 * 1000;
|
||||||
|
const today = new Date();
|
||||||
|
const expirationDateObject = new Date(dateString);
|
||||||
|
return today > expirationDateObject.getTime() - twentyeightDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -86,7 +110,9 @@ function ProctoringInfoPanel({ courseId, username, intl }) {
|
|||||||
setStatus(response.onboarding_status);
|
setStatus(response.onboarding_status);
|
||||||
setLink(response.onboarding_link);
|
setLink(response.onboarding_link);
|
||||||
const expirationDate = response.expiration_date;
|
const expirationDate = response.expiration_date;
|
||||||
if (expirationDate && isExpiringSoon(expirationDate)) {
|
if (expirationDate && isExpired(expirationDate)) {
|
||||||
|
setReadableStatus(getReadableStatusClass('expired'));
|
||||||
|
} else if (expirationDate && isExpiringSoon(expirationDate)) {
|
||||||
setReadableStatus(getReadableStatusClass('expiringSoon'));
|
setReadableStatus(getReadableStatusClass('expiringSoon'));
|
||||||
} else {
|
} else {
|
||||||
setReadableStatus(getReadableStatusClass(response.onboarding_status));
|
setReadableStatus(getReadableStatusClass(response.onboarding_status));
|
||||||
@@ -95,7 +121,13 @@ function ProctoringInfoPanel({ courseId, username, intl }) {
|
|||||||
setOnboardingPastDue(response.onboarding_past_due);
|
setOnboardingPastDue(response.onboarding_past_due);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
.catch(() => {
|
||||||
|
/* Do nothing. API throws 404 when class does not have proctoring */
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
dispatch(fetchProctoringInfoResolved());
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let onboardingExamButton = null;
|
let onboardingExamButton = null;
|
||||||
@@ -159,17 +191,17 @@ function ProctoringInfoPanel({ courseId, username, intl }) {
|
|||||||
{![readableStatuses.verified, readableStatuses.otherCourseApproved].includes(readableStatus) && (
|
{![readableStatuses.verified, readableStatuses.otherCourseApproved].includes(readableStatus) && (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>
|
||||||
{isNotYetSubmitted(status) && (
|
{!isCurrentlySubmitted(status) && (
|
||||||
intl.formatMessage(messages.proctoringPanelGeneralInfo)
|
intl.formatMessage(messages.proctoringPanelGeneralInfo)
|
||||||
)}
|
)}
|
||||||
{!isNotYetSubmitted(status) && (
|
{isCurrentlySubmitted(status) && (
|
||||||
intl.formatMessage(messages.proctoringPanelGeneralInfoSubmitted)
|
intl.formatMessage(messages.proctoringPanelGeneralInfoSubmitted)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p>{intl.formatMessage(messages.proctoringPanelGeneralTime)}</p>
|
<p>{intl.formatMessage(messages.proctoringPanelGeneralTime)}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isNotYetSubmitted(status) && (
|
{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="https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams">
|
||||||
@@ -183,13 +215,7 @@ function ProctoringInfoPanel({ courseId, username, intl }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProctoringInfoPanel.propTypes = {
|
ProctoringInfoPanel.propTypes = {
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
username: PropTypes.string,
|
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
ProctoringInfoPanel.defaultProps = {
|
|
||||||
username: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(ProctoringInfoPanel);
|
export default injectIntl(ProctoringInfoPanel);
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Card } from '@edx/paragon';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||||
|
import messages from '../messages';
|
||||||
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
|
function StartOrResumeCourseCard({ intl }) {
|
||||||
|
const {
|
||||||
|
courseId,
|
||||||
|
} = useSelector(state => state.courseHome);
|
||||||
|
|
||||||
|
const {
|
||||||
|
org,
|
||||||
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
|
const eventProperties = {
|
||||||
|
org_key: org,
|
||||||
|
courserun_key: courseId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
resumeCourse: {
|
||||||
|
hasVisitedCourse,
|
||||||
|
url: resumeCourseUrl,
|
||||||
|
},
|
||||||
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
|
if (!resumeCourseUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logResumeCourseClick = () => {
|
||||||
|
sendTrackingLogEvent('edx.course.home.resume_course.clicked', {
|
||||||
|
...eventProperties,
|
||||||
|
event_type: hasVisitedCourse ? 'resume' : 'start',
|
||||||
|
url: resumeCourseUrl,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="mb-3 raised-card" data-testid="start-resume-card">
|
||||||
|
<Card.Header
|
||||||
|
title={hasVisitedCourse ? intl.formatMessage(messages.resumeBlurb) : intl.formatMessage(messages.startBlurb)}
|
||||||
|
actions={(
|
||||||
|
<Button
|
||||||
|
variant="brand"
|
||||||
|
block
|
||||||
|
href={resumeCourseUrl}
|
||||||
|
onClick={() => logResumeCourseClick()}
|
||||||
|
>
|
||||||
|
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* Footer is needed for internal vertical spacing to work out. If you can remove, be my guest */}
|
||||||
|
<Card.Footer><></></Card.Footer>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StartOrResumeCourseCard.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(StartOrResumeCourseCard);
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Dropdown } from '@edx/paragon';
|
|
||||||
|
|
||||||
import messages from '../messages';
|
|
||||||
import { saveCourseGoal } from '../../data';
|
|
||||||
|
|
||||||
function UpdateGoalSelector({
|
|
||||||
courseId,
|
|
||||||
goalOptions,
|
|
||||||
intl,
|
|
||||||
selectedGoal,
|
|
||||||
setGoalToDisplay,
|
|
||||||
setGoalToastHeader,
|
|
||||||
}) {
|
|
||||||
function selectGoalHandler(event) {
|
|
||||||
const key = event.currentTarget.id;
|
|
||||||
const text = event.currentTarget.innerText;
|
|
||||||
const newGoal = {
|
|
||||||
key,
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
|
|
||||||
setGoalToDisplay(newGoal);
|
|
||||||
saveCourseGoal(courseId, key).then((response) => {
|
|
||||||
const { data } = response;
|
|
||||||
const {
|
|
||||||
header,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
setGoalToastHeader(header);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section className="mb-4">
|
|
||||||
<div className="row w-100 m-0">
|
|
||||||
<div className="col-12 p-0">
|
|
||||||
<label className="h4 m-0" htmlFor="edit-goal-selector">
|
|
||||||
{intl.formatMessage(messages.goal)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 p-0">
|
|
||||||
<Dropdown className="py-2">
|
|
||||||
<Dropdown.Toggle variant="outline-primary" block id="edit-goal-selector" data-testid="edit-goal-selector">
|
|
||||||
{selectedGoal.text}
|
|
||||||
</Dropdown.Toggle>
|
|
||||||
<Dropdown.Menu>
|
|
||||||
{goalOptions.map(([goalKey, goalText]) => (
|
|
||||||
<Dropdown.Item
|
|
||||||
id={goalKey}
|
|
||||||
key={goalKey}
|
|
||||||
onClick={(event) => { selectGoalHandler(event); }}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
{goalText}
|
|
||||||
</Dropdown.Item>
|
|
||||||
))}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateGoalSelector.propTypes = {
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
goalOptions: PropTypes.arrayOf(
|
|
||||||
PropTypes.arrayOf(PropTypes.string),
|
|
||||||
).isRequired,
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
selectedGoal: PropTypes.shape({
|
|
||||||
key: PropTypes.string,
|
|
||||||
text: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
setGoalToDisplay: PropTypes.func.isRequired,
|
|
||||||
setGoalToastHeader: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(UpdateGoalSelector);
|
|
||||||
161
src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx
Normal file
161
src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Form, Card, Icon } from '@edx/paragon';
|
||||||
|
import { history } from '@edx/frontend-platform';
|
||||||
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Email } from '@edx/paragon/icons';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import messages from '../messages';
|
||||||
|
import LearningGoalButton from './LearningGoalButton';
|
||||||
|
import { saveWeeklyLearningGoal } from '../../data';
|
||||||
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
import './FlagButton.scss';
|
||||||
|
|
||||||
|
function WeeklyLearningGoalCard({
|
||||||
|
daysPerWeek,
|
||||||
|
subscribedToReminders,
|
||||||
|
intl,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
courseId,
|
||||||
|
} = useSelector(state => state.courseHome);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isMasquerading,
|
||||||
|
org,
|
||||||
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
|
const { administrator } = getAuthenticatedUser();
|
||||||
|
|
||||||
|
const [daysPerWeekGoal, setDaysPerWeekGoal] = useState(daysPerWeek);
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
const [isGetReminderSelected, setGetReminderSelected] = useState(subscribedToReminders);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
function handleSelect(days, triggeredFromEmail = false) {
|
||||||
|
// Set the subscription button if this is the first time selecting a goal
|
||||||
|
const selectReminders = daysPerWeekGoal === null ? true : isGetReminderSelected;
|
||||||
|
setGetReminderSelected(selectReminders);
|
||||||
|
setDaysPerWeekGoal(days);
|
||||||
|
if (!isMasquerading) { // don't save goal updates while masquerading
|
||||||
|
saveWeeklyLearningGoal(courseId, days, selectReminders);
|
||||||
|
sendTrackEvent('edx.ui.lms.goal.days-per-week.changed', {
|
||||||
|
org_key: org,
|
||||||
|
courserun_key: courseId,
|
||||||
|
is_staff: administrator,
|
||||||
|
num_days: days,
|
||||||
|
reminder_selected: selectReminders,
|
||||||
|
});
|
||||||
|
if (triggeredFromEmail) {
|
||||||
|
sendTrackEvent('enrollment.email.clicked.setgoal', {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubscribeToReminders(event) {
|
||||||
|
const isGetReminderChecked = event.target.checked;
|
||||||
|
setGetReminderSelected(isGetReminderChecked);
|
||||||
|
if (!isMasquerading) { // don't save goal updates while masquerading
|
||||||
|
saveWeeklyLearningGoal(courseId, daysPerWeekGoal, isGetReminderChecked);
|
||||||
|
sendTrackEvent('edx.ui.lms.goal.reminder-selected.changed', {
|
||||||
|
org_key: org,
|
||||||
|
courserun_key: courseId,
|
||||||
|
is_staff: administrator,
|
||||||
|
num_days: daysPerWeekGoal,
|
||||||
|
reminder_selected: isGetReminderChecked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentParams = new URLSearchParams(location.search);
|
||||||
|
const weeklyGoal = Number(currentParams.get('weekly_goal'));
|
||||||
|
if ([1, 3, 5].includes(weeklyGoal)) {
|
||||||
|
handleSelect(weeklyGoal, true);
|
||||||
|
|
||||||
|
// Deleting the weekly_goal query param as it only needs to be set once
|
||||||
|
// whenever passed in query params.
|
||||||
|
currentParams.delete('weekly_goal');
|
||||||
|
history.replace({
|
||||||
|
search: currentParams.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
id="courseHome-weeklyLearningGoal"
|
||||||
|
className="row w-100 m-0 mb-3 raised-card"
|
||||||
|
data-testid="weekly-learning-goal-card"
|
||||||
|
>
|
||||||
|
<Card.Header
|
||||||
|
size="sm"
|
||||||
|
title={(<div id="set-weekly-goal-header">{intl.formatMessage(messages.setWeeklyGoal)}</div>)}
|
||||||
|
subtitle={intl.formatMessage(messages.setWeeklyGoalDetail)}
|
||||||
|
/>
|
||||||
|
<Card.Section className="text-gray-700 small">
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby="set-weekly-goal-header"
|
||||||
|
className="flag-button-container m-0 p-0"
|
||||||
|
>
|
||||||
|
<LearningGoalButton
|
||||||
|
level="casual"
|
||||||
|
isSelected={daysPerWeekGoal === 1}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
/>
|
||||||
|
<LearningGoalButton
|
||||||
|
level="regular"
|
||||||
|
isSelected={daysPerWeekGoal === 3}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
/>
|
||||||
|
<LearningGoalButton
|
||||||
|
level="intense"
|
||||||
|
isSelected={daysPerWeekGoal === 5}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex pt-3">
|
||||||
|
<Form.Switch
|
||||||
|
checked={isGetReminderSelected}
|
||||||
|
onChange={(event) => handleSubscribeToReminders(event)}
|
||||||
|
disabled={!daysPerWeekGoal}
|
||||||
|
>
|
||||||
|
<small>{intl.formatMessage(messages.setGoalReminder)}</small>
|
||||||
|
</Form.Switch>
|
||||||
|
</div>
|
||||||
|
</Card.Section>
|
||||||
|
{isGetReminderSelected && (
|
||||||
|
<Card.Section muted>
|
||||||
|
<div className="row w-100 m-0 small align-center">
|
||||||
|
<div className="d-flex align-items-center pr-1">
|
||||||
|
<Icon
|
||||||
|
className="text-primary-500"
|
||||||
|
src={Email}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col">
|
||||||
|
{intl.formatMessage(messages.goalReminderDetail)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Section>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WeeklyLearningGoalCard.propTypes = {
|
||||||
|
daysPerWeek: PropTypes.number,
|
||||||
|
subscribedToReminders: PropTypes.bool,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
WeeklyLearningGoalCard.defaultProps = {
|
||||||
|
daysPerWeek: null,
|
||||||
|
subscribedToReminders: false,
|
||||||
|
};
|
||||||
|
export default injectIntl(WeeklyLearningGoalCard);
|
||||||
@@ -37,6 +37,7 @@ function WelcomeMessage({ courseId, intl }) {
|
|||||||
setDisplay(false);
|
setDisplay(false);
|
||||||
dispatch(dismissWelcomeMessage(courseId));
|
dispatch(dismissWelcomeMessage(courseId));
|
||||||
}}
|
}}
|
||||||
|
className="raised-card"
|
||||||
actions={messageCanBeShortened ? [
|
actions={messageCanBeShortened ? [
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowShortMessage(!showShortMessage)}
|
onClick={() => setShowShortMessage(!showShortMessage)}
|
||||||
|
|||||||
3
src/course-home/outline-tab/widgets/flag_black.svg
Normal file
3
src/course-home/outline-tab/widgets/flag_black.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.4 6L14 4H5V21H7V14H12.6L13 16H20V6H14.4Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 173 B |
18
src/course-home/outline-tab/widgets/flag_gray.svg
Normal file
18
src/course-home/outline-tab/widgets/flag_gray.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 15 17"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg11"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M9.4 2L9 0H0V17H2V10H7.6L8 12H15V2H9.4ZM13 10H9.64L9.24 8H2V2H7.36L7.76 4H13V10Z"
|
||||||
|
fill="#002B2B"
|
||||||
|
id="path9" />
|
||||||
|
<path
|
||||||
|
style="fill:#808080;fill-rule:evenodd;stroke-width:0.0150977"
|
||||||
|
d="M 9.6594698,9.9871226 C 9.6577909,9.9829707 9.5654776,9.5311723 9.4543296,8.9831261 L 9.2522415,7.9866785 5.6376662,7.9790074 2.0230906,7.9713362 V 4.9970494 2.0227625 l 2.6636151,0.00771 2.6636151,0.00771 0.1968204,0.9888987 0.1968205,0.9888988 h 2.6200263 2.620026 v 2.9893428 2.9893428 h -1.660746 c -0.91341,0 -1.6621194,-0.0034 -1.6637982,-0.00755 z"
|
||||||
|
id="path302" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 801 B |
3
src/course-home/outline-tab/widgets/flag_outline.svg
Normal file
3
src/course-home/outline-tab/widgets/flag_outline.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="15" height="17" viewBox="0 0 15 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.4 2L9 0H0V17H2V10H7.6L8 12H15V2H9.4ZM13 10H9.64L9.24 8H2V2H7.36L7.76 4H13V10Z" fill="#002B2B"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 211 B |
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { layoutGenerator } from 'react-break';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||||
|
|
||||||
import CertificateStatus from './certificate-status/CertificateStatus';
|
import CertificateStatus from './certificate-status/CertificateStatus';
|
||||||
import CourseCompletion from './course-completion/CourseCompletion';
|
import CourseCompletion from './course-completion/CourseCompletion';
|
||||||
@@ -23,13 +23,15 @@ function ProgressTab() {
|
|||||||
|
|
||||||
const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
||||||
|
|
||||||
const layout = layoutGenerator({
|
const windowWidth = useWindowSize().width;
|
||||||
mobile: 0,
|
if (windowWidth === undefined) {
|
||||||
desktop: 992,
|
// Bail because we don't want to load <CertificateStatus/> twice, emitting 'visited' events both times.
|
||||||
});
|
// This is a hacky solution, since the user can resize the screen and still get two visited events.
|
||||||
|
// But I'm leaving a larger refactor as an exercise to a future reader.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const OnMobile = layout.is('mobile');
|
const wideScreen = windowWidth >= breakpoints.large.minWidth;
|
||||||
const OnDesktop = layout.isAtLeast('desktop');
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProgressHeader />
|
<ProgressHeader />
|
||||||
@@ -37,11 +39,9 @@ function ProgressTab() {
|
|||||||
{/* Main body */}
|
{/* Main body */}
|
||||||
<div className="col-12 col-md-8 p-0">
|
<div className="col-12 col-md-8 p-0">
|
||||||
<CourseCompletion />
|
<CourseCompletion />
|
||||||
<OnMobile>
|
{!wideScreen && <CertificateStatus />}
|
||||||
<CertificateStatus />
|
|
||||||
</OnMobile>
|
|
||||||
<CourseGrade />
|
<CourseGrade />
|
||||||
<div className={`grades my-4 p-4 rounded shadow-sm ${applyLockedOverlay}`} aria-hidden={gradesFeatureIsFullyLocked}>
|
<div className={`grades my-4 p-4 rounded raised-card ${applyLockedOverlay}`} aria-hidden={gradesFeatureIsFullyLocked}>
|
||||||
<GradeSummary />
|
<GradeSummary />
|
||||||
<DetailedGrades />
|
<DetailedGrades />
|
||||||
</div>
|
</div>
|
||||||
@@ -49,9 +49,7 @@ function ProgressTab() {
|
|||||||
|
|
||||||
{/* 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">
|
||||||
<OnDesktop>
|
{wideScreen && <CertificateStatus />}
|
||||||
<CertificateStatus />
|
|
||||||
</OnDesktop>
|
|
||||||
<RelatedLinks />
|
<RelatedLinks />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Factory } from 'rosie';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } 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 '@edx/paragon';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +14,7 @@ import * as thunks from '../data/thunks';
|
|||||||
import initializeStore from '../../store';
|
import initializeStore from '../../store';
|
||||||
import ProgressTab from './ProgressTab';
|
import ProgressTab from './ProgressTab';
|
||||||
import LoadedTabPage from '../../tab-page/LoadedTabPage';
|
import LoadedTabPage from '../../tab-page/LoadedTabPage';
|
||||||
|
import messages from './grades/messages';
|
||||||
|
|
||||||
initializeMockApp();
|
initializeMockApp();
|
||||||
jest.mock('@edx/frontend-platform/analytics');
|
jest.mock('@edx/frontend-platform/analytics');
|
||||||
@@ -20,18 +22,21 @@ jest.mock('@edx/frontend-platform/analytics');
|
|||||||
describe('Progress Tab', () => {
|
describe('Progress Tab', () => {
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
|
|
||||||
const courseId = 'course-v1:edX+Test+run';
|
const store = initializeStore();
|
||||||
|
const defaultMetadata = Factory.build('courseHomeMetadata');
|
||||||
|
const defaultTabData = Factory.build('progressTabData');
|
||||||
|
|
||||||
|
const courseId = defaultMetadata.id;
|
||||||
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`;
|
||||||
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
|
||||||
const progressUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/progress/*`);
|
const progressUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/progress/*`);
|
||||||
const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`;
|
const masqueradeUrl = `${getConfig().LMS_BASE_URL}/courses/${courseId}/masquerade`;
|
||||||
|
const now = new Date();
|
||||||
const store = initializeStore();
|
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||||
const defaultMetadata = Factory.build('courseHomeMetadata', { id: courseId });
|
const overmorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2);
|
||||||
const defaultTabData = Factory.build('progressTabData');
|
|
||||||
|
|
||||||
function setMetadata(attributes, options) {
|
function setMetadata(attributes, options) {
|
||||||
const courseMetadata = Factory.build('courseHomeMetadata', { id: courseId, ...attributes }, options);
|
const courseMetadata = Factory.build('courseHomeMetadata', attributes, options);
|
||||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +68,7 @@ describe('Progress Tab', () => {
|
|||||||
|
|
||||||
it('sends event on click of dates tab link', async () => {
|
it('sends event on click of dates tab link', async () => {
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
|
sendTrackEvent.mockClear();
|
||||||
|
|
||||||
const datesTabLink = screen.getByRole('link', { name: 'Dates' });
|
const datesTabLink = screen.getByRole('link', { name: 'Dates' });
|
||||||
fireEvent.click(datesTabLink);
|
fireEvent.click(datesTabLink);
|
||||||
@@ -78,6 +84,7 @@ describe('Progress Tab', () => {
|
|||||||
|
|
||||||
it('sends event on click of outline tab link', async () => {
|
it('sends event on click of outline tab link', async () => {
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
|
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
|
||||||
@@ -129,6 +136,8 @@ describe('Progress Tab', () => {
|
|||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.queryByRole('button', { name: 'Grade range tooltip' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('button', { name: 'Grade range tooltip' })).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('currentGradeTooltipContent').innerHTML).toEqual('50%');
|
||||||
|
expect(screen.getByTestId('gradeSummaryFooterTotalWeightedGrade').innerHTML).toEqual('50%');
|
||||||
expect(screen.getByText('A weighted grade of 75% is required to pass in this course')).toBeInTheDocument();
|
expect(screen.getByText('A weighted grade of 75% is required to pass in this course')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,6 +171,8 @@ describe('Progress Tab', () => {
|
|||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByRole('button', { name: 'Grade range tooltip' }));
|
expect(screen.getByRole('button', { name: 'Grade range tooltip' }));
|
||||||
|
expect(screen.getByTestId('currentGradeTooltipContent').innerHTML).toEqual('0%');
|
||||||
|
expect(screen.getByTestId('gradeSummaryFooterTotalWeightedGrade').innerHTML).toEqual('0%');
|
||||||
expect(screen.getByText('A weighted grade of 80% is required to pass in this course')).toBeInTheDocument();
|
expect(screen.getByText('A weighted grade of 80% is required to pass in this course')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,6 +220,8 @@ describe('Progress Tab', () => {
|
|||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByRole('button', { name: 'Grade range tooltip' }));
|
expect(screen.getByRole('button', { name: 'Grade range tooltip' }));
|
||||||
|
expect(screen.getByTestId('currentGradeTooltipContent').innerHTML).toEqual('80%');
|
||||||
|
expect(screen.getByTestId('gradeSummaryFooterTotalWeightedGrade').innerHTML).toEqual('80%');
|
||||||
expect(await screen.findByText('You’re currently passing this course with a grade of B (80-90%)')).toBeInTheDocument();
|
expect(await screen.findByText('You’re currently passing this course with a grade of B (80-90%)')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -285,7 +298,6 @@ describe('Progress Tab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sends events on click of upgrade button in locked content header (CourseGradeHeader)', async () => {
|
it('sends events on click of upgrade button in locked content header (CourseGradeHeader)', async () => {
|
||||||
sendTrackEvent.mockClear();
|
|
||||||
setTabData({
|
setTabData({
|
||||||
completion_summary: {
|
completion_summary: {
|
||||||
complete_count: 1,
|
complete_count: 1,
|
||||||
@@ -322,6 +334,7 @@ describe('Progress Tab', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
|
sendTrackEvent.mockClear();
|
||||||
expect(screen.getByText('locked feature')).toBeInTheDocument();
|
expect(screen.getByText('locked feature')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Unlock to view grades and work towards a certificate.')).toBeInTheDocument();
|
expect(screen.getByText('Unlock to view grades and work towards a certificate.')).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -438,9 +451,8 @@ describe('Progress Tab', () => {
|
|||||||
expect(screen.queryAllByTestId('blocked-icon')).toHaveLength(4);
|
expect(screen.queryAllByTestId('blocked-icon')).toHaveLength(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correct current grade tooltip when showGrades is false', async () => {
|
it('does not render subsections for which showGrades is false', async () => {
|
||||||
// The learner has a 50% on the first assignment and a 100% on the second, making their grade a 75%
|
// The second assignment has showGrades set to false, so it should not be shown.
|
||||||
// The second assignment has showGrades set to false, so the grade reflected to the learner should be 50%.
|
|
||||||
setTabData({
|
setTabData({
|
||||||
section_scores: [
|
section_scores: [
|
||||||
{
|
{
|
||||||
@@ -482,10 +494,31 @@ describe('Progress Tab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByTestId('currentGradeTooltipContent').innerHTML).toEqual('50%');
|
expect(screen.getByText('First subsection')).toBeInTheDocument();
|
||||||
// Although the learner's true grade is passing, we should expect this to reflect the grade that's
|
expect(screen.queryByText('Second subsection')).not.toBeInTheDocument();
|
||||||
// visible to them, which is non-passing
|
});
|
||||||
expect(screen.getByText('A weighted grade of 75% is required to pass in this course')).toBeInTheDocument();
|
|
||||||
|
it('renders correct title when credit information is available', async () => {
|
||||||
|
setTabData({
|
||||||
|
credit_course_requirements: {
|
||||||
|
eligibility_status: 'eligible',
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
namespace: 'proctored_exam',
|
||||||
|
name: 'i4x://edX/DemoX/proctoring-block/final_uuid',
|
||||||
|
display_name: 'Proctored Mid Term Exam',
|
||||||
|
criteria: {},
|
||||||
|
reason: {},
|
||||||
|
status: 'satisfied',
|
||||||
|
status_date: '2015-06-26 11:07:42',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText('Grades & Credit')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -634,9 +667,7 @@ describe('Progress Tab', () => {
|
|||||||
expect(screen.getByRole('row', { name: 'Exam 50% 0% 0%' })).toBeInTheDocument();
|
expect(screen.getByRole('row', { name: 'Exam 50% 0% 0%' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correct total weighted grade when showGrades is false', async () => {
|
it('renders override notice', async () => {
|
||||||
// The learner has a 50% on the first assignment and a 100% on the second, making their grade a 75%
|
|
||||||
// The second assignment has showGrades set to false, so the grade reflected to the learner should be 50%.
|
|
||||||
setTabData({
|
setTabData({
|
||||||
section_scores: [
|
section_scores: [
|
||||||
{
|
{
|
||||||
@@ -647,12 +678,16 @@ describe('Progress Tab', () => {
|
|||||||
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||||
display_name: 'First subsection',
|
display_name: 'First subsection',
|
||||||
has_graded_assignment: true,
|
has_graded_assignment: true,
|
||||||
|
learner_has_access: true,
|
||||||
num_points_earned: 1,
|
num_points_earned: 1,
|
||||||
num_points_possible: 2,
|
num_points_possible: 2,
|
||||||
percent_graded: 1.0,
|
percent_graded: 1.0,
|
||||||
|
problem_scores: [{
|
||||||
|
earned: 1,
|
||||||
|
possible: 2,
|
||||||
|
}],
|
||||||
show_correctness: 'always',
|
show_correctness: 'always',
|
||||||
show_grades: true,
|
show_grades: true,
|
||||||
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -660,15 +695,24 @@ describe('Progress Tab', () => {
|
|||||||
display_name: 'Second section',
|
display_name: 'Second section',
|
||||||
subsections: [
|
subsections: [
|
||||||
{
|
{
|
||||||
assignment_type: 'Homework',
|
assignment_type: 'Exam',
|
||||||
|
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@98765',
|
||||||
display_name: 'Second subsection',
|
display_name: 'Second subsection',
|
||||||
learner_has_access: true,
|
learner_has_access: true,
|
||||||
has_graded_assignment: true,
|
has_graded_assignment: true,
|
||||||
num_points_earned: 1,
|
num_points_earned: 0,
|
||||||
num_points_possible: 1,
|
num_points_possible: 1,
|
||||||
|
override: {
|
||||||
|
system: 'PROCTORING',
|
||||||
|
reason: 'Suspicious activity',
|
||||||
|
},
|
||||||
percent_graded: 1.0,
|
percent_graded: 1.0,
|
||||||
|
problem_scores: [{
|
||||||
|
earned: 0,
|
||||||
|
possible: 1,
|
||||||
|
}],
|
||||||
show_correctness: 'always',
|
show_correctness: 'always',
|
||||||
show_grades: false,
|
show_grades: true,
|
||||||
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection',
|
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -677,7 +721,14 @@ describe('Progress Tab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.getByTestId('gradeSummaryFooterTotalWeightedGrade').innerHTML).toEqual('50%');
|
|
||||||
|
const problemScoreDrawerToggle = screen.getByRole('button', { name: 'Toggle individual problem scores for Second subsection' });
|
||||||
|
expect(problemScoreDrawerToggle).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Open the problem score drawer
|
||||||
|
fireEvent.click(problemScoreDrawerToggle);
|
||||||
|
|
||||||
|
expect(screen.getByText(messages.sectionGradeOverridden.defaultMessage)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -691,8 +742,8 @@ describe('Progress Tab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sends event on click of subsection link', async () => {
|
it('sends event on click of subsection link', async () => {
|
||||||
sendTrackEvent.mockClear();
|
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
|
sendTrackEvent.mockClear();
|
||||||
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
expect(screen.getByText('Detailed grades')).toBeInTheDocument();
|
||||||
|
|
||||||
const subsectionLink = screen.getByRole('link', { name: 'First subsection' });
|
const subsectionLink = screen.getByRole('link', { name: 'First subsection' });
|
||||||
@@ -708,8 +759,8 @@ describe('Progress Tab', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sends event on click of course outline link', async () => {
|
it('sends event on click of course outline link', async () => {
|
||||||
sendTrackEvent.mockClear();
|
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
|
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];
|
||||||
@@ -749,22 +800,7 @@ describe('Progress Tab', () => {
|
|||||||
|
|
||||||
describe('Certificate Status', () => {
|
describe('Certificate Status', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
global.innerWidth = breakpoints.large.minWidth;
|
||||||
writable: true,
|
|
||||||
value: jest.fn().mockImplementation(query => {
|
|
||||||
const matches = !!(query === 'screen and (min-width: 992px)');
|
|
||||||
return {
|
|
||||||
matches,
|
|
||||||
media: query,
|
|
||||||
onchange: null,
|
|
||||||
addListener: jest.fn(), // deprecated
|
|
||||||
removeListener: jest.fn(), // deprecated
|
|
||||||
addEventListener: jest.fn(),
|
|
||||||
removeEventListener: jest.fn(),
|
|
||||||
dispatchEvent: jest.fn(),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('enrolled user', () => {
|
describe('enrolled user', () => {
|
||||||
@@ -923,49 +959,6 @@ describe('Progress Tab', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Displays download link', async () => {
|
|
||||||
setTabData({
|
|
||||||
certificate_data: {
|
|
||||||
cert_status: 'downloadable',
|
|
||||||
download_url: 'fake.download.url',
|
|
||||||
},
|
|
||||||
user_has_passing_grade: true,
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(screen.getByRole('link', { name: 'Download my certificate' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sends events on view of progress tab and on click of downloadable certificate link', async () => {
|
|
||||||
setTabData({
|
|
||||||
certificate_data: {
|
|
||||||
cert_status: 'downloadable',
|
|
||||||
download_url: 'fake.download.url',
|
|
||||||
},
|
|
||||||
user_has_passing_grade: true,
|
|
||||||
});
|
|
||||||
await fetchAndRender();
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.course_progress.visited', {
|
|
||||||
org_key: 'edX',
|
|
||||||
courserun_key: courseId,
|
|
||||||
is_staff: false,
|
|
||||||
track_variant: 'audit',
|
|
||||||
grade_variant: 'passing',
|
|
||||||
certificate_status_variant: 'earned_downloadable',
|
|
||||||
});
|
|
||||||
|
|
||||||
const downloadCertificateLink = screen.getByRole('link', { name: 'Download my certificate' });
|
|
||||||
fireEvent.click(downloadCertificateLink);
|
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
|
|
||||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.ui.lms.course_progress.certificate_status.clicked', {
|
|
||||||
org_key: 'edX',
|
|
||||||
courserun_key: courseId,
|
|
||||||
is_staff: false,
|
|
||||||
certificate_status_variant: 'earned_downloadable',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Displays webview link', async () => {
|
it('Displays webview link', async () => {
|
||||||
setTabData({
|
setTabData({
|
||||||
certificate_data: {
|
certificate_data: {
|
||||||
@@ -1187,6 +1180,114 @@ describe('Progress Tab', () => {
|
|||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
expect(screen.queryByTestId('certificate-status-component')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('certificate-status-component')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Shows not available messaging before certificates are available to nonpassing learners when theres no certificate data', async () => {
|
||||||
|
setMetadata({
|
||||||
|
can_view_certificate: false,
|
||||||
|
is_enrolled: true,
|
||||||
|
});
|
||||||
|
setTabData({
|
||||||
|
end: tomorrow.toISOString(),
|
||||||
|
certificate_data: undefined,
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText(`Final grades and any earned certificates are scheduled to be available after ${tomorrow.toLocaleDateString('en-us', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}.`)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shows not available messaging before certificates are available to passing learners when theres no certificate data', async () => {
|
||||||
|
setMetadata({
|
||||||
|
can_view_certificate: false,
|
||||||
|
is_enrolled: true,
|
||||||
|
});
|
||||||
|
setTabData({
|
||||||
|
end: tomorrow.toISOString(),
|
||||||
|
user_has_passing_grade: true,
|
||||||
|
certificate_data: undefined,
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText(`Final grades and any earned certificates are scheduled to be available after ${tomorrow.toLocaleDateString('en-us', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})}.`)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shows certificate_available_date if learner is passing', async () => {
|
||||||
|
setMetadata({
|
||||||
|
can_view_certificate: false,
|
||||||
|
is_enrolled: true,
|
||||||
|
});
|
||||||
|
setTabData({
|
||||||
|
end: tomorrow.toISOString(),
|
||||||
|
user_has_passing_grade: true,
|
||||||
|
certificate_data: {
|
||||||
|
cert_status: 'earned_but_not_available',
|
||||||
|
certificate_available_date: overmorrow.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText('Certificate status'));
|
||||||
|
expect(screen.getByText(
|
||||||
|
overmorrow.toLocaleDateString('en-us', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
}),
|
||||||
|
{ exact: false },
|
||||||
|
)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Credit Information', () => {
|
||||||
|
it('renders credit information when provided', async () => {
|
||||||
|
setTabData({
|
||||||
|
credit_course_requirements: {
|
||||||
|
eligibility_status: 'eligible',
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
namespace: 'proctored_exam',
|
||||||
|
name: 'i4x://edX/DemoX/proctoring-block/final_uuid',
|
||||||
|
display_name: 'Proctored Mid Term Exam',
|
||||||
|
criteria: {},
|
||||||
|
reason: {},
|
||||||
|
status: null,
|
||||||
|
status_date: '2015-06-26 11:07:42',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
namespace: 'grade',
|
||||||
|
name: 'i4x://edX/DemoX/proctoring-block/final_uuid',
|
||||||
|
display_name: 'Minimum Passing Grade',
|
||||||
|
criteria: { min_grade: 0.8 },
|
||||||
|
reason: { final_grade: 0.95 },
|
||||||
|
status: 'satisfied',
|
||||||
|
status_date: '2015-06-26 11:07:44',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.getByText('Grades & Credit')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Requirements for course credit')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('You have met the requirements for credit in this course.', { exact: false })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Proctored Mid Term Exam:')).toBeInTheDocument();
|
||||||
|
// 80% comes from the criteria.minGrade being 0.8
|
||||||
|
expect(screen.getByText('Minimum grade for credit (80%):')).toBeInTheDocument();
|
||||||
|
// Completed because the grade requirement has been satisfied
|
||||||
|
expect(screen.getByText('Completed')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render credit information when it is not provided', async () => {
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.queryByText('Grades & Credit')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Requirements for course credit.')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Access expiration masquerade banner', () => {
|
describe('Access expiration masquerade banner', () => {
|
||||||
@@ -1202,7 +1303,7 @@ describe('Progress Tab', () => {
|
|||||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||||
expect(screen.getByTestId('instructor-toolbar')).toBeInTheDocument();
|
expect(screen.getByTestId('instructor-toolbar')).toBeInTheDocument();
|
||||||
expect(screen.getByText('This learner no longer has access to this course. Their access expired on', { exact: false })).toBeInTheDocument();
|
expect(screen.getByText('This learner no longer has access to this course. Their access expired on', { exact: false })).toBeInTheDocument();
|
||||||
expect(screen.getByText('1/1/2020')).toBeInTheDocument();
|
expect(screen.getByText('1/1/2020', { exact: false })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('does not render banner when not masquerading', async () => {
|
it('does not render banner when not masquerading', async () => {
|
||||||
setMetadata({ is_enrolled: true, original_user_is_staff: true });
|
setMetadata({ is_enrolled: true, original_user_is_staff: true });
|
||||||
@@ -1215,7 +1316,7 @@ describe('Progress Tab', () => {
|
|||||||
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||||
expect(screen.queryByText('This learner no longer has access to this course. Their access expired on', { exact: false })).not.toBeInTheDocument();
|
expect(screen.queryByText('This learner no longer has access to this course. Their access expired on', { exact: false })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('1/1/2020')).not.toBeInTheDocument();
|
expect(screen.queryByText('1/1/2020', { exact: false })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1231,7 +1332,7 @@ describe('Progress Tab', () => {
|
|||||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||||
expect(screen.getByTestId('instructor-toolbar')).toBeInTheDocument();
|
expect(screen.getByTestId('instructor-toolbar')).toBeInTheDocument();
|
||||||
expect(screen.getByText('This learner does not yet have access to this course. The course starts on', { exact: false })).toBeInTheDocument();
|
expect(screen.getByText('This learner does not yet have access to this course. The course starts on', { exact: false })).toBeInTheDocument();
|
||||||
expect(screen.getByText('1/1/2999')).toBeInTheDocument();
|
expect(screen.getByText('1/1/2999', { exact: false })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('does not render banner when not masquerading', async () => {
|
it('does not render banner when not masquerading', async () => {
|
||||||
setMetadata({
|
setMetadata({
|
||||||
@@ -1243,7 +1344,7 @@ describe('Progress Tab', () => {
|
|||||||
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||||
expect(screen.queryByText('This learner does not yet have access to this course. The course starts on', { exact: false })).not.toBeInTheDocument();
|
expect(screen.queryByText('This learner does not yet have access to this course. The course starts on', { exact: false })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('1/1/2999')).not.toBeInTheDocument();
|
expect(screen.queryByText('1/1/2999', { exact: false })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ function CertificateStatus({ intl }) {
|
|||||||
const {
|
const {
|
||||||
isEnrolled,
|
isEnrolled,
|
||||||
org,
|
org,
|
||||||
|
canViewCertificate,
|
||||||
|
userTimezone,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -45,6 +47,8 @@ function CertificateStatus({ intl }) {
|
|||||||
hasScheduledContent,
|
hasScheduledContent,
|
||||||
isEnrolled,
|
isEnrolled,
|
||||||
userHasPassingGrade,
|
userHasPassingGrade,
|
||||||
|
null, // CourseExitPageIsActive
|
||||||
|
canViewCertificate,
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventProperties = {
|
const eventProperties = {
|
||||||
@@ -57,12 +61,11 @@ function CertificateStatus({ intl }) {
|
|||||||
|
|
||||||
let certStatus;
|
let certStatus;
|
||||||
let certWebViewUrl;
|
let certWebViewUrl;
|
||||||
let downloadUrl;
|
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||||
|
|
||||||
if (certificateData) {
|
if (certificateData) {
|
||||||
certStatus = certificateData.certStatus;
|
certStatus = certificateData.certStatus;
|
||||||
certWebViewUrl = certificateData.certWebViewUrl;
|
certWebViewUrl = certificateData.certWebViewUrl;
|
||||||
downloadUrl = certificateData.downloadUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let certCase;
|
let certCase;
|
||||||
@@ -115,6 +118,7 @@ function CertificateStatus({ intl }) {
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="progress.certificateStatus.unverifiedBody"
|
id="progress.certificateStatus.unverifiedBody"
|
||||||
defaultMessage="In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}."
|
defaultMessage="In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}."
|
||||||
|
description="Its shown when learner are not verified thus it recommends going over the verification process"
|
||||||
values={{ idVerificationSupportLink }}
|
values={{ idVerificationSupportLink }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -133,18 +137,14 @@ function CertificateStatus({ intl }) {
|
|||||||
Showcase your accomplishment on LinkedIn or your resumé today.
|
Showcase your accomplishment on LinkedIn or your resumé today.
|
||||||
You can download your certificate now and access it any time from your
|
You can download your certificate now and access it any time from your
|
||||||
{dashboardLink} and {profileLink}."
|
{dashboardLink} and {profileLink}."
|
||||||
|
description="Recommending an action for learner when course certificate is available"
|
||||||
values={{ dashboardLink, profileLink }}
|
values={{ dashboardLink, profileLink }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (certWebViewUrl) {
|
if (certWebViewUrl) {
|
||||||
certEventName = 'earned_viewable';
|
certEventName = 'earned_viewable';
|
||||||
buttonLocation = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
|
buttonLocation = `${getConfig().LMS_BASE_URL}${certWebViewUrl}`;
|
||||||
buttonText = intl.formatMessage(messages.viewableButton);
|
buttonText = intl.formatMessage(messages.viewableButton);
|
||||||
} else if (downloadUrl) {
|
|
||||||
certEventName = 'earned_downloadable';
|
|
||||||
buttonLocation = downloadUrl;
|
|
||||||
buttonText = intl.formatMessage(messages.downloadableButton);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -155,8 +155,9 @@ function CertificateStatus({ intl }) {
|
|||||||
body = (
|
body = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="courseCelebration.certificateBody.notAvailable.endDate"
|
id="courseCelebration.certificateBody.notAvailable.endDate"
|
||||||
defaultMessage="This course ends on {endDate}. Final grades and certificates are
|
defaultMessage="This course ends on {endDate}. Final grades and any earned certificates are
|
||||||
scheduled to be available after {certAvailabilityDate}."
|
scheduled to be available after {certAvailabilityDate}."
|
||||||
|
description="This shown for leaner when they are eligible for certifcate but it't not available yet, it could because leaners just finished the course quickly!"
|
||||||
values={{ endDate, certAvailabilityDate }}
|
values={{ endDate, certAvailabilityDate }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -175,10 +176,22 @@ function CertificateStatus({ intl }) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// This code shouldn't be hit but coding defensively since switch expects a default statement
|
|
||||||
default:
|
default:
|
||||||
certCase = null;
|
// if user completes a course before certificates are available, treat it as notAvailable
|
||||||
certEventName = 'no_certificate_status';
|
// regardless of passing or nonpassing status
|
||||||
|
if (!canViewCertificate) {
|
||||||
|
certCase = 'notAvailable';
|
||||||
|
endDate = intl.formatDate(end, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
...timezoneFormatArgs,
|
||||||
|
});
|
||||||
|
body = intl.formatMessage(messages.notAvailableEndDateBody, { endDate });
|
||||||
|
} else {
|
||||||
|
certCase = null;
|
||||||
|
certEventName = 'no_certificate_status';
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,14 +234,12 @@ function 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 shadow-sm border-0">
|
<Card className="bg-light-200 raised-card">
|
||||||
<Card.Body>
|
<Card.Header title={header} />
|
||||||
<Card.Title>
|
<Card.Section className="small text-gray-700">
|
||||||
<h3>{header}</h3>
|
{body}
|
||||||
</Card.Title>
|
</Card.Section>
|
||||||
<Card.Text className="small text-gray-700">
|
<Card.Footer>
|
||||||
{body}
|
|
||||||
</Card.Text>
|
|
||||||
{buttonText && (buttonLocation || buttonAction) && (
|
{buttonText && (buttonLocation || buttonAction) && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline-brand"
|
variant="outline-brand"
|
||||||
@@ -242,7 +253,7 @@ function CertificateStatus({ intl }) {
|
|||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Card.Body>
|
</Card.Footer>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,86 +4,107 @@ const messages = defineMessages({
|
|||||||
notPassingHeader: {
|
notPassingHeader: {
|
||||||
id: 'progress.certificateStatus.notPassingHeader',
|
id: 'progress.certificateStatus.notPassingHeader',
|
||||||
defaultMessage: 'Certificate status',
|
defaultMessage: 'Certificate status',
|
||||||
|
description: 'Header text when learner certifcate status is not passing',
|
||||||
},
|
},
|
||||||
notPassingBody: {
|
notPassingBody: {
|
||||||
id: 'progress.certificateStatus.notPassingBody',
|
id: 'progress.certificateStatus.notPassingBody',
|
||||||
defaultMessage: 'In order to qualify for a certificate, you must have a passing grade.',
|
defaultMessage: 'In order to qualify for a certificate, you must have a passing grade.',
|
||||||
|
description: 'Body text when learner certifcate status is not passing',
|
||||||
},
|
},
|
||||||
inProgressHeader: {
|
inProgressHeader: {
|
||||||
id: 'progress.certificateStatus.inProgressHeader',
|
id: 'progress.certificateStatus.inProgressHeader',
|
||||||
defaultMessage: 'More content is coming soon!',
|
defaultMessage: 'More content is coming soon!',
|
||||||
|
description: 'Header text when learner certifcate is in progress',
|
||||||
},
|
},
|
||||||
inProgressBody: {
|
inProgressBody: {
|
||||||
id: 'progress.certificateStatus.inProgressBody',
|
id: 'progress.certificateStatus.inProgressBody',
|
||||||
defaultMessage: 'It looks like there is more content in this course that will be released in the future. Look out for email updates or check back on your course for when this content will be available.',
|
defaultMessage: 'It looks like there is more content in this course that will be released in the future. Look out for email updates or check back on your course for when this content will be available.',
|
||||||
|
description: 'Body text when learner certifcate is in progress',
|
||||||
},
|
},
|
||||||
requestableHeader: {
|
requestableHeader: {
|
||||||
id: 'progress.certificateStatus.requestableHeader',
|
id: 'progress.certificateStatus.requestableHeader',
|
||||||
defaultMessage: 'Certificate status',
|
defaultMessage: 'Certificate status',
|
||||||
|
description: 'Header text when learner certifcate status is requestable',
|
||||||
},
|
},
|
||||||
requestableBody: {
|
requestableBody: {
|
||||||
id: 'progress.certificateStatus.requestableBody',
|
id: 'progress.certificateStatus.requestableBody',
|
||||||
defaultMessage: 'Congratulations, you qualified for a certificate! In order to access your certificate, request it below.',
|
defaultMessage: 'Congratulations, you qualified for a certificate! In order to access your certificate, request it below.',
|
||||||
|
description: 'Body text when learner certifcate status is requestable',
|
||||||
},
|
},
|
||||||
requestableButton: {
|
requestableButton: {
|
||||||
id: 'progress.certificateStatus.requestableButton',
|
id: 'progress.certificateStatus.requestableButton',
|
||||||
defaultMessage: 'Request certificate',
|
defaultMessage: 'Request certificate',
|
||||||
|
description: 'Button text when learner certifcate status is requestable',
|
||||||
},
|
},
|
||||||
unverifiedHeader: {
|
unverifiedHeader: {
|
||||||
id: 'progress.certificateStatus.unverifiedHeader',
|
id: 'progress.certificateStatus.unverifiedHeader',
|
||||||
defaultMessage: 'Certificate status',
|
defaultMessage: 'Certificate status',
|
||||||
|
description: 'Header text when learner certifcate status is unverified',
|
||||||
},
|
},
|
||||||
unverifiedButton: {
|
unverifiedButton: {
|
||||||
id: 'progress.certificateStatus.unverifiedButton',
|
id: 'progress.certificateStatus.unverifiedButton',
|
||||||
defaultMessage: 'Verify ID',
|
defaultMessage: 'Verify ID',
|
||||||
|
description: 'Button text when learner certifcate status is unverified',
|
||||||
},
|
},
|
||||||
unverifiedPendingBody: {
|
unverifiedPendingBody: {
|
||||||
id: 'progress.certificateStatus.courseCelebration.verificationPending',
|
id: 'progress.certificateStatus.courseCelebration.verificationPending',
|
||||||
defaultMessage: 'Your ID verification is pending and your certificate will be available once approved.',
|
defaultMessage: 'Your ID verification is pending and your certificate will be available once approved.',
|
||||||
|
description: 'Body text when learner certifcate status is unverified pending',
|
||||||
},
|
},
|
||||||
downloadableHeader: {
|
downloadableHeader: {
|
||||||
id: 'progress.certificateStatus.downloadableHeader',
|
id: 'progress.certificateStatus.downloadableHeader',
|
||||||
defaultMessage: 'Your certificate is available!',
|
defaultMessage: 'Your certificate is available!',
|
||||||
|
description: 'Header text when the certifcate is available',
|
||||||
},
|
},
|
||||||
downloadableBody: {
|
downloadableBody: {
|
||||||
id: 'progress.certificateStatus.downloadableBody',
|
id: 'progress.certificateStatus.downloadableBody',
|
||||||
defaultMessage: 'Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.',
|
defaultMessage: 'Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.',
|
||||||
},
|
description: 'Recommending an action for learner when course certificate is available',
|
||||||
downloadableButton: {
|
|
||||||
id: 'progress.certificateStatus.downloadableButton',
|
|
||||||
defaultMessage: 'Download my certificate',
|
|
||||||
},
|
},
|
||||||
viewableButton: {
|
viewableButton: {
|
||||||
id: 'progress.certificateStatus.viewableButton',
|
id: 'progress.certificateStatus.viewableButton',
|
||||||
defaultMessage: 'View my certificate',
|
defaultMessage: 'View my certificate',
|
||||||
|
description: 'Button text which view or links to the certifcate',
|
||||||
},
|
},
|
||||||
notAvailableHeader: {
|
notAvailableHeader: {
|
||||||
id: 'progress.certificateStatus.notAvailableHeader',
|
id: 'progress.certificateStatus.notAvailableHeader',
|
||||||
defaultMessage: 'Certificate status',
|
defaultMessage: 'Certificate status',
|
||||||
|
description: 'Header text when the certifcate is not available',
|
||||||
|
},
|
||||||
|
notAvailableEndDateBody: {
|
||||||
|
id: 'progress.certificateBody.notAvailable.endDate',
|
||||||
|
defaultMessage: 'Final grades and any earned certificates are scheduled to be available after {endDate}.',
|
||||||
|
description: 'Shown for learners who have finished a course before grades and certificates are available.',
|
||||||
},
|
},
|
||||||
upgradeHeader: {
|
upgradeHeader: {
|
||||||
id: 'progress.certificateStatus.upgradeHeader',
|
id: 'progress.certificateStatus.upgradeHeader',
|
||||||
defaultMessage: 'Earn a certificate',
|
defaultMessage: 'Earn a certificate',
|
||||||
|
description: 'Header text when the learner needs to upgrade to earn a certifcate ',
|
||||||
},
|
},
|
||||||
upgradeBody: {
|
upgradeBody: {
|
||||||
id: 'progress.certificateStatus.upgradeBody',
|
id: 'progress.certificateStatus.upgradeBody',
|
||||||
defaultMessage: 'You are in an audit track and do not qualify for a certificate. In order to work towards a certificate, upgrade your course today.',
|
defaultMessage: 'You are in an audit track and do not qualify for a certificate. In order to work towards a certificate, upgrade your course today.',
|
||||||
|
description: 'Body text when the learner needs to upgrade to earn a certifcate ',
|
||||||
},
|
},
|
||||||
upgradeButton: {
|
upgradeButton: {
|
||||||
id: 'progress.certificateStatus.upgradeButton',
|
id: 'progress.certificateStatus.upgradeButton',
|
||||||
defaultMessage: 'Upgrade now',
|
defaultMessage: 'Upgrade now',
|
||||||
|
description: 'Button text which leaner needs to upgrade to get the certifcate',
|
||||||
},
|
},
|
||||||
unverifiedHomeHeader: {
|
unverifiedHomeHeader: {
|
||||||
id: 'progress.certificateStatus.unverifiedHomeHeader',
|
id: 'progress.certificateStatus.unverifiedHomeHeader.v2',
|
||||||
defaultMessage: 'Verify your identity to earn a certificate!',
|
defaultMessage: 'Verify your identity to qualify for a certificate.',
|
||||||
|
description: 'Header text when the learner needs to do verification to earn a certifcate ',
|
||||||
},
|
},
|
||||||
unverifiedHomeButton: {
|
unverifiedHomeButton: {
|
||||||
id: 'progress.certificateStatus.unverifiedHomeButton',
|
id: 'progress.certificateStatus.unverifiedHomeButton',
|
||||||
defaultMessage: 'Verify my ID',
|
defaultMessage: 'Verify my ID',
|
||||||
|
description: 'Button text which leaner needs to do verification to earn a certifcate',
|
||||||
},
|
},
|
||||||
unverifiedHomeBody: {
|
unverifiedHomeBody: {
|
||||||
id: 'progress.certificateStatus.unverifiedHomeBody',
|
id: 'progress.certificateStatus.unverifiedHomeBody',
|
||||||
defaultMessage: 'In order to generate a certificate for this course, you must complete the ID verification process.',
|
defaultMessage: 'In order to generate a certificate for this course, you must complete the ID verification process.',
|
||||||
|
description: 'Body text when the learner needs to do verification to earn a certifcate',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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 {
|
||||||
|
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';
|
||||||
@@ -26,6 +28,8 @@ function CompletionDonutChart({ intl }) {
|
|||||||
const lockedPercentage = lockedCount ? Number(((lockedCount / numTotalUnits) * 100).toFixed(0)) : 0;
|
const lockedPercentage = lockedCount ? Number(((lockedCount / numTotalUnits) * 100).toFixed(0)) : 0;
|
||||||
const incompletePercentage = 100 - completePercentage - lockedPercentage;
|
const incompletePercentage = 100 - completePercentage - lockedPercentage;
|
||||||
|
|
||||||
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<svg role="img" width="50%" height="100%" viewBox="0 0 42 42" className="donut" style={{ maxWidth: '178px' }} aria-hidden="true">
|
<svg role="img" width="50%" height="100%" viewBox="0 0 42 42" className="donut" style={{ maxWidth: '178px' }} aria-hidden="true">
|
||||||
@@ -35,7 +39,7 @@ function CompletionDonutChart({ intl }) {
|
|||||||
<circle className="donut-hole" fill="#fff" cx="21" cy="21" r="15.91549430918954" />
|
<circle className="donut-hole" fill="#fff" cx="21" cy="21" r="15.91549430918954" />
|
||||||
<g className="donut-chart-text">
|
<g className="donut-chart-text">
|
||||||
<text x="50%" y="50%" className="donut-chart-number">
|
<text x="50%" y="50%" className="donut-chart-number">
|
||||||
{completePercentage}%
|
{completePercentage}{isLocaleRtl && '\u200f'}%
|
||||||
</text>
|
</text>
|
||||||
<text x="50%" y="50%" className="donut-chart-label">
|
<text x="50%" y="50%" className="donut-chart-label">
|
||||||
{intl.formatMessage(messages.donutLabel)}
|
{intl.formatMessage(messages.donutLabel)}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import messages from './messages';
|
|||||||
|
|
||||||
function CourseCompletion({ intl }) {
|
function CourseCompletion({ intl }) {
|
||||||
return (
|
return (
|
||||||
<section className="text-dark-700 mb-4 rounded shadow-sm p-4">
|
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
||||||
<div className="row w-100 m-0">
|
<div className="row w-100 m-0">
|
||||||
<div className="col-12 col-sm-6 col-md-7 p-0">
|
<div className="col-12 col-sm-6 col-md-7 p-0">
|
||||||
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
||||||
|
|||||||
@@ -4,38 +4,47 @@ const messages = defineMessages({
|
|||||||
donutLabel: {
|
donutLabel: {
|
||||||
id: 'progress.completion.donut.label',
|
id: 'progress.completion.donut.label',
|
||||||
defaultMessage: 'completed',
|
defaultMessage: 'completed',
|
||||||
|
description: 'Label text for progress donut chart',
|
||||||
},
|
},
|
||||||
completionBody: {
|
completionBody: {
|
||||||
id: 'progress.completion.body',
|
id: 'progress.completion.body',
|
||||||
defaultMessage: 'This represents how much of the course content you have completed. Note that some content may not yet be released.',
|
defaultMessage: 'This represents how much of the course content you have completed. Note that some content may not yet be released.',
|
||||||
|
description: 'It explains the meaning of progress donut chart',
|
||||||
},
|
},
|
||||||
completeContentTooltip: {
|
completeContentTooltip: {
|
||||||
id: 'progress.completion.tooltip.locked',
|
id: 'progress.completion.tooltip.locked',
|
||||||
defaultMessage: 'Content that you have completed.',
|
defaultMessage: 'Content that you have completed.',
|
||||||
|
description: 'It expalains the meaning of content that is completed',
|
||||||
},
|
},
|
||||||
courseCompletion: {
|
courseCompletion: {
|
||||||
id: 'progress.completion.header',
|
id: 'progress.completion.header',
|
||||||
defaultMessage: 'Course completion',
|
defaultMessage: 'Course completion',
|
||||||
|
description: 'Header text for (completion donut chart) section of the progress tab',
|
||||||
},
|
},
|
||||||
incompleteContentTooltip: {
|
incompleteContentTooltip: {
|
||||||
id: 'progress.completion.tooltip',
|
id: 'progress.completion.tooltip',
|
||||||
defaultMessage: 'Content that you have access to and have not completed.',
|
defaultMessage: 'Content that you have access to and have not completed.',
|
||||||
|
description: 'It explain the meaning for content is completed',
|
||||||
},
|
},
|
||||||
lockedContentTooltip: {
|
lockedContentTooltip: {
|
||||||
id: 'progress.completion.tooltip.complete',
|
id: 'progress.completion.tooltip.complete',
|
||||||
defaultMessage: 'Content that is locked and available only to those who upgrade.',
|
defaultMessage: 'Content that is locked and available only to those who upgrade.',
|
||||||
|
description: 'It expalains the meaning of content that is locked',
|
||||||
},
|
},
|
||||||
percentComplete: {
|
percentComplete: {
|
||||||
id: 'progress.completion.donut.percentComplete',
|
id: 'progress.completion.donut.percentComplete',
|
||||||
defaultMessage: 'You have completed {percent}% of content in this course.',
|
defaultMessage: 'You have completed {percent}% of content in this course.',
|
||||||
|
description: 'It summarize the progress in the course (100% - %incomplete)',
|
||||||
},
|
},
|
||||||
percentIncomplete: {
|
percentIncomplete: {
|
||||||
id: 'progress.completion.donut.percentIncomplete',
|
id: 'progress.completion.donut.percentIncomplete',
|
||||||
defaultMessage: 'You have not completed {percent}% of content in this course that you have access to.',
|
defaultMessage: 'You have not completed {percent}% of content in this course that you have access to.',
|
||||||
|
description: 'It summarize the progress in the course (100% - %complete)',
|
||||||
},
|
},
|
||||||
percentLocked: {
|
percentLocked: {
|
||||||
id: 'progress.completion.donut.percentLocked',
|
id: 'progress.completion.donut.percentLocked',
|
||||||
defaultMessage: '{percent}% of content in this course is locked and available only for those who upgrade.',
|
defaultMessage: '{percent}% of content in this course is locked and available only for those who upgrade.',
|
||||||
|
description: 'It indicate the relative size of content that is locked in the course (100% - %open_content)',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { CheckCircle, WarningFilled, WatchFilled } from '@edx/paragon/icons';
|
||||||
|
import { Hyperlink, Icon } from '@edx/paragon';
|
||||||
|
|
||||||
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
import { DashboardLink } from '../../../shared/links';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
function CreditInformation({ intl }) {
|
||||||
|
const {
|
||||||
|
courseId,
|
||||||
|
} = useSelector(state => state.courseHome);
|
||||||
|
|
||||||
|
const {
|
||||||
|
creditCourseRequirements,
|
||||||
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
|
if (!creditCourseRequirements) { return null; }
|
||||||
|
|
||||||
|
let eligibilityStatus;
|
||||||
|
let requirementStatus;
|
||||||
|
const requirements = [];
|
||||||
|
const dashboardLink = <DashboardLink />;
|
||||||
|
const creditLink = (
|
||||||
|
<Hyperlink
|
||||||
|
variant="muted"
|
||||||
|
isInline
|
||||||
|
destination={getConfig().CREDIT_HELP_LINK_URL}
|
||||||
|
>{intl.formatMessage(messages.courseCredit)}
|
||||||
|
</Hyperlink>
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (creditCourseRequirements.eligibilityStatus) {
|
||||||
|
case 'not_eligible':
|
||||||
|
eligibilityStatus = (
|
||||||
|
<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;
|
||||||
|
case 'eligible':
|
||||||
|
eligibilityStatus = (
|
||||||
|
<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;
|
||||||
|
case 'partial_eligible':
|
||||||
|
eligibilityStatus = (
|
||||||
|
<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;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
creditCourseRequirements.requirements.forEach(requirement => {
|
||||||
|
switch (requirement.status) {
|
||||||
|
case 'submitted':
|
||||||
|
requirementStatus = (<>{intl.formatMessage(messages.verificationSubmitted)} <Icon src={CheckCircle} className="text-success-500 d-inline-flex align-bottom" /></>);
|
||||||
|
break;
|
||||||
|
case 'failed':
|
||||||
|
case 'declined':
|
||||||
|
requirementStatus = (<>{intl.formatMessage(messages.verificationFailed)} <Icon src={WarningFilled} className="d-inline-flex align-bottom" /></>);
|
||||||
|
break;
|
||||||
|
case 'satisfied':
|
||||||
|
requirementStatus = (<>{intl.formatMessage(messages.completed)} <Icon src={CheckCircle} className="text-success-500 d-inline-flex align-bottom" /></>);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
requirementStatus = (<>{intl.formatMessage(messages.upcoming)} <Icon src={WatchFilled} className="text-gray-500 d-inline-flex align-bottom" /></>);
|
||||||
|
}
|
||||||
|
requirements.push((
|
||||||
|
<div className="row w-100 m-0 small" key={`requirement-${requirement.order}`}>
|
||||||
|
<p className="font-weight-bold">
|
||||||
|
{requirement.namespace === 'grade'
|
||||||
|
? `${intl.formatMessage(messages.minimumGrade, { minGrade: Number(requirement.criteria.minGrade) * 100 })}:`
|
||||||
|
: `${requirement.displayName}:`}
|
||||||
|
</p>
|
||||||
|
<div className="ml-1">
|
||||||
|
{requirementStatus}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="h4 col-12 p-0">{intl.formatMessage(messages.requirementsHeader)}</h3>
|
||||||
|
<p className="small">{eligibilityStatus}</p>
|
||||||
|
{requirements}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreditInformation.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(CreditInformation);
|
||||||
40
src/course-home/progress-tab/credit-information/messages.js
Normal file
40
src/course-home/progress-tab/credit-information/messages.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
completed: {
|
||||||
|
id: 'progress.creditInformation.completed',
|
||||||
|
defaultMessage: 'Completed',
|
||||||
|
description: 'Label text if a requirement for (course credit) is satisfied',
|
||||||
|
},
|
||||||
|
courseCredit: {
|
||||||
|
id: 'progress.creditInformation.courseCredit',
|
||||||
|
defaultMessage: 'course credit',
|
||||||
|
description: 'Anchor text for link that redirects (course credit) help page',
|
||||||
|
},
|
||||||
|
minimumGrade: {
|
||||||
|
id: 'progress.creditInformation.minimumGrade',
|
||||||
|
defaultMessage: 'Minimum grade for credit ({minGrade}%)',
|
||||||
|
},
|
||||||
|
requirementsHeader: {
|
||||||
|
id: 'progress.creditInformation.requirementsHeader',
|
||||||
|
defaultMessage: 'Requirements for course credit',
|
||||||
|
description: 'Header for the requirements section in course credit',
|
||||||
|
},
|
||||||
|
upcoming: {
|
||||||
|
id: 'progress.creditInformation.upcoming',
|
||||||
|
defaultMessage: 'Upcoming',
|
||||||
|
description: 'It indicate that the a (credit requirement) status is not known yet',
|
||||||
|
},
|
||||||
|
verificationFailed: {
|
||||||
|
id: 'progress.creditInformation.verificationFailed',
|
||||||
|
defaultMessage: 'Verification failed',
|
||||||
|
description: 'It indicate that the learner submitted a requirement but is either failed or declined',
|
||||||
|
},
|
||||||
|
verificationSubmitted: {
|
||||||
|
id: 'progress.creditInformation.verificationSubmitted',
|
||||||
|
defaultMessage: 'Verification submitted',
|
||||||
|
description: 'It indicate that the learner submitted a requirement but is not graded or reviewed yet',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -7,6 +7,7 @@ import { useModel } from '../../../../generic/model-store';
|
|||||||
import CourseGradeFooter from './CourseGradeFooter';
|
import CourseGradeFooter from './CourseGradeFooter';
|
||||||
import CourseGradeHeader from './CourseGradeHeader';
|
import CourseGradeHeader from './CourseGradeHeader';
|
||||||
import GradeBar from './GradeBar';
|
import GradeBar from './GradeBar';
|
||||||
|
import CreditInformation from '../../credit-information/CreditInformation';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ function CourseGrade({ intl }) {
|
|||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
creditCourseRequirements,
|
||||||
gradesFeatureIsFullyLocked,
|
gradesFeatureIsFullyLocked,
|
||||||
gradesFeatureIsPartiallyLocked,
|
gradesFeatureIsPartiallyLocked,
|
||||||
gradingPolicy: {
|
gradingPolicy: {
|
||||||
@@ -28,18 +30,24 @@ function CourseGrade({ intl }) {
|
|||||||
const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
const applyLockedOverlay = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="text-dark-700 my-4 rounded shadow-sm">
|
<section className="text-dark-700 my-4 rounded raised-card">
|
||||||
{(gradesFeatureIsFullyLocked || gradesFeatureIsPartiallyLocked) && <CourseGradeHeader />}
|
{(gradesFeatureIsFullyLocked || gradesFeatureIsPartiallyLocked) && <CourseGradeHeader />}
|
||||||
<div className={applyLockedOverlay} aria-hidden={gradesFeatureIsFullyLocked}>
|
<div className={applyLockedOverlay} aria-hidden={gradesFeatureIsFullyLocked}>
|
||||||
<div className="row w-100 m-0 p-4">
|
<div className="row w-100 m-0 p-4">
|
||||||
<div className="col-12 col-sm-6 p-0 pr-sm-2">
|
<div className="col-12 col-sm-6 p-0 pr-sm-5.5">
|
||||||
<h2>{intl.formatMessage(messages.grades)}</h2>
|
<h2>{creditCourseRequirements
|
||||||
|
? intl.formatMessage(messages.gradesAndCredit)
|
||||||
|
: intl.formatMessage(messages.grades)}
|
||||||
|
</h2>
|
||||||
<p className="small">
|
<p className="small">
|
||||||
{intl.formatMessage(messages.courseGradeBody)}
|
{intl.formatMessage(messages.courseGradeBody)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<GradeBar passingGrade={passingGrade} />
|
<GradeBar passingGrade={passingGrade} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="row w-100 m-0 px-4">
|
||||||
|
<CreditInformation />
|
||||||
|
</div>
|
||||||
<CourseGradeFooter passingGrade={passingGrade} />
|
<CourseGradeFooter passingGrade={passingGrade} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ import React from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { layoutGenerator } from 'react-break';
|
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { CheckCircle, WarningFilled } from '@edx/paragon/icons';
|
import { CheckCircle, WarningFilled } from '@edx/paragon/icons';
|
||||||
import { Icon } from '@edx/paragon';
|
import { breakpoints, Icon, useWindowSize } from '@edx/paragon';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
import GradeRangeTooltip from './GradeRangeTooltip';
|
import GradeRangeTooltip from './GradeRangeTooltip';
|
||||||
@@ -27,13 +25,7 @@ function CourseGradeFooter({ intl, passingGrade }) {
|
|||||||
},
|
},
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
const layout = layoutGenerator({
|
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
||||||
mobile: 0,
|
|
||||||
tablet: 768,
|
|
||||||
});
|
|
||||||
|
|
||||||
const OnMobile = layout.is('mobile');
|
|
||||||
const OnAtLeastTablet = layout.isAtLeast('tablet');
|
|
||||||
|
|
||||||
const hasLetterGrades = Object.keys(gradeRange).length > 1; // A pass/fail course will only have one key
|
const hasLetterGrades = Object.keys(gradeRange).length > 1; // A pass/fail course will only have one key
|
||||||
let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade });
|
let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade });
|
||||||
@@ -66,7 +58,7 @@ function CourseGradeFooter({ intl, passingGrade }) {
|
|||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-11 pl-2 px-0">
|
<div className="col-11 pl-2 px-0">
|
||||||
<OnMobile>
|
{!wideScreen && (
|
||||||
<span className="h5 align-bottom">
|
<span className="h5 align-bottom">
|
||||||
{footerText}
|
{footerText}
|
||||||
{hasLetterGrades && (
|
{hasLetterGrades && (
|
||||||
@@ -76,8 +68,8 @@ function CourseGradeFooter({ intl, passingGrade }) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</OnMobile>
|
)}
|
||||||
<OnAtLeastTablet>
|
{wideScreen && (
|
||||||
<span className="h4 m-0 align-bottom">
|
<span className="h4 m-0 align-bottom">
|
||||||
{footerText}
|
{footerText}
|
||||||
{hasLetterGrades && (
|
{hasLetterGrades && (
|
||||||
@@ -87,7 +79,7 @@ function CourseGradeFooter({ intl, passingGrade }) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</OnAtLeastTablet>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import {
|
||||||
|
getLocale, injectIntl, intlShape, isRtl,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { OverlayTrigger, Popover } from '@edx/paragon';
|
import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||||
|
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
@@ -17,11 +19,19 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
|||||||
const {
|
const {
|
||||||
courseGrade: {
|
courseGrade: {
|
||||||
isPassing,
|
isPassing,
|
||||||
visiblePercent,
|
percent,
|
||||||
},
|
},
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
const currentGrade = Number((visiblePercent * 100).toFixed(0));
|
const currentGrade = Number((percent * 100).toFixed(0));
|
||||||
|
|
||||||
|
let currentGradeDirection = currentGrade < 50 ? '' : '-';
|
||||||
|
|
||||||
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
|
if (isLocaleRtl) {
|
||||||
|
currentGradeDirection = currentGrade < 50 ? '-' : '';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -31,22 +41,22 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
|||||||
overlay={(
|
overlay={(
|
||||||
<Popover id={`${isPassing ? 'passing' : 'non-passing'}-grade-tooltip`} aria-hidden="true" className={tooltipClassName}>
|
<Popover id={`${isPassing ? 'passing' : 'non-passing'}-grade-tooltip`} aria-hidden="true" className={tooltipClassName}>
|
||||||
<Popover.Content data-testid="currentGradeTooltipContent" className={isPassing ? 'text-white' : 'text-dark-700'}>
|
<Popover.Content data-testid="currentGradeTooltipContent" className={isPassing ? 'text-white' : 'text-dark-700'}>
|
||||||
{currentGrade.toFixed(0)}%
|
{currentGrade.toFixed(0)}{isLocaleRtl ? '\u200f' : ''}%
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<g>
|
<g>
|
||||||
<circle cx={`${Math.min(...[currentGrade, 100])}%`} cy="50%" r="8.5" fill="transparent" />
|
<circle cx={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`} cy="50%" r="8.5" fill="transparent" />
|
||||||
<rect className="grade-bar__divider" x={`${Math.min(...[currentGrade, 100])}%`} style={{ transform: 'translateY(2.61em)' }} />
|
<rect className="grade-bar__divider" x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`} style={{ transform: 'translateY(2.61em)' }} />
|
||||||
</g>
|
</g>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
<text
|
<text
|
||||||
className="x-small"
|
className="x-small"
|
||||||
textAnchor={currentGrade < 50 ? 'start' : 'end'}
|
textAnchor={currentGrade < 50 ? 'start' : 'end'}
|
||||||
x={`${Math.min(...[currentGrade, 100])}%`}
|
x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`}
|
||||||
y="20px"
|
y="20px"
|
||||||
style={{ transform: `translateX(${currentGrade < 50 ? '' : '-'}3.4em)` }}
|
style={{ transform: `translateX(${currentGradeDirection}3.4em)` }}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.currentGradeLabel)}
|
{intl.formatMessage(messages.currentGradeLabel)}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import React from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import {
|
||||||
|
getLocale, injectIntl, intlShape, isRtl,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
import CurrentGradeTooltip from './CurrentGradeTooltip';
|
import CurrentGradeTooltip from './CurrentGradeTooltip';
|
||||||
import PassingGradeTooltip from './PassingGradeTooltip';
|
import PassingGradeTooltip from './PassingGradeTooltip';
|
||||||
@@ -17,23 +19,25 @@ function GradeBar({ intl, passingGrade }) {
|
|||||||
const {
|
const {
|
||||||
courseGrade: {
|
courseGrade: {
|
||||||
isPassing,
|
isPassing,
|
||||||
visiblePercent,
|
percent,
|
||||||
},
|
},
|
||||||
gradesFeatureIsFullyLocked,
|
gradesFeatureIsFullyLocked,
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
const currentGrade = Number((visiblePercent * 100).toFixed(0));
|
const currentGrade = Number((percent * 100).toFixed(0));
|
||||||
|
|
||||||
const lockedTooltipClassName = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
const lockedTooltipClassName = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
||||||
|
|
||||||
|
const adjustedRtlStyle = (percentOffest) => (isRtl(getLocale()) ? { transform: `translateX(${100 - percentOffest}%)` } : {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-12 col-sm-6 align-self-center">
|
<div className="col-12 col-sm-6 align-self-center p-0">
|
||||||
<div className="sr-only">{intl.formatMessage(messages.courseGradeBarAltText, { currentGrade, passingGrade })}</div>
|
<div className="sr-only">{intl.formatMessage(messages.courseGradeBarAltText, { currentGrade, passingGrade })}</div>
|
||||||
<svg width="100%" height="100px" className="grade-bar" aria-hidden="true">
|
<svg width="100%" height="100px" className="grade-bar" aria-hidden="true">
|
||||||
<g style={{ transform: 'translateY(2.61em)' }}>
|
<g style={{ transform: 'translateY(2.61em)' }}>
|
||||||
<rect className="grade-bar__base" width="100%" />
|
<rect className="grade-bar__base" width="100%" />
|
||||||
<rect className="grade-bar--passing" width={`${passingGrade}%`} />
|
<rect className="grade-bar--passing" width={`${passingGrade}%`} style={adjustedRtlStyle(passingGrade)} />
|
||||||
<rect className={`grade-bar--current-${isPassing ? 'passing' : 'non-passing'}`} width={`${currentGrade}%`} />
|
<rect className={`grade-bar--current-${isPassing ? 'passing' : 'non-passing'}`} width={`${currentGrade}%`} style={adjustedRtlStyle(currentGrade)} />
|
||||||
|
|
||||||
{/* Start divider */}
|
{/* Start divider */}
|
||||||
<rect className="grade-bar__divider" />
|
<rect className="grade-bar__divider" />
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
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 {
|
||||||
|
getLocale, injectIntl, intlShape, isRtl,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { OverlayTrigger, Popover } from '@edx/paragon';
|
import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
||||||
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
|
let passingGradeDirection = passingGrade < 50 ? '' : '-';
|
||||||
|
|
||||||
|
if (isLocaleRtl) {
|
||||||
|
passingGradeDirection = passingGrade < 50 ? '-' : '';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
@@ -15,23 +25,23 @@ function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
|||||||
overlay={(
|
overlay={(
|
||||||
<Popover id="minimum-grade-tooltip" className={`bg-primary-500 ${tooltipClassName}`} aria-hidden="true">
|
<Popover id="minimum-grade-tooltip" className={`bg-primary-500 ${tooltipClassName}`} aria-hidden="true">
|
||||||
<Popover.Content className="text-white">
|
<Popover.Content className="text-white">
|
||||||
{passingGrade}%
|
{passingGrade}{isLocaleRtl && '\u200f'}%
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<g>
|
<g>
|
||||||
<circle cx={`${passingGrade}%`} cy="50%" r="8.5" fill="transparent" />
|
<circle cx={`${isLocaleRtl ? 100 - passingGrade : passingGrade}%`} cy="50%" r="8.5" fill="transparent" />
|
||||||
<circle className="grade-bar--passing" cx={`${passingGrade}%`} cy="50%" r="4.5" />
|
<circle className="grade-bar--passing" cx={`${isLocaleRtl ? 100 - passingGrade : passingGrade}%`} cy="50%" r="4.5" />
|
||||||
</g>
|
</g>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
|
|
||||||
<text
|
<text
|
||||||
className="x-small"
|
className="x-small"
|
||||||
textAnchor={passingGrade < 50 ? 'start' : 'end'}
|
textAnchor={passingGrade < 50 ? 'start' : 'end'}
|
||||||
x={`${passingGrade}%`}
|
x={`${isLocaleRtl ? 100 - passingGrade : passingGrade}%`}
|
||||||
y="90px"
|
y="90px"
|
||||||
style={{ transform: `translateX(${passingGrade < 50 ? '' : '-'}3.4em)` }}
|
style={{ transform: `translateX(${passingGradeDirection}3.4em)` }}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.passingGradeLabel)}
|
{intl.formatMessage(messages.passingGradeLabel)}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig } 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 { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
@@ -20,6 +19,7 @@ function DetailedGrades({ intl }) {
|
|||||||
} = useSelector(state => state.courseHome);
|
} = useSelector(state => state.courseHome);
|
||||||
const {
|
const {
|
||||||
org,
|
org,
|
||||||
|
tabs,
|
||||||
} = useModel('courseHomeMeta', courseId);
|
} = useModel('courseHomeMeta', courseId);
|
||||||
const {
|
const {
|
||||||
gradesFeatureIsFullyLocked,
|
gradesFeatureIsFullyLocked,
|
||||||
@@ -37,10 +37,14 @@ function DetailedGrades({ intl }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const outlineLink = (
|
const overviewTab = tabs.find(tab => tab.slug === 'outline');
|
||||||
|
const overviewTabUrl = overviewTab && overviewTab.url;
|
||||||
|
|
||||||
|
const outlineLink = overviewTabUrl && (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
className="muted-link inline-link"
|
variant="muted"
|
||||||
destination={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course`}
|
isInline
|
||||||
|
destination={overviewTabUrl}
|
||||||
onClick={logOutlineLinkClick}
|
onClick={logOutlineLinkClick}
|
||||||
tabIndex={gradesFeatureIsFullyLocked ? '-1' : '0'}
|
tabIndex={gradesFeatureIsFullyLocked ? '-1' : '0'}
|
||||||
>
|
>
|
||||||
@@ -63,13 +67,16 @@ function DetailedGrades({ intl }) {
|
|||||||
{!hasSectionScores && (
|
{!hasSectionScores && (
|
||||||
<p className="small">{intl.formatMessage(messages.detailedGradesEmpty)}</p>
|
<p className="small">{intl.formatMessage(messages.detailedGradesEmpty)}</p>
|
||||||
)}
|
)}
|
||||||
<p className="x-small m-0">
|
{overviewTabUrl && (
|
||||||
<FormattedMessage
|
<p className="x-small m-0">
|
||||||
id="progress.ungradedAlert"
|
<FormattedMessage
|
||||||
defaultMessage="For progress on ungraded aspects of the course, view your {outlineLink}."
|
id="progress.ungradedAlert"
|
||||||
values={{ outlineLink }}
|
defaultMessage="For progress on ungraded aspects of the course, view your {outlineLink}."
|
||||||
/>
|
description="Text that precede link that redirect to course outline page"
|
||||||
</p>
|
values={{ outlineLink }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
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 {
|
||||||
|
getLocale, injectIntl, intlShape, isRtl,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { DataTable } from '@edx/paragon';
|
import { DataTable } from '@edx/paragon';
|
||||||
|
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
@@ -17,6 +19,7 @@ function DetailedGradesTable({ intl }) {
|
|||||||
sectionScores,
|
sectionScores,
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
return (
|
return (
|
||||||
sectionScores.map((chapter) => {
|
sectionScores.map((chapter) => {
|
||||||
const subsectionScores = chapter.subsections.filter(
|
const subsectionScores = chapter.subsections.filter(
|
||||||
@@ -32,7 +35,7 @@ function DetailedGradesTable({ intl }) {
|
|||||||
|
|
||||||
const detailedGradesData = subsectionScores.map((subsection) => ({
|
const detailedGradesData = subsectionScores.map((subsection) => ({
|
||||||
subsectionTitle: <SubsectionTitleCell subsection={subsection} />,
|
subsectionTitle: <SubsectionTitleCell subsection={subsection} />,
|
||||||
score: <span className={subsection.learnerHasAccess ? '' : 'greyed-out'}>{subsection.numPointsEarned}/{subsection.numPointsPossible}</span>,
|
score: <span className={subsection.learnerHasAccess ? '' : 'greyed-out'}>{subsection.numPointsEarned}{isLocaleRtl ? '\\' : '/'}{subsection.numPointsPossible}</span>,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import {
|
||||||
|
getLocale, injectIntl, intlShape, isRtl,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function ProblemScoreDrawer({ intl, problemScores, subsection }) {
|
function ProblemScoreDrawer({ intl, problemScores, subsection }) {
|
||||||
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
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(messages.problemScoreLabel)}</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 => (
|
{problemScores.map(problemScore => (
|
||||||
<li className="ml-3">{problemScore.earned}/{problemScore.possible}</li>
|
<li className="ml-3">{problemScore.earned}{isLocaleRtl ? '\\' : '/'}{problemScore.possible}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ 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 { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Collapsible, Icon, Row } from '@edx/paragon';
|
import { Collapsible, Icon, Row } from '@edx/paragon';
|
||||||
import { ArrowDropDown, ArrowDropUp, Blocked } from '@edx/paragon/icons';
|
import {
|
||||||
|
ArrowDropDown, ArrowDropUp, Blocked, Info,
|
||||||
|
} from '@edx/paragon/icons';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
@@ -79,7 +81,21 @@ function SubsectionTitleCell({ intl, subsection }) {
|
|||||||
</span>
|
</span>
|
||||||
</Row>
|
</Row>
|
||||||
<Collapsible.Body className="d-flex w-100">
|
<Collapsible.Body className="d-flex w-100">
|
||||||
<ProblemScoreDrawer problemScores={problemScores} subsection={subsection} />
|
<div className="col">
|
||||||
|
{ subsection.override && (
|
||||||
|
<div className="row w-100 m-0 x-small ml-4 pt-2 pl-1 text-gray-700 flex-nowrap">
|
||||||
|
<div>
|
||||||
|
<Icon
|
||||||
|
src={Info}
|
||||||
|
className="x-small mr-1 text-primary-500"
|
||||||
|
style={{ height: '1.3em', width: '1.3em' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>{intl.formatMessage(messages.sectionGradeOverridden)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ProblemScoreDrawer problemScores={problemScores} subsection={subsection} />
|
||||||
|
</div>
|
||||||
</Collapsible.Body>
|
</Collapsible.Body>
|
||||||
</Collapsible.Advanced>
|
</Collapsible.Advanced>
|
||||||
);
|
);
|
||||||
@@ -87,7 +103,20 @@ function SubsectionTitleCell({ intl, subsection }) {
|
|||||||
|
|
||||||
SubsectionTitleCell.propTypes = {
|
SubsectionTitleCell.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
subsection: PropTypes.shape.isRequired,
|
subsection: PropTypes.shape({
|
||||||
|
blockKey: PropTypes.string.isRequired,
|
||||||
|
displayName: PropTypes.string.isRequired,
|
||||||
|
learnerHasAccess: PropTypes.bool.isRequired,
|
||||||
|
override: PropTypes.shape({
|
||||||
|
system: PropTypes.string,
|
||||||
|
reason: PropTypes.string,
|
||||||
|
}),
|
||||||
|
problemScores: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
earned: PropTypes.number.isRequired,
|
||||||
|
possible: PropTypes.number.isRequired,
|
||||||
|
})).isRequired,
|
||||||
|
url: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(SubsectionTitleCell);
|
export default injectIntl(SubsectionTitleCell);
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import {
|
||||||
|
getLocale, injectIntl, intlShape, isRtl,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { DataTable } from '@edx/paragon';
|
import { DataTable } from '@edx/paragon';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
@@ -64,15 +66,17 @@ function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
|||||||
footnoteMarker = footnotes.length;
|
footnoteMarker = footnotes.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type) ? 'greyed-out' : '';
|
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type);
|
||||||
|
|
||||||
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: {
|
type: {
|
||||||
footnoteId, footnoteMarker, type: assignment.type, locked,
|
footnoteId, footnoteMarker, type: assignment.type, locked,
|
||||||
},
|
},
|
||||||
weight: { weight: `${(assignment.weight * 100).toFixed(0)}%`, locked },
|
weight: { weight: `${(assignment.weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||||
grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}%`, locked },
|
grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||||
weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}%`, locked },
|
weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
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 {
|
||||||
|
getLocale, injectIntl, intlShape, isRtl,
|
||||||
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { DataTable } from '@edx/paragon';
|
import { DataTable } from '@edx/paragon';
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
@@ -15,18 +17,20 @@ function GradeSummaryTableFooter({ intl }) {
|
|||||||
const {
|
const {
|
||||||
courseGrade: {
|
courseGrade: {
|
||||||
isPassing,
|
isPassing,
|
||||||
visiblePercent,
|
percent,
|
||||||
},
|
},
|
||||||
} = useModel('progress', courseId);
|
} = useModel('progress', courseId);
|
||||||
|
|
||||||
const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100';
|
const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100';
|
||||||
const totalGrade = (visiblePercent * 100).toFixed(0);
|
const totalGrade = (percent * 100).toFixed(0);
|
||||||
|
|
||||||
|
const isLocaleRtl = isRtl(getLocale());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable.TableFooter className={`border-top border-primary ${bgColor}`}>
|
<DataTable.TableFooter className={`border-top border-primary ${bgColor}`}>
|
||||||
<div className="row w-100 m-0">
|
<div className="row w-100 m-0">
|
||||||
<div id="weighted-grade-summary" className="col-8 p-0 small">{intl.formatMessage(messages.weightedGradeSummary)}</div>
|
<div id="weighted-grade-summary" className="col-8 p-0 small">{intl.formatMessage(messages.weightedGradeSummary)}</div>
|
||||||
<div data-testid="gradeSummaryFooterTotalWeightedGrade" aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}%</div>
|
<div data-testid="gradeSummaryFooterTotalWeightedGrade" aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}{isLocaleRtl && '\u200f'}%</div>
|
||||||
</div>
|
</div>
|
||||||
</DataTable.TableFooter>
|
</DataTable.TableFooter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,149 +4,196 @@ const messages = defineMessages({
|
|||||||
assignmentType: {
|
assignmentType: {
|
||||||
id: 'progress.assignmentType',
|
id: 'progress.assignmentType',
|
||||||
defaultMessage: 'Assignment type',
|
defaultMessage: 'Assignment type',
|
||||||
|
description: 'Header for column that indicate type of the assignment in grade summary table',
|
||||||
},
|
},
|
||||||
backToContent: {
|
backToContent: {
|
||||||
id: 'progress.footnotes.backToContent',
|
id: 'progress.footnotes.backToContent',
|
||||||
defaultMessage: 'Back to content',
|
defaultMessage: 'Back to content',
|
||||||
|
description: 'Text for button that redirects to contnet',
|
||||||
},
|
},
|
||||||
courseGradeBody: {
|
courseGradeBody: {
|
||||||
id: 'progress.courseGrade.body',
|
id: 'progress.courseGrade.body',
|
||||||
defaultMessage: 'This represents your weighted grade against the grade needed to pass this course.',
|
defaultMessage: 'This represents your weighted grade against the grade needed to pass this course.',
|
||||||
|
description: 'This text is shown to explain the meaning of the (grade bar) chart',
|
||||||
},
|
},
|
||||||
courseGradeBarAltText: {
|
courseGradeBarAltText: {
|
||||||
id: 'progress.courseGrade.gradeBar.altText',
|
id: 'progress.courseGrade.gradeBar.altText',
|
||||||
defaultMessage: 'Your current grade is {currentGrade}%. A weighted grade of {passingGrade}% is required to pass in this course.',
|
defaultMessage: 'Your current grade is {currentGrade}%. A weighted grade of {passingGrade}% is required to pass in this course.',
|
||||||
|
description: 'Alt text for the grade chart bar',
|
||||||
},
|
},
|
||||||
courseGradeFooterGenericPassing: {
|
courseGradeFooterGenericPassing: {
|
||||||
id: 'progress.courseGrade.footer.generic.passing',
|
id: 'progress.courseGrade.footer.generic.passing',
|
||||||
defaultMessage: 'You’re currently passing this course',
|
defaultMessage: 'You’re currently passing this course',
|
||||||
|
description: 'This shown when learner weighted grade is greater or equal course passing grade',
|
||||||
},
|
},
|
||||||
courseGradeFooterNonPassing: {
|
courseGradeFooterNonPassing: {
|
||||||
id: 'progress.courseGrade.footer.nonPassing',
|
id: 'progress.courseGrade.footer.nonPassing',
|
||||||
defaultMessage: 'A weighted grade of {passingGrade}% is required to pass in this course',
|
defaultMessage: 'A weighted grade of {passingGrade}% is required to pass in this course',
|
||||||
|
description: 'This shown when learner weighted grade is less than course passing grade',
|
||||||
},
|
},
|
||||||
courseGradeFooterPassingWithGrade: {
|
courseGradeFooterPassingWithGrade: {
|
||||||
id: 'progress.courseGrade.footer.passing',
|
id: 'progress.courseGrade.footer.passing',
|
||||||
defaultMessage: 'You’re currently passing this course with a grade of {letterGrade} ({minGrade}-{maxGrade}%)',
|
defaultMessage: 'You’re currently passing this course with a grade of {letterGrade} ({minGrade}-{maxGrade}%)',
|
||||||
|
description: 'This shown when learner weighted grade is greater or equal course passing grade amd course is using letter grade',
|
||||||
},
|
},
|
||||||
courseGradePreviewHeaderLocked: {
|
courseGradePreviewHeaderLocked: {
|
||||||
id: 'progress.courseGrade.preview.headerLocked',
|
id: 'progress.courseGrade.preview.headerLocked',
|
||||||
defaultMessage: 'locked feature',
|
defaultMessage: 'locked feature',
|
||||||
|
description: 'This when (progress page) feature is locked, sometimes learner needs to upgrade to get insight about their progress',
|
||||||
},
|
},
|
||||||
courseGradePreviewHeaderLimited: {
|
courseGradePreviewHeaderLimited: {
|
||||||
id: 'progress.courseGrade.preview.headerLimited',
|
id: 'progress.courseGrade.preview.headerLimited',
|
||||||
defaultMessage: 'limited feature',
|
defaultMessage: 'limited feature',
|
||||||
|
description: 'This when (progress page) feature is partially locked, it means leaners can see their progress but not get to a certificate',
|
||||||
},
|
},
|
||||||
courseGradePreviewHeaderAriaHidden: {
|
courseGradePreviewHeaderAriaHidden: {
|
||||||
id: 'progress.courseGrade.preview.header.ariaHidden',
|
id: 'progress.courseGrade.preview.header.ariaHidden',
|
||||||
defaultMessage: 'Preview of a ',
|
defaultMessage: 'Preview of a ',
|
||||||
|
description: 'This text precedes either (locked feature) or (limited feature)',
|
||||||
},
|
},
|
||||||
courseGradePreviewUnlockCertificateBody: {
|
courseGradePreviewUnlockCertificateBody: {
|
||||||
id: 'progress.courseGrade.preview.body.unlockCertificate',
|
id: 'progress.courseGrade.preview.body.unlockCertificate',
|
||||||
defaultMessage: 'Unlock to view grades and work towards a certificate.',
|
defaultMessage: 'Unlock to view grades and work towards a certificate.',
|
||||||
|
description: 'Recommending an action for learner when they need to upgrade to view progress and get a certificate',
|
||||||
},
|
},
|
||||||
courseGradePartialPreviewUnlockCertificateBody: {
|
courseGradePartialPreviewUnlockCertificateBody: {
|
||||||
id: 'progress.courseGrade.partialpreview.body.unlockCertificate',
|
id: 'progress.courseGrade.partialpreview.body.unlockCertificate',
|
||||||
defaultMessage: 'Unlock to work towards a certificate.',
|
defaultMessage: 'Unlock to work towards a certificate.',
|
||||||
|
description: 'Recommending an action for learner when they need to upgrade to get a certificate',
|
||||||
},
|
},
|
||||||
courseGradePreviewUpgradeDeadlinePassedBody: {
|
courseGradePreviewUpgradeDeadlinePassedBody: {
|
||||||
id: 'progress.courseGrade.preview.body.upgradeDeadlinePassed',
|
id: 'progress.courseGrade.preview.body.upgradeDeadlinePassed',
|
||||||
defaultMessage: 'The deadline to upgrade in this course has passed.',
|
defaultMessage: 'The deadline to upgrade in this course has passed.',
|
||||||
|
description: 'Shown when learner no longer can upgrade',
|
||||||
},
|
},
|
||||||
courseGradePreviewUpgradeButton: {
|
courseGradePreviewUpgradeButton: {
|
||||||
id: 'progress.courseGrade.preview.button.upgrade',
|
id: 'progress.courseGrade.preview.button.upgrade',
|
||||||
defaultMessage: 'Upgrade now',
|
defaultMessage: 'Upgrade now',
|
||||||
|
description: 'Text for button that redirects to the upgrade page',
|
||||||
},
|
},
|
||||||
courseGradeRangeTooltip: {
|
courseGradeRangeTooltip: {
|
||||||
id: 'progress.courseGrade.gradeRange.tooltip',
|
id: 'progress.courseGrade.gradeRange.tooltip',
|
||||||
defaultMessage: 'Grade ranges for this course:',
|
defaultMessage: 'Grade ranges for this course:',
|
||||||
|
description: 'This shown when course is using (letter grade) to explain e.g. range for A, B, and C...etc',
|
||||||
},
|
},
|
||||||
courseOutline: {
|
courseOutline: {
|
||||||
id: 'progress.courseOutline',
|
id: 'progress.courseOutline',
|
||||||
defaultMessage: 'Course Outline',
|
defaultMessage: 'Course Outline',
|
||||||
|
description: 'Anchor text for link that redirects to (course outline) tab',
|
||||||
},
|
},
|
||||||
currentGradeLabel: {
|
currentGradeLabel: {
|
||||||
id: 'progress.courseGrade.label.currentGrade',
|
id: 'progress.courseGrade.label.currentGrade',
|
||||||
defaultMessage: 'Your current grade',
|
defaultMessage: 'Your current grade',
|
||||||
|
description: 'Text label current leaner grade on (grade bar) chart',
|
||||||
},
|
},
|
||||||
detailedGrades: {
|
detailedGrades: {
|
||||||
id: 'progress.detailedGrades',
|
id: 'progress.detailedGrades',
|
||||||
defaultMessage: 'Detailed grades',
|
defaultMessage: 'Detailed grades',
|
||||||
|
description: 'Headline for the (detailed grade) section in the progress tab',
|
||||||
},
|
},
|
||||||
detailedGradesEmpty: {
|
detailedGradesEmpty: {
|
||||||
id: 'progress.detailedGrades.emptyTable',
|
id: 'progress.detailedGrades.emptyTable',
|
||||||
defaultMessage: 'You currently have no graded problem scores.',
|
defaultMessage: 'You currently have no graded problem scores.',
|
||||||
|
description: 'It indicate that there are no problem or assignments to be scored',
|
||||||
},
|
},
|
||||||
footnotesTitle: {
|
footnotesTitle: {
|
||||||
id: 'progress.footnotes.title',
|
id: 'progress.footnotes.title',
|
||||||
defaultMessage: 'Grade summary footnotes',
|
defaultMessage: 'Grade summary footnotes',
|
||||||
|
description: 'Title for grade summary footnotes, if exists',
|
||||||
},
|
},
|
||||||
grade: {
|
grade: {
|
||||||
id: 'progress.gradeSummary.grade',
|
id: 'progress.gradeSummary.grade',
|
||||||
defaultMessage: 'Grade',
|
defaultMessage: 'Grade',
|
||||||
|
description: 'Headline for (grade column) in grade summary table',
|
||||||
},
|
},
|
||||||
grades: {
|
grades: {
|
||||||
id: 'progress.courseGrade.grades',
|
id: 'progress.courseGrade.grades',
|
||||||
defaultMessage: 'Grades',
|
defaultMessage: 'Grades',
|
||||||
|
description: 'Headline for grades section in progress tab',
|
||||||
|
},
|
||||||
|
gradesAndCredit: {
|
||||||
|
id: 'progress.courseGrade.gradesAndCredit',
|
||||||
|
defaultMessage: 'Grades & Credit',
|
||||||
|
description: 'Headline for (grades and credit) section in progress tab',
|
||||||
},
|
},
|
||||||
gradeRangeTooltipAlt: {
|
gradeRangeTooltipAlt: {
|
||||||
id: 'progress.courseGrade.gradeRange.Tooltip',
|
id: 'progress.courseGrade.gradeRange.Tooltip',
|
||||||
defaultMessage: 'Grade range tooltip',
|
defaultMessage: 'Grade range tooltip',
|
||||||
|
description: 'Alt text for icon which that triggers (tip box) for grade range',
|
||||||
},
|
},
|
||||||
gradeSummary: {
|
gradeSummary: {
|
||||||
id: 'progress.gradeSummary',
|
id: 'progress.gradeSummary',
|
||||||
defaultMessage: 'Grade summary',
|
defaultMessage: 'Grade summary',
|
||||||
|
description: 'Headline for the (grade summary) section in (grades) section in progress tab',
|
||||||
},
|
},
|
||||||
gradeSummaryLimitedAccessExplanation: {
|
gradeSummaryLimitedAccessExplanation: {
|
||||||
id: 'progress.gradeSummary.limitedAccessExplanation',
|
id: 'progress.gradeSummary.limitedAccessExplanation',
|
||||||
defaultMessage: 'You have limited access to graded assignments as part of the audit track in this course.',
|
defaultMessage: 'You have limited access to graded assignments as part of the audit track in this course.',
|
||||||
|
description: 'Text shown when learner has limited access to grade feature',
|
||||||
},
|
},
|
||||||
gradeSummaryTooltipAlt: {
|
gradeSummaryTooltipAlt: {
|
||||||
id: 'progress.gradeSummary.tooltip.alt',
|
id: 'progress.gradeSummary.tooltip.alt',
|
||||||
defaultMessage: 'Grade summary tooltip',
|
defaultMessage: 'Grade summary tooltip',
|
||||||
|
description: 'Alt text for icon which that triggers (tip box) for grade summary',
|
||||||
},
|
},
|
||||||
gradeSummaryTooltipBody: {
|
gradeSummaryTooltipBody: {
|
||||||
id: 'progress.gradeSummary.tooltip.body',
|
id: 'progress.gradeSummary.tooltip.body',
|
||||||
defaultMessage: "Your course assignment's weight is determined by your instructor. "
|
defaultMessage: "Your course assignment's weight is determined by your instructor. "
|
||||||
+ 'By multiplying your grade by the weight for that assignment type, your weighted grade is calculated. '
|
+ 'By multiplying your grade by the weight for that assignment type, your weighted grade is calculated. '
|
||||||
+ "Your weighted grade is what's used to determine if you pass the course.",
|
+ "Your weighted grade is what's used to determine if you pass the course.",
|
||||||
},
|
description: 'The content of (tip box) for the grade summary section',
|
||||||
passingGradeLabel: {
|
|
||||||
id: 'progress.courseGrade.label.passingGrade',
|
|
||||||
defaultMessage: 'Passing grade',
|
|
||||||
},
|
|
||||||
problemScoreLabel: {
|
|
||||||
id: 'progress.detailedGrades.problemScore.label',
|
|
||||||
defaultMessage: 'Problem Scores:',
|
|
||||||
},
|
|
||||||
problemScoreToggleAltText: {
|
|
||||||
id: 'progress.detailedGrades.problemScore.toggleButton',
|
|
||||||
defaultMessage: 'Toggle individual problem scores for {subsectionTitle}',
|
|
||||||
},
|
|
||||||
score: {
|
|
||||||
id: 'progress.score',
|
|
||||||
defaultMessage: 'Score',
|
|
||||||
},
|
|
||||||
weight: {
|
|
||||||
id: 'progress.weight',
|
|
||||||
defaultMessage: 'Weight',
|
|
||||||
},
|
|
||||||
weightedGrade: {
|
|
||||||
id: 'progress.weightedGrade',
|
|
||||||
defaultMessage: 'Weighted grade',
|
|
||||||
},
|
|
||||||
weightedGradeSummary: {
|
|
||||||
id: 'progress.weightedGradeSummary',
|
|
||||||
defaultMessage: 'Your current weighted grade summary',
|
|
||||||
},
|
},
|
||||||
noAccessToAssignmentType: {
|
noAccessToAssignmentType: {
|
||||||
id: 'progress.noAcessToAssignmentType',
|
id: 'progress.noAcessToAssignmentType',
|
||||||
defaultMessage: 'You do not have access to assignments of type {assignmentType}',
|
defaultMessage: 'You do not have access to assignments of type {assignmentType}',
|
||||||
|
description: 'Its alt text for locked icon which is shown if assignment type in (grade summary table) is locked',
|
||||||
},
|
},
|
||||||
noAccessToSubsection: {
|
noAccessToSubsection: {
|
||||||
id: 'progress.noAcessToSubsection',
|
id: 'progress.noAcessToSubsection',
|
||||||
defaultMessage: 'You do not have access to subsection {displayName}',
|
defaultMessage: 'You do not have access to subsection {displayName}',
|
||||||
|
description: 'Text shown when learner have limited access to grades feature',
|
||||||
},
|
},
|
||||||
|
passingGradeLabel: {
|
||||||
|
id: 'progress.courseGrade.label.passingGrade',
|
||||||
|
defaultMessage: 'Passing grade',
|
||||||
|
description: 'Label for mark on the (grade bar) chart which indicate the poisition of passing grade on the bar',
|
||||||
|
},
|
||||||
|
problemScoreLabel: {
|
||||||
|
id: 'progress.detailedGrades.problemScore.label',
|
||||||
|
defaultMessage: 'Problem Scores:',
|
||||||
|
description: 'Label text which precedes detailed view of all scores per assignment',
|
||||||
|
},
|
||||||
|
problemScoreToggleAltText: {
|
||||||
|
id: 'progress.detailedGrades.problemScore.toggleButton',
|
||||||
|
defaultMessage: 'Toggle individual problem scores for {subsectionTitle}',
|
||||||
|
description: 'Alt text for button which switches detailed view per module',
|
||||||
|
},
|
||||||
|
sectionGradeOverridden: {
|
||||||
|
id: 'progress.detailedGrades.overridden',
|
||||||
|
defaultMessage: 'Section grade has been overridden.',
|
||||||
|
description: 'This indicate that the graded score has been changed, it can happen if leaner initial assessment was not fair, might be for other reasons as well',
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
id: 'progress.score',
|
||||||
|
defaultMessage: 'Score',
|
||||||
|
description: 'It indicate how many points the learner have socred scored in particular assignment, or exam',
|
||||||
|
},
|
||||||
|
weight: {
|
||||||
|
id: 'progress.weight',
|
||||||
|
defaultMessage: 'Weight',
|
||||||
|
description: 'It indicate the weight of particular assignment on overall course grade, it is demeterined by course author',
|
||||||
|
},
|
||||||
|
weightedGrade: {
|
||||||
|
id: 'progress.weightedGrade',
|
||||||
|
defaultMessage: 'Weighted grade',
|
||||||
|
description: 'Weighed grade is calculated by (weight %) * (grade score) ',
|
||||||
|
},
|
||||||
|
weightedGradeSummary: {
|
||||||
|
id: 'progress.weightedGradeSummary',
|
||||||
|
defaultMessage: 'Your current weighted grade summary',
|
||||||
|
description: 'It the text precede the sum of weighted grades of all the assignment',
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user