Compare commits
18 Commits
v1.4.30
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edbbe42f3b | ||
|
|
3ee7e40ecd | ||
|
|
310d0844fd | ||
|
|
7fd06c2373 | ||
|
|
45b922d6e6 | ||
|
|
12574a7561 | ||
|
|
8f1c89a025 | ||
|
|
a1de3a8612 | ||
|
|
4e26247ac3 | ||
|
|
f21e6da598 | ||
|
|
650e9321b1 | ||
|
|
e8a8cca483 | ||
|
|
f5d2a34660 | ||
|
|
97d3a29a7f | ||
|
|
5b8f67e8d2 | ||
|
|
c6e33307ba | ||
|
|
a4df8f7238 | ||
|
|
15b76edb5d |
1
.env
1
.env
@@ -30,4 +30,3 @@ ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS=''
|
||||
|
||||
@@ -23,6 +23,7 @@ MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
|
||||
FACEBOOK_URL='https://www.facebook.com'
|
||||
@@ -36,4 +37,3 @@ ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS=''
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
const config = createConfig('eslint');
|
||||
const config = createConfig('eslint', {
|
||||
rules: {
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
},
|
||||
});
|
||||
|
||||
config.settings = {
|
||||
"import/resolver": {
|
||||
|
||||
10
.github/workflows/commitlint.yml
vendored
Normal file
10
.github/workflows/commitlint.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Run commitlint on the commit messages in a pull request.
|
||||
|
||||
name: Lint Commit Messages
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,6 +5,8 @@ npm-debug.log
|
||||
coverage
|
||||
|
||||
dist/
|
||||
src/i18n/transifex_input.json
|
||||
temp/babel-plugin-react-intl
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
@@ -17,3 +19,7 @@ dist/
|
||||
### Development environments ###
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
### transifex ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
|
||||
9
.tx/config
Normal file
9
.tx/config
Normal file
@@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[edx-platform.frontend-app-gradebook]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
64
Makefile
64
Makefile
@@ -2,8 +2,64 @@ npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
git add package.json
|
||||
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
git diff --exit-code package-lock.json
|
||||
transifex_resource = frontend-app-gradebook
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
test:
|
||||
npm run test
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
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 .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test is-es5
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
|
||||
.PHONY: test.npm.*
|
||||
test.npm.%: validate-no-uncommitted-package-lock-changes
|
||||
test -d node_modules || $(MAKE) requirements
|
||||
npm run $(*)
|
||||
|
||||
.PHONY: requirements
|
||||
requirements: ## install ci requirements
|
||||
npm ci
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
rm -rf $(transifex_temp)
|
||||
npm run-script i18n_extract
|
||||
|
||||
i18n.concat:
|
||||
# Gathering JSON messages into one file...
|
||||
$(transifex_utils) $(transifex_temp) $(transifex_input)
|
||||
|
||||
extract_translations: | requirements i18n.extract i18n.concat
|
||||
|
||||
# Despite the name, we actually need this target to detect changes in the incoming translated message files as well.
|
||||
detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
|
||||
@@ -22,19 +22,13 @@ For existing documentation see:
|
||||
|
||||
### What does this offer over the legacy gradebook?
|
||||
|
||||

|
||||
|
||||
The micro-frontend offers a great deal more granularity when searching for problems, an easy interface for editing grades, an
|
||||
audit trail for seeing who edited what grade and what reason they gave (if any) for doing so.
|
||||
|
||||

|
||||
|
||||
UsageProblems can be filtered by student as in the traditional gradebook, but can also be filtered by scores to see who
|
||||
scored within a certain range, and by assignment types (note: Not problem types, but categories like ‘Exams’ or
|
||||
‘Homework’).
|
||||
|
||||

|
||||
|
||||
### What does the legacy gradebook offer that this project does not?
|
||||
|
||||
This project does not (yet, at least) create any graphs, which the traditional gradebook does. It also does not give
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 254 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 240 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 319 KiB |
@@ -68,33 +68,40 @@ Confirm the following workflows:
|
||||
- [ ] Clicking "Save Grade" applies the override, shows the successful "grade has been edited" banner and updates score in grades table (may take a few seconds).
|
||||
- [ ] Opening back up the "Edit Grades" modal shows the change as an entry in the override history table.
|
||||
|
||||
- [ ] *Masters only*: "Bulk Management" allows overriding grades in bulk.
|
||||
- [ ] *Master's (or selectively-enabled) only*: "Bulk Management" allows overriding grades in bulk.
|
||||
- Open a non-masters-track course.
|
||||
- [ ] Verify that the "Bulk Management" tab does not appear.
|
||||
- [ ] Verify that the "Bulk Management" button does not appear.
|
||||
- [ ] Verify that the "Download Interventions" interface does not appear.
|
||||
- Open a masters-track course.
|
||||
- [ ] Verify that the "Bulk Management" tab appears to the right of the "Grades" tab.
|
||||
- [ ] Verify that the "Bulk Management" button appears.
|
||||
- Click the "Bulk Management" button. This downloads existing student/assignment info.
|
||||
- [ ] Verify that the "Bulk Management History" button appears at the right of the header.
|
||||
- [ ] Verify that the "Download Interventions" interface appears.
|
||||
- [ ] Verify that the "Download Grades" button appears.
|
||||
- [ ] Verify that the "Import Grades" button appears.
|
||||
- Click the "Download Grades" button. This downloads existing student/assignment info.
|
||||
- [ ] Open the downloaded CSV and verify that students and assignments in the file match applied filters/searches.
|
||||
- Add values in the "new_override-{subsection-short-id}" columns for student grades to be overridden and save the CSV file.
|
||||
- [ ] Clicking the "Bulk Management" tab shows the Bulk Management page.
|
||||
- Navigate to Bulk Management History tab.
|
||||
- [ ] Clicking the "ViewBulk Management History" tab shows the Bulk Management History view.
|
||||
- [ ] The bulk management history table appears with columns: "Gradebook", "Download Summary", "Who", "When".
|
||||
- [ ] Previous bulk management imports (if applicable) appear in the table.
|
||||
- Add values in the "new_override-{subsection-short-id}" columns for student grades to be overridden and save the CSV file.
|
||||
- Navigate back to Gradebook view
|
||||
- Click the "Import Grades" button and select the modified CSV file.
|
||||
- [ ] Verify that the "CSV processing" banner appears.
|
||||
- Wait for processing to complete and reload the page. (Can take seconds to minutes depending on environment and size of the override.)
|
||||
- Navigate back to the "Bulk Management" tab.
|
||||
- [ ] Verify that Import Grades Success toast appears (and disappears after 5 seconds)
|
||||
- Navigate back to the "Bulk Management History" view.
|
||||
- [ ] Verify that a new entry appears in the results table indicating how many students were affected by the bulk grade change.
|
||||
- Click the "Download Summary" link to see the summary of changes from the bulk grade changes.
|
||||
- [ ] Verify that students are shown with modified subsections and actions: "No Action" for unchanged users, "Success" for successful overrides.
|
||||
|
||||
- [ ] *Masters only*: Interventions report shows student activity in the course.
|
||||
- Open a non-masters-track course.
|
||||
- [ ] Verify that the "Interventions" tab does not appear.
|
||||
- [ ] Verify that the "Interventions" button does not appear.
|
||||
- [ ] Verify that the "View Bulk Management History" button does not appear.
|
||||
- [ ] Verify that the "Interventions" interface does not appear.
|
||||
- [ ] Verify that the "Download Grades" and "Import Grades" buttons do not appear.
|
||||
- Open a masters-track course.
|
||||
- [ ] Verify that the "Interventions" tab appears to the right of the "Grades" tab.
|
||||
- [ ] Verify that the "Interventions" button appears.
|
||||
- Click on the "Interventions" button to generate a CSV students and activity info.
|
||||
- [ ] Verify that the "View Bulk Management History" button appears at the right of the header.
|
||||
- [ ] Verify that the "Interventions" interface appears.
|
||||
- [ ] Verify that the "Download Grades" and "Import Grades" buttons appear.
|
||||
- Click on the "Download Interventions" button to generate a CSV students and activity info.
|
||||
- Open the interventions report and verify student info and activity info appear.
|
||||
|
||||
@@ -14,7 +14,7 @@ Suggested resources:
|
||||
- [Adding Exercises and Tools](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/grading/index.html)
|
||||
- [Set the Assignment Type and Due Date for a Subsection](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/developing_course/course_subsections.html#set-the-assignment-type-and-due-date-for-a-subsection)
|
||||
|
||||
## Enable Gradebook and feature toggles for course
|
||||
## Enable Gradebook for course
|
||||
|
||||
See README.md #Quickstart for more detailed instructions.
|
||||
|
||||
@@ -25,7 +25,13 @@ As an admin user, visit Django Admin (`{lms-url}/admin`) to modify features.
|
||||
- [ ] Set name to `grades.assume_zero_grade_if_absent`, select "Active", and click "Save"
|
||||
- In Waffle_Utils > Waffle flag course overrides:
|
||||
- [ ] Add a new flag called `grades.writeable_gradebook`, select "Force On", and enable it for your course
|
||||
- [ ] Add a new flag called `grades.bulk_management`, select "Force On", and enable it for your course
|
||||
|
||||
## Enable Bulk Management
|
||||
|
||||
Bulk Management is an added feature to allow modifying grades in bulk via CSV upload. Bulk Management is default enabled for Master's track courses but can be selectively enabled for other courses with a waffle flag following the steps below.
|
||||
|
||||
- In Waffle_Utils > Waffle flag course overrides:
|
||||
- [ ] Add a new flag called `grades.bulk_management`, select "Force On", and enable it for your course.
|
||||
|
||||
## Create a Master's track for testing Master's-only features
|
||||
|
||||
|
||||
@@ -8,4 +8,8 @@ module.exports = createConfig('jest', {
|
||||
snapshotSerializers: [
|
||||
'enzyme-to-json/serializer',
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
],
|
||||
});
|
||||
|
||||
128
package-lock.json
generated
128
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.4.36",
|
||||
"version": "1.4.47",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -3241,56 +3241,90 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-component-footer": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-10.1.1.tgz",
|
||||
"integrity": "sha512-NF64Dgznzy4A4nsfzC+HXAmtcmHwAAQCp17qtyUQ8a+LJhXUi/eXbrYhD8t0GboDHCI2HfRK2YOeiVJ3XjsNsw==",
|
||||
"version": "10.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-10.1.6.tgz",
|
||||
"integrity": "sha512-ls9E2Lvy0WNaZEWA7Imxq/qrw9C0HiTZRh4mavzbJsBvuo9QwZh0OsA/jam5dQnf4IlgHwGwP8cIDEujdhQ/vg==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.32",
|
||||
"@fortawesome/free-brands-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.8.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.13"
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.1.15"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.32.tgz",
|
||||
"integrity": "sha512-ux2EDjKMpcdHBVLi/eWZynnPxs0BtFVXJkgHIxXRl+9ZFaHPvYamAfCzeeQFqHRjuJtX90wVnMRaMQAAlctz3w=="
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
"integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
|
||||
},
|
||||
"@fortawesome/fontawesome-svg-core": {
|
||||
"version": "1.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.32.tgz",
|
||||
"integrity": "sha512-XjqyeLCsR/c/usUpdWcOdVtWFVjPbDFBTQkn2fQRrWhhUoxriQohO2RWDxLyUM8XpD+Zzg5xwJ8gqTYGDLeGaQ==",
|
||||
"version": "1.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
|
||||
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.32"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-brands-svg-icons": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.8.2.tgz",
|
||||
"integrity": "sha512-nhEWctDOP6f+Ka10LXAFoF+6mtWidC2iQgTBGRGgydmhBtcIEwyxWVx5wQHa86A1zAMi5TnipDAYQs2qn7DD6A==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-f1witbwycL9cTENJegcmcZRYyawAFbm8+c6IirLmwbbpqz46wyjbQYLuxOc7weXFXfB7QR8/Vd2u5R3q6JYD9g==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.18"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.8.2.tgz",
|
||||
"integrity": "sha512-5tF6WOFlqqO95zY5ukSB6jliDa3jnk1p5L4K/a58ccDFsbjSkhfGuvZkRkeWxH8uMms81pZd6yQTwQrkedeJmg==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.18"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
}
|
||||
},
|
||||
"@fortawesome/react-fontawesome": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.13.tgz",
|
||||
"integrity": "sha512-/HrLnIft5Ks2511Pz6TxHBIctC9QalVscAC64sufQ4sJH/sXaQlG3uR9LCu6VpEwkBemgcBLrz/QPNP/ddbjDg==",
|
||||
"version": "0.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.15.tgz",
|
||||
"integrity": "sha512-/HFHdcoLESxxMkqZAcZ6RXDJ69pVApwdwRos/B2kiMWxDSAX2dFK8Er2/+rG+RsrzWB/dsAyjefLmemgmfE18g==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@edx/frontend-component-header": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-2.2.5.tgz",
|
||||
"integrity": "sha512-8ZfdQBgp5beLd3r+xIxpDQT4/eYggUrzkHe4iJk322lYqmjAEHio9jrLnHv7lW/B8ooH3qZNl+sJlT2UcWRxmg==",
|
||||
"requires": {
|
||||
"babel-polyfill": "6.26.0",
|
||||
"react-responsive": "8.0.3",
|
||||
"react-transition-group": "4.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-responsive": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.0.3.tgz",
|
||||
"integrity": "sha512-F9VXyLao7O8XHXbLjQbIr4+mC6Zr0RDTwNjd7ixTmYEAyKyNanBkLkFchNaMZgszoSK6PgSs/3m/QDWw33/gpg==",
|
||||
"requires": {
|
||||
"hyphenate-style-name": "^1.0.0",
|
||||
"matchmediaquery": "^0.3.0",
|
||||
"prop-types": "^15.6.1",
|
||||
"shallow-equal": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz",
|
||||
"integrity": "sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@edx/frontend-platform": {
|
||||
"version": "1.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-1.9.5.tgz",
|
||||
@@ -3428,11 +3462,18 @@
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-regular-svg-icons": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.8.2.tgz",
|
||||
"integrity": "sha512-499WODlsDcXA9hM+Y3ZqoWj1kKhVLCHS8PnJs3zEaoPr5W6yrE9Jnp3C9Hb9KpwnRMh3IRXZ3XqxyUaQFMMRFw==",
|
||||
"version": "5.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz",
|
||||
"integrity": "sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.18"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.36"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.36",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
|
||||
"integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
@@ -6576,7 +6617,6 @@
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
|
||||
"integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"core-js": "^2.5.0",
|
||||
@@ -6586,14 +6626,12 @@
|
||||
"core-js": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
|
||||
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=",
|
||||
"dev": true
|
||||
"integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6631,7 +6669,6 @@
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "^2.4.0",
|
||||
"regenerator-runtime": "^0.11.0"
|
||||
@@ -6640,14 +6677,12 @@
|
||||
"core-js": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -24779,6 +24814,12 @@
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"reactifex": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reactifex/-/reactifex-1.1.1.tgz",
|
||||
"integrity": "sha512-HH2N/b5tRxh7ypIgCRsiBl/CTxRkTEPf9DhIstaM6hne4WiwM5/bBbWuvVlRZc/i3FdqZED3pZ//6n4mtxma4w==",
|
||||
"dev": true
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
|
||||
@@ -26455,6 +26496,11 @@
|
||||
"kind-of": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"shallow-equal": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
|
||||
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.4.39",
|
||||
"version": "1.4.47",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -10,6 +10,7 @@
|
||||
"build": "fedx-scripts webpack",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"prepush": "npm run lint",
|
||||
@@ -27,7 +28,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "10.1.1",
|
||||
"@edx/frontend-component-footer": "10.1.6",
|
||||
"@edx/frontend-component-header": "2.2.5",
|
||||
"@edx/frontend-platform": "1.9.5",
|
||||
"@edx/paragon": "14.16.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
@@ -75,6 +77,7 @@
|
||||
"jest": "24.9.0",
|
||||
"react-dev-utils": "^5.0.3",
|
||||
"react-test-renderer": "^16.10.1",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"semantic-release": "^17.2.3",
|
||||
"travis-deploy-once": "^5.0.11"
|
||||
|
||||
34
src/App.jsx
Executable file
34
src/App.jsx
Executable file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
import './App.scss';
|
||||
|
||||
const App = () => (
|
||||
<AppProvider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routePath}
|
||||
component={GradebookPage}
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
export default App;
|
||||
@@ -9,8 +9,10 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
|
||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
|
||||
@import "./components/GradesTab/GradesTab";
|
||||
@import "./components/GradesView/GradesView";
|
||||
@import "./components/BulkManagementHistoryView/BulkManagementHistoryView";
|
||||
@import "./components/WithSidebar/WithSidebar";
|
||||
@import "./components/GradebookFilters/GradebookFilters";
|
||||
|
||||
74
src/App.test.jsx
Normal file
74
src/App.test.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
|
||||
import App from './App';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
BrowserRouter: () => 'BrowserRouter',
|
||||
Route: () => 'Route',
|
||||
Switch: () => 'Switch',
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppProvider: () => 'AppProvider',
|
||||
}));
|
||||
jest.mock('data/constants/app', () => ({
|
||||
routePath: '/:courseId',
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
jest.mock('data/store', () => 'testStore');
|
||||
jest.mock('containers/GradebookPage', () => 'GradebookPage');
|
||||
jest.mock('@edx/frontend-component-header', () => 'Header');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
|
||||
describe('App router component', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<App />)).toMatchSnapshot();
|
||||
});
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.childAt(0);
|
||||
});
|
||||
describe('AppProvider', () => {
|
||||
test('AppProvider is the parent component, passed the redux store props', () => {
|
||||
expect(el.type()).toBe(AppProvider);
|
||||
expect(el.props().store).toEqual(store);
|
||||
});
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('first child of AppProvider', () => {
|
||||
expect(router.type()).toBe(Router);
|
||||
});
|
||||
test('Header is above/outside-of the routing', () => {
|
||||
expect(router.childAt(0).childAt(0).type()).toBe(Header);
|
||||
expect(router.childAt(0).childAt(1).type()).toBe('main');
|
||||
});
|
||||
test('Routing - GradebookPage is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path={routePath} component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
});
|
||||
});
|
||||
});
|
||||
23
src/__snapshots__/App.test.jsx.snap
Normal file
23
src/__snapshots__/App.test.jsx.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot 1`] = `
|
||||
<AppProvider
|
||||
store="testStore"
|
||||
>
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="GradebookPage"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</AppProvider>
|
||||
`;
|
||||
@@ -1,20 +1,22 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import * as appConstants from 'data/constants/app';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
const { messages: { BulkManagementTab: messages } } = appConstants;
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <BulkManagementAlerts />
|
||||
* Alerts to display at the top of the BulkManagement tab
|
||||
*/
|
||||
export const BulkManagementAlerts = ({ bulkImportError, uploadSuccess }) => (
|
||||
export const BulkManagementAlerts = ({
|
||||
bulkImportError,
|
||||
uploadSuccess,
|
||||
}) => (
|
||||
<>
|
||||
<Alert
|
||||
variant="danger"
|
||||
@@ -28,7 +30,7 @@ export const BulkManagementAlerts = ({ bulkImportError, uploadSuccess }) => (
|
||||
show={uploadSuccess}
|
||||
dismissible={false}
|
||||
>
|
||||
{messages.successDialog}
|
||||
<FormattedMessage {...messages.successDialog} />
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
@@ -1,12 +1,17 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import * as appConstants from 'data/constants/app';
|
||||
import messages from './messages';
|
||||
|
||||
import { BulkManagementAlerts, mapStateToProps } from './BulkManagementAlerts';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Alert: () => 'Alert',
|
||||
}));
|
||||
@@ -61,8 +66,8 @@ describe('BulkManagementAlerts', () => {
|
||||
});
|
||||
test('open success alert with messages.successDialog content', () => {
|
||||
expect(el.childAt(1).is(Alert)).toEqual(true);
|
||||
expect(el.childAt(1).children().text()).toEqual(
|
||||
appConstants.messages.BulkManagementTab.successDialog,
|
||||
expect(el.childAt(1).children().getElement()).toEqual(
|
||||
<FormattedMessage {...messages.successDialog} />,
|
||||
);
|
||||
expect(el.childAt(1).props().show).toEqual(true);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
.bulk-management-history-view {
|
||||
.help-text {
|
||||
margin-bottom: 40px;
|
||||
max-width: 70%;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
|
||||
import { bulkManagementColumns, messages } from 'data/constants/app';
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import ResultsSummary from './ResultsSummary';
|
||||
|
||||
export const mapHistoryRows = ({
|
||||
@@ -21,21 +22,15 @@ export const mapHistoryRows = ({
|
||||
...rest,
|
||||
});
|
||||
|
||||
const { hints } = messages.BulkManagementTab;
|
||||
|
||||
/**
|
||||
* <HistoryTable />
|
||||
* Table with history of bulk management uploads, including a results summary which
|
||||
* displays total, skipped, and failed uploads
|
||||
*/
|
||||
export const HistoryTable = ({ bulkManagementHistory }) => (
|
||||
export const HistoryTable = ({
|
||||
bulkManagementHistory,
|
||||
}) => (
|
||||
<>
|
||||
<p>
|
||||
{hints[0]}
|
||||
<br />
|
||||
{hints[1]}
|
||||
</p>
|
||||
|
||||
<Table
|
||||
data={bulkManagementHistory.map(mapHistoryRows)}
|
||||
hasFixedColumnWidths
|
||||
@@ -55,7 +50,6 @@ HistoryTable.propTypes = {
|
||||
timeUploaded: PropTypes.string.isRequired,
|
||||
resultsSummary: PropTypes.shape({
|
||||
rowId: PropTypes.number.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
}),
|
||||
})),
|
||||
@@ -4,11 +4,15 @@ import { shallow } from 'enzyme';
|
||||
import { Table } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { bulkManagementColumns, messages } from 'data/constants/app';
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
|
||||
import ResultsSummary from './ResultsSummary';
|
||||
import { HistoryTable, mapStateToProps } from './HistoryTable';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Table: () => 'Table',
|
||||
}));
|
||||
@@ -52,19 +56,9 @@ describe('HistoryTable', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<HistoryTable {...props} />);
|
||||
});
|
||||
const snapshotSegments = [
|
||||
'hints display',
|
||||
'formatted table',
|
||||
];
|
||||
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
|
||||
test('snapshot - loads formatted table', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('hints with break in between', () => {
|
||||
const hints = el.find('p');
|
||||
expect(hints.childAt(0).text()).toEqual(messages.BulkManagementTab.hints[0]);
|
||||
expect(hints.childAt(1).is('br')).toEqual(true);
|
||||
expect(hints.childAt(2).text()).toEqual(messages.BulkManagementTab.hints[1]);
|
||||
});
|
||||
describe('history table', () => {
|
||||
let table;
|
||||
beforeEach(() => {
|
||||
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import { Hyperlink, Icon } from '@edx/paragon';
|
||||
import { Download } from '@edx/paragon/icons';
|
||||
|
||||
import { bulkGradesUrlByCourseAndRow } from 'data/constants/api';
|
||||
import lms from 'data/services/lms';
|
||||
|
||||
/**
|
||||
* <ResultsSummary {...{ courseId, rowId, text }} />
|
||||
@@ -15,12 +15,11 @@ import { bulkGradesUrlByCourseAndRow } from 'data/constants/api';
|
||||
* @param {string} text - summary string
|
||||
*/
|
||||
const ResultsSummary = ({
|
||||
courseId,
|
||||
rowId,
|
||||
text,
|
||||
}) => (
|
||||
<Hyperlink
|
||||
href={bulkGradesUrlByCourseAndRow(courseId, rowId)}
|
||||
href={lms.urls.bulkGradesUrlByRow(rowId)}
|
||||
destination="www.edx.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -32,7 +31,6 @@ const ResultsSummary = ({
|
||||
);
|
||||
|
||||
ResultsSummary.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
rowId: PropTypes.number.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { shallow } from 'enzyme';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { Download } from '@edx/paragon/icons';
|
||||
|
||||
import * as api from 'data/constants/api';
|
||||
import lms from 'data/services/lms';
|
||||
import ResultsSummary from './ResultsSummary';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
@@ -14,13 +14,14 @@ jest.mock('@edx/paragon', () => ({
|
||||
jest.mock('@edx/paragon/icons', () => ({
|
||||
Download: 'DownloadIcon',
|
||||
}));
|
||||
jest.mock('data/constants/api', () => ({
|
||||
bulkGradesUrlByCourseAndRow: jest.fn((courseId, rowId) => ({ url: { courseId, rowId } })),
|
||||
jest.mock('data/services/lms', () => ({
|
||||
urls: {
|
||||
bulkGradesUrlByRow: jest.fn((rowId) => ({ url: { rowId } })),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ResultsSummary component', () => {
|
||||
const props = {
|
||||
courseId: 'classy',
|
||||
rowId: 42,
|
||||
text: 'texty',
|
||||
};
|
||||
@@ -41,7 +42,7 @@ describe('ResultsSummary component', () => {
|
||||
expect(el.props().rel).toEqual('noopener noreferrer');
|
||||
});
|
||||
test('Hyperlink has href to bulkGradesUrl', () => {
|
||||
expect(el.props().href).toEqual(api.bulkGradesUrlByCourseAndRow(props.courseId, props.rowId));
|
||||
expect(el.props().href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
|
||||
});
|
||||
test('displays Download Icon and text', () => {
|
||||
const icon = el.childAt(0);
|
||||
@@ -12,7 +12,11 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - b
|
||||
show={false}
|
||||
variant="success"
|
||||
>
|
||||
CSV processing. File uploads may take several minutes to complete.
|
||||
<FormattedMessage
|
||||
defaultMessage="CSV processing. File uploads may take several minutes to complete."
|
||||
description="Success Dialog message in BulkManagement Tab File Upload Form"
|
||||
id="gradebook.BulkManagementHistoryView.successDialog"
|
||||
/>
|
||||
</Alert>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -31,7 +35,11 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - d
|
||||
show={true}
|
||||
variant="success"
|
||||
>
|
||||
CSV processing. File uploads may take several minutes to complete.
|
||||
<FormattedMessage
|
||||
defaultMessage="CSV processing. File uploads may take several minutes to complete."
|
||||
description="Success Dialog message in BulkManagement Tab File Upload Form"
|
||||
id="gradebook.BulkManagementHistoryView.successDialog"
|
||||
/>
|
||||
</Alert>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -41,13 +41,8 @@ Array [
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`HistoryTable component snapshot snapshot - loads hints display, formatted table 1`] = `
|
||||
exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
|
||||
<Fragment>
|
||||
<p>
|
||||
Results appear in the table below.
|
||||
<br />
|
||||
Grade processing may take a few seconds.
|
||||
</p>
|
||||
<Table
|
||||
className="table-striped"
|
||||
columns={
|
||||
@@ -6,7 +6,6 @@ exports[`ResultsSummary component snapshot - safe hyperlink with bulkGradesUrl w
|
||||
href={
|
||||
Object {
|
||||
"url": Object {
|
||||
"courseId": "classy",
|
||||
"rowId": 42,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BulkManagementHistoryView component snapshot snapshot - loads heading from messages.BulkManagementHistoryView.heading, <BulkManagementAlerts />, <HistoryTable /> 1`] = `
|
||||
<div
|
||||
className="bulk-management-history-view"
|
||||
>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
defaultMessage="Bulk Management History"
|
||||
description="Heading text for BulkManagement History Tab"
|
||||
id="gradebook.BulkManagementHistoryView.heading"
|
||||
/>
|
||||
</h4>
|
||||
<p
|
||||
className="help-text"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override."
|
||||
description="Bulk Management History View help text"
|
||||
id="gradebook.BulkManagementHistoryView"
|
||||
/>
|
||||
</p>
|
||||
<BulkManagementAlerts />
|
||||
<HistoryTable />
|
||||
</div>
|
||||
`;
|
||||
24
src/components/BulkManagementHistoryView/index.jsx
Normal file
24
src/components/BulkManagementHistoryView/index.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import BulkManagementAlerts from './BulkManagementAlerts';
|
||||
import HistoryTable from './HistoryTable';
|
||||
|
||||
/**
|
||||
* <BulkManagementHistoryView />
|
||||
* top-level view for managing uploads of bulk management override csvs.
|
||||
*/
|
||||
export const BulkManagementHistoryView = () => (
|
||||
<div className="bulk-management-history-view">
|
||||
<h4><FormattedMessage {...messages.heading} /></h4>
|
||||
<p className="help-text">
|
||||
<FormattedMessage {...messages.helpText} />
|
||||
</p>
|
||||
<BulkManagementAlerts />
|
||||
<HistoryTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default BulkManagementHistoryView;
|
||||
@@ -1,28 +1,26 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { messages } from 'data/constants/app';
|
||||
import { BulkManagementTab } from '.';
|
||||
import { BulkManagementHistoryView } from '.';
|
||||
import BulkManagementAlerts from './BulkManagementAlerts';
|
||||
import FileUploadForm from './FileUploadForm';
|
||||
import HistoryTable from './HistoryTable';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts');
|
||||
jest.mock('./FileUploadForm', () => 'FileUploadForm');
|
||||
jest.mock('./HistoryTable', () => 'HistoryTable');
|
||||
|
||||
describe('BulkManagementTab', () => {
|
||||
describe('BulkManagementHistoryView', () => {
|
||||
describe('component', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<BulkManagementTab />);
|
||||
el = shallow(<BulkManagementHistoryView />);
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
const snapshotSegments = [
|
||||
'heading from messages.BulkManagementTab.heading',
|
||||
'heading from messages.BulkManagementHistoryView.heading',
|
||||
'<BulkManagementAlerts />',
|
||||
'<FileUploadForm />',
|
||||
'<HistoryTable />',
|
||||
];
|
||||
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
|
||||
@@ -30,12 +28,15 @@ describe('BulkManagementTab', () => {
|
||||
});
|
||||
test('heading - h4 loaded from messages', () => {
|
||||
const heading = el.find('h4');
|
||||
expect(heading.text()).toEqual(messages.BulkManagementTab.heading);
|
||||
expect(heading.getElement()).toEqual((
|
||||
<h4>
|
||||
<FormattedMessage {...messages.heading} />
|
||||
</h4>
|
||||
));
|
||||
});
|
||||
test('heading, then alerts, then upload form, then table', () => {
|
||||
expect(el.childAt(0).is('h4')).toEqual(true);
|
||||
expect(el.childAt(1).is(BulkManagementAlerts)).toEqual(true);
|
||||
expect(el.childAt(2).is(FileUploadForm)).toEqual(true);
|
||||
expect(el.childAt(2).is(BulkManagementAlerts)).toEqual(true);
|
||||
expect(el.childAt(3).is(HistoryTable)).toEqual(true);
|
||||
});
|
||||
});
|
||||
21
src/components/BulkManagementHistoryView/messages.js
Normal file
21
src/components/BulkManagementHistoryView/messages.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: {
|
||||
id: 'gradebook.BulkManagementHistoryView.heading',
|
||||
defaultMessage: 'Bulk Management History',
|
||||
description: 'Heading text for BulkManagement History Tab',
|
||||
},
|
||||
helpText: {
|
||||
id: 'gradebook.BulkManagementHistoryView',
|
||||
defaultMessage: 'Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override.',
|
||||
description: 'Bulk Management History View help text',
|
||||
},
|
||||
successDialog: {
|
||||
id: 'gradebook.BulkManagementHistoryView.successDialog',
|
||||
defaultMessage: 'CSV processing. File uploads may take several minutes to complete.',
|
||||
description: 'Success Dialog message in BulkManagement Tab File Upload Form',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,35 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileUploadForm component snapshot snapshot - loads export form w/ alerts and file input, import btn 1`] = `
|
||||
<React.Fragment>
|
||||
<Form
|
||||
action="fakeUrl"
|
||||
inline={false}
|
||||
method="post"
|
||||
>
|
||||
<FormGroup
|
||||
as="div"
|
||||
controlId="csv"
|
||||
isInvalid={false}
|
||||
isValid={false}
|
||||
>
|
||||
<ForwardRef
|
||||
as="input"
|
||||
className="d-none"
|
||||
label="Upload Grade CSV"
|
||||
onChange={[MockFunction this.handleFileInputChange]}
|
||||
plaintext={false}
|
||||
type="file"
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
<ForwardRef
|
||||
active={false}
|
||||
disabled={false}
|
||||
onClick={[MockFunction this.handleClickImportGrades]}
|
||||
variant="primary"
|
||||
>
|
||||
Import Grades
|
||||
</ForwardRef>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BulkManagementTab component snapshot snapshot - loads heading from messages.BulkManagementTab.heading, <BulkManagementAlerts />, <FileUploadForm />, <HistoryTable /> 1`] = `
|
||||
<div>
|
||||
<h4>
|
||||
Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload.
|
||||
</h4>
|
||||
<BulkManagementAlerts />
|
||||
<FileUploadForm />
|
||||
<HistoryTable />
|
||||
</div>
|
||||
`;
|
||||
@@ -1,22 +0,0 @@
|
||||
/* eslint-disable react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
|
||||
import { messages } from 'data/constants/app';
|
||||
import BulkManagementAlerts from './BulkManagementAlerts';
|
||||
import FileUploadForm from './FileUploadForm';
|
||||
import HistoryTable from './HistoryTable';
|
||||
|
||||
/**
|
||||
* <BulkManagementTab />
|
||||
* top-level view for managing uploads of bulk management override csvs.
|
||||
*/
|
||||
export const BulkManagementTab = () => (
|
||||
<div>
|
||||
<h4>{messages.BulkManagementTab.heading}</h4>
|
||||
<BulkManagementAlerts />
|
||||
<FileUploadForm />
|
||||
<HistoryTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default BulkManagementTab;
|
||||
@@ -7,7 +7,13 @@ exports[`AssignmentFilter Component snapshots basic snapshot 1`] = `
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="assignment"
|
||||
label="Assignment"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Assignment filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.assignmentFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleChange]}
|
||||
options={
|
||||
Array [
|
||||
|
||||
@@ -3,10 +3,13 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from '../messages';
|
||||
import SelectGroup from '../SelectGroup';
|
||||
|
||||
const { fetchGradesIfAssignmentGradeFiltersSet } = thunkActions.grades;
|
||||
@@ -46,7 +49,7 @@ export class AssignmentFilter extends React.Component {
|
||||
<div className="student-filters">
|
||||
<SelectGroup
|
||||
id="assignment"
|
||||
label="Assignment"
|
||||
label={<FormattedMessage {...messages.assignment} />}
|
||||
value={this.props.selectedAssignment}
|
||||
onChange={this.handleChange}
|
||||
disabled={this.props.assignmentFilterOptions.length === 0}
|
||||
@@ -64,7 +67,6 @@ AssignmentFilter.defaultProps = {
|
||||
|
||||
AssignmentFilter.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
|
||||
// redux
|
||||
assignmentFilterOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
|
||||
@@ -69,7 +69,6 @@ describe('AssignmentFilter', () => {
|
||||
el = mount(<AssignmentFilter {...props} />);
|
||||
el.instance().handleChange(event);
|
||||
});
|
||||
|
||||
it('calls props.updateAssignmentFilter with selection', () => {
|
||||
expect(props.updateAssignmentFilter).toHaveBeenCalledWith({
|
||||
label: newAssgn,
|
||||
@@ -87,6 +86,30 @@ describe('AssignmentFilter', () => {
|
||||
const method = props.fetchGradesIfAssignmentGradeFiltersSet;
|
||||
expect(method).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('no selected option', () => {
|
||||
const value = 'fake';
|
||||
beforeEach(() => {
|
||||
el = mount(<AssignmentFilter {...props} />);
|
||||
el.instance().handleChange({ target: { value } });
|
||||
});
|
||||
it('calls props.updateAssignmentFilter with selection', () => {
|
||||
expect(props.updateAssignmentFilter).toHaveBeenCalledWith({
|
||||
label: value,
|
||||
type: undefined,
|
||||
id: undefined,
|
||||
});
|
||||
});
|
||||
it('calls props.updateQueryParams with selected assignment id',
|
||||
() => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith({
|
||||
assignment: undefined,
|
||||
});
|
||||
});
|
||||
it('calls props.fetchGradesIfAssignmentGradeFiltersSet', () => {
|
||||
const method = props.fetchGradesIfAssignmentGradeFiltersSet;
|
||||
expect(method).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
|
||||
@@ -7,14 +7,26 @@ exports[`AssignmentGradeFilter Component snapshots buttons and groups disabled i
|
||||
<PercentGroup
|
||||
disabled={true}
|
||||
id="assignmentGradeMin"
|
||||
label="Min Grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Min Grade"
|
||||
description="Min-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.minGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleSetMin]}
|
||||
value="2"
|
||||
/>
|
||||
<PercentGroup
|
||||
disabled={true}
|
||||
id="assignmentGradeMax"
|
||||
label="Max Grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Max Grade"
|
||||
description="Max-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.maxGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleSetMax]}
|
||||
value="98"
|
||||
/>
|
||||
@@ -42,14 +54,26 @@ exports[`AssignmentGradeFilter Component snapshots smoke test 1`] = `
|
||||
<PercentGroup
|
||||
disabled={false}
|
||||
id="assignmentGradeMin"
|
||||
label="Min Grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Min Grade"
|
||||
description="Min-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.minGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleSetMin]}
|
||||
value="2"
|
||||
/>
|
||||
<PercentGroup
|
||||
disabled={false}
|
||||
id="assignmentGradeMax"
|
||||
label="Max Grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Max Grade"
|
||||
description="Max-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.maxGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleSetMax]}
|
||||
value="98"
|
||||
/>
|
||||
|
||||
@@ -3,12 +3,14 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from '../messages';
|
||||
import PercentGroup from '../PercentGroup';
|
||||
|
||||
export class AssignmentGradeFilter extends React.Component {
|
||||
@@ -34,19 +36,21 @@ export class AssignmentGradeFilter extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { assignmentGradeMin, assignmentGradeMax } = this.props.localAssignmentLimits;
|
||||
const {
|
||||
localAssignmentLimits: { assignmentGradeMax, assignmentGradeMin },
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="grade-filter-inputs">
|
||||
<PercentGroup
|
||||
id="assignmentGradeMin"
|
||||
label="Min Grade"
|
||||
label={<FormattedMessage {...messages.minGrade} />}
|
||||
value={assignmentGradeMin}
|
||||
disabled={!this.props.selectedAssignment}
|
||||
onChange={this.handleSetMin}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="assignmentGradeMax"
|
||||
label="Max Grade"
|
||||
label={<FormattedMessage {...messages.maxGrade} />}
|
||||
value={assignmentGradeMax}
|
||||
disabled={!this.props.selectedAssignment}
|
||||
onChange={this.handleSetMax}
|
||||
|
||||
@@ -7,7 +7,13 @@ exports[`AssignmentTypeFilter Component snapshots SelectGroup disabled if no ass
|
||||
<SelectGroup
|
||||
disabled={true}
|
||||
id="assignment-types"
|
||||
label="Assignment Types"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment Types"
|
||||
description="Assignment Types filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.assignmentTypesLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleChange]}
|
||||
options={
|
||||
Array [
|
||||
@@ -40,7 +46,13 @@ exports[`AssignmentTypeFilter Component snapshots smoke test 1`] = `
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="assignment-types"
|
||||
label="Assignment Types"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment Types"
|
||||
description="Assignment Types filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.assignmentTypesLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleChange]}
|
||||
options={
|
||||
Array [
|
||||
|
||||
@@ -3,9 +3,13 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import messages from '../messages';
|
||||
|
||||
export class AssignmentTypeFilter extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -34,7 +38,7 @@ export class AssignmentTypeFilter extends React.Component {
|
||||
<div className="student-filters">
|
||||
<SelectGroup
|
||||
id="assignment-types"
|
||||
label="Assignment Types"
|
||||
label={<FormattedMessage {...messages.assignmentTypes} />}
|
||||
value={this.props.selectedAssignmentType}
|
||||
onChange={this.handleChange}
|
||||
disabled={this.props.assignmentFilterOptions.length === 0}
|
||||
|
||||
@@ -7,13 +7,25 @@ exports[`CourseGradeFilter Component snapshots basic snapshot 1`] = `
|
||||
>
|
||||
<PercentGroup
|
||||
id="minimum-grade"
|
||||
label="Min Grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Min Grade"
|
||||
description="Min-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.minGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleUpdateMin]}
|
||||
value="5"
|
||||
/>
|
||||
<PercentGroup
|
||||
id="maximum-grade"
|
||||
label="Max Grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Max Grade"
|
||||
description="Max-grade filter select label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.maxGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction handleUpdateMax]}
|
||||
value="92"
|
||||
/>
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from '../messages';
|
||||
import PercentGroup from '../PercentGroup';
|
||||
|
||||
export class CourseGradeFilter extends React.Component {
|
||||
@@ -41,19 +43,21 @@ export class CourseGradeFilter extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { courseGradeMin, courseGradeMax } = this.props.localCourseLimits;
|
||||
const {
|
||||
localCourseLimits: { courseGradeMin, courseGradeMax },
|
||||
} = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className="grade-filter-inputs">
|
||||
<PercentGroup
|
||||
id="minimum-grade"
|
||||
label="Min Grade"
|
||||
label={<FormattedMessage {...messages.minGrade} />}
|
||||
value={courseGradeMin}
|
||||
onChange={this.handleUpdateMin}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="maximum-grade"
|
||||
label="Max Grade"
|
||||
label={<FormattedMessage {...messages.maxGrade} />}
|
||||
value={courseGradeMax}
|
||||
onChange={this.handleUpdateMax}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,7 @@ PercentGroup.defaultProps = {
|
||||
};
|
||||
PercentGroup.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
@@ -23,7 +23,7 @@ const SelectGroup = ({
|
||||
);
|
||||
SelectGroup.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
@@ -3,10 +3,13 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from '../messages';
|
||||
import SelectGroup from '../SelectGroup';
|
||||
|
||||
export const optionFactory = ({ data, defaultOption, key }) => [
|
||||
@@ -28,7 +31,7 @@ export class StudentGroupsFilter extends React.Component {
|
||||
mapCohortsEntries() {
|
||||
return optionFactory({
|
||||
data: this.props.cohorts,
|
||||
defaultOption: 'Cohort-All',
|
||||
defaultOption: this.translate(messages.cohortAll),
|
||||
key: 'id',
|
||||
});
|
||||
}
|
||||
@@ -36,7 +39,7 @@ export class StudentGroupsFilter extends React.Component {
|
||||
mapTracksEntries() {
|
||||
return optionFactory({
|
||||
data: this.props.tracks,
|
||||
defaultOption: 'Track-All',
|
||||
defaultOption: this.translate(messages.trackAll),
|
||||
key: 'slug',
|
||||
});
|
||||
}
|
||||
@@ -65,19 +68,23 @@ export class StudentGroupsFilter extends React.Component {
|
||||
this.props.fetchGrades();
|
||||
}
|
||||
|
||||
translate(message) {
|
||||
return this.props.intl.formatMessage(message);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SelectGroup
|
||||
id="Tracks"
|
||||
label="Tracks"
|
||||
label={this.translate(messages.tracks)}
|
||||
value={this.props.selectedTrackEntry.name}
|
||||
onChange={this.updateTracks}
|
||||
options={this.mapTracksEntries()}
|
||||
/>
|
||||
<SelectGroup
|
||||
id="Cohorts"
|
||||
label="Cohorts"
|
||||
label={this.translate(messages.cohorts)}
|
||||
value={this.props.selectedCohortEntry.name}
|
||||
disabled={this.props.cohorts.length === 0}
|
||||
onChange={this.updateCohorts}
|
||||
@@ -100,6 +107,9 @@ StudentGroupsFilter.defaultProps = {
|
||||
StudentGroupsFilter.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
// redux
|
||||
cohorts: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
@@ -139,4 +149,4 @@ export const mapDispatchToProps = {
|
||||
updateTrack: actions.filters.update.track,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter);
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter));
|
||||
|
||||
@@ -63,6 +63,7 @@ describe('StudentGroupsFilter', () => {
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
cohortsByName: {
|
||||
[props.cohorts[0].name]: props.cohorts[0],
|
||||
[props.cohorts[1].name]: props.cohorts[1],
|
||||
|
||||
@@ -22,7 +22,13 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Assignments"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignments"
|
||||
description="Assignment filter group label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.assignmentsFilterLabel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Connect(AssignmentTypeFilter)
|
||||
@@ -39,7 +45,13 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Overall Grade"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Overall Grade"
|
||||
description="Overall Grade filter group label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.overallGradeFilterLabel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Connect(CourseGradeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
@@ -48,22 +60,38 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Student Groups"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Student Groups"
|
||||
description="Student Groups filter group label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.studentGroupsFilterLabel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Connect(StudentGroupsFilter)
|
||||
<InjectIntl(ShimmedIntlComponent)
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Include Course Team Members"
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Include Course Team Members"
|
||||
description="Include Course Team Members filter label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
checked={true}
|
||||
onChange={[MockFunction handleIncludeTeamMembersChange]}
|
||||
>
|
||||
Include Course Team Members
|
||||
<FormattedMessage
|
||||
defaultMessage="Include Course Team Members"
|
||||
description="Include Course Team Members filter label in Gradebook Filters"
|
||||
id="gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel"
|
||||
/>
|
||||
</Checkbox>
|
||||
</Collapsible>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -10,11 +10,13 @@ import {
|
||||
Form,
|
||||
} from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from './messages';
|
||||
import AssignmentTypeFilter from './AssignmentTypeFilter';
|
||||
import AssignmentFilter from './AssignmentFilter';
|
||||
import AssignmentGradeFilter from './AssignmentGradeFilter';
|
||||
@@ -39,13 +41,18 @@ export class GradebookFilters extends React.Component {
|
||||
}
|
||||
|
||||
collapsibleGroup = (title, content) => (
|
||||
<Collapsible title={title} defaultOpen className="filter-group mb-3">
|
||||
<Collapsible
|
||||
title={<FormattedMessage {...title} />}
|
||||
defaultOpen
|
||||
className="filter-group mb-3"
|
||||
>
|
||||
{content}
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
updateQueryParams,
|
||||
} = this.props;
|
||||
return (
|
||||
@@ -57,12 +64,12 @@ export class GradebookFilters extends React.Component {
|
||||
onClick={this.props.closeMenu}
|
||||
iconAs={Icon}
|
||||
src={Close}
|
||||
alt="Close Filters"
|
||||
aria-label="Close Filters"
|
||||
alt={intl.formatMessage(messages.closeFilters)}
|
||||
aria-label={intl.formatMessage(messages.closeFilters)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{this.collapsibleGroup('Assignments', (
|
||||
{this.collapsibleGroup(messages.assignments, (
|
||||
<div>
|
||||
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentFilter updateQueryParams={updateQueryParams} />
|
||||
@@ -70,20 +77,20 @@ export class GradebookFilters extends React.Component {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{this.collapsibleGroup('Overall Grade', (
|
||||
{this.collapsibleGroup(messages.overallGrade, (
|
||||
<CourseGradeFilter updateQueryParams={updateQueryParams} />
|
||||
))}
|
||||
|
||||
{this.collapsibleGroup('Student Groups', (
|
||||
{this.collapsibleGroup(messages.studentGroups, (
|
||||
<StudentGroupsFilter updateQueryParams={updateQueryParams} />
|
||||
))}
|
||||
|
||||
{this.collapsibleGroup('Include Course Team Members', (
|
||||
{this.collapsibleGroup(messages.includeCourseTeamMembers, (
|
||||
<Form.Checkbox
|
||||
checked={this.state.includeCourseRoleMembers}
|
||||
onChange={this.handleIncludeTeamMembersChange}
|
||||
>
|
||||
Include Course Team Members
|
||||
<FormattedMessage {...messages.includeCourseTeamMembers} />
|
||||
</Form.Checkbox>
|
||||
))}
|
||||
</>
|
||||
@@ -95,6 +102,8 @@ GradebookFilters.defaultProps = {
|
||||
};
|
||||
GradebookFilters.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
fetchGrades: PropTypes.func.isRequired,
|
||||
@@ -112,4 +121,4 @@ export const mapDispatchToProps = {
|
||||
updateIncludeCourseRoleMembers: actions.filters.update.includeCourseRoleMembers,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradebookFilters);
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(GradebookFilters));
|
||||
|
||||
71
src/components/GradebookFilters/messages.js
Normal file
71
src/components/GradebookFilters/messages.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
assignments: {
|
||||
id: 'gradebook.GradebookFilters.assignmentsFilterLabel',
|
||||
defaultMessage: 'Assignments',
|
||||
description: 'Assignment filter group label in Gradebook Filters',
|
||||
},
|
||||
overallGrade: {
|
||||
id: 'gradebook.GradebookFilters.overallGradeFilterLabel',
|
||||
defaultMessage: 'Overall Grade',
|
||||
description: 'Overall Grade filter group label in Gradebook Filters',
|
||||
},
|
||||
studentGroups: {
|
||||
id: 'gradebook.GradebookFilters.studentGroupsFilterLabel',
|
||||
defaultMessage: 'Student Groups',
|
||||
description: 'Student Groups filter group label in Gradebook Filters',
|
||||
},
|
||||
includeCourseTeamMembers: {
|
||||
id: 'gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel',
|
||||
defaultMessage: 'Include Course Team Members',
|
||||
description: 'Include Course Team Members filter label in Gradebook Filters',
|
||||
},
|
||||
assignment: {
|
||||
id: 'gradebook.GradebookFilters.assignmentFilterLabel',
|
||||
defaultMessage: 'Assignment',
|
||||
description: 'Assignment filter select label in Gradebook Filters',
|
||||
},
|
||||
assignmentTypes: {
|
||||
id: 'gradebook.GradebookFilters.assignmentTypesLabel',
|
||||
defaultMessage: 'Assignment Types',
|
||||
description: 'Assignment Types filter select label in Gradebook Filters',
|
||||
},
|
||||
maxGrade: {
|
||||
id: 'gradebook.GradebookFilters.maxGradeFilterLabel',
|
||||
defaultMessage: 'Max Grade',
|
||||
description: 'Max-grade filter select label in Gradebook Filters',
|
||||
},
|
||||
minGrade: {
|
||||
id: 'gradebook.GradebookFilters.minGradeFilterLabel',
|
||||
defaultMessage: 'Min Grade',
|
||||
description: 'Min-grade filter select label in Gradebook Filters',
|
||||
},
|
||||
cohorts: {
|
||||
id: 'gradebook.GradebookFilters.cohorts',
|
||||
defaultMessage: 'Cohorts',
|
||||
description: 'Cohorts filter select label in Gradebook Filters',
|
||||
},
|
||||
cohortAll: {
|
||||
id: 'gradebook.GradebookFilters.cohortsAll',
|
||||
defaultMessage: 'Cohort-All',
|
||||
description: 'Cohorts filter select default in Gradebook Filters',
|
||||
},
|
||||
tracks: {
|
||||
id: 'gradebook.GradebookFilters.tracks',
|
||||
defaultMessage: 'Tracks',
|
||||
description: 'Tracks filter select label in Gradebook Filters',
|
||||
},
|
||||
trackAll: {
|
||||
id: 'gradebook.GradebookFilters.trackAll',
|
||||
defaultMessage: 'Track-All',
|
||||
description: 'Tracks filter select default in Gradebook Filters',
|
||||
},
|
||||
closeFilters: {
|
||||
id: 'gradebook.GradebookFilters.closeFilters',
|
||||
defaultMessage: 'Close Filters',
|
||||
description: 'Button label for Close button in Gradebook Filters',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -46,6 +46,7 @@ describe('GradebookFilters', () => {
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
closeMenu: jest.fn().mockName('this.props.closeMenu'),
|
||||
fetchGrades: jest.fn(),
|
||||
updateIncludeCourseRoleMembers: jest.fn(),
|
||||
|
||||
@@ -13,20 +13,35 @@ exports[`GradebookHeader component snapshots default values (grades frozen, cann
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<h3>
|
||||
|
||||
fakeID
|
||||
</h3>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h3>
|
||||
fakeID
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
You are not authorized to view the gradebook for this course.
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -44,20 +59,35 @@ exports[`GradebookHeader component snapshots grades frozen, can view. grades fro
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<h3>
|
||||
|
||||
fakeID
|
||||
</h3>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h3>
|
||||
fakeID
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||
<FormattedMessage
|
||||
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
|
||||
description="Warning message in Gradebook Header for frozen messages"
|
||||
id="gradebook.GradebookHeader.frozenWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -75,26 +105,157 @@ exports[`GradebookHeader component snapshots grades frozen, cannot view unauthor
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<h3>
|
||||
|
||||
fakeID
|
||||
</h3>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||
<h3>
|
||||
fakeID
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
You are not authorized to view the gradebook for this course.
|
||||
<FormattedMessage
|
||||
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
|
||||
description="Warning message in Gradebook Header for frozen messages"
|
||||
id="gradebook.GradebookHeader.frozenWarning"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots show bulk management, active view is bulkManagementHistory view toggle view button to grades 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h3>
|
||||
fakeID
|
||||
</h3>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Return to Gradebook"
|
||||
description="Button text for button navigating to Grades view."
|
||||
id="gradebook.GradebookHeader.toGradesView"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots show bulk management, active view is grades view toggle view button to activity log 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h3>
|
||||
fakeID
|
||||
</h3>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View Bulk Management History"
|
||||
description="Button text for button navigating to Bulk Managment Activity Log"
|
||||
id="gradebook.GradebookHeader.toActivityLogButton"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2,14 +2,37 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { configuration } from 'config';
|
||||
import { views } from 'data/constants/app';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export class GradebookHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleToggleViewClick = this.handleToggleViewClick.bind(this);
|
||||
}
|
||||
|
||||
get toggleViewMessage() {
|
||||
return this.props.activeView === views.grades
|
||||
? messages.toActivityLog
|
||||
: messages.toGradesView;
|
||||
}
|
||||
|
||||
lmsInstructorDashboardUrl = courseId => (
|
||||
`${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`
|
||||
);
|
||||
|
||||
handleToggleViewClick() {
|
||||
const newView = this.props.activeView === views.grades ? views.bulkManagementHistory : views.grades;
|
||||
this.props.setView(newView);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gradebook-header">
|
||||
@@ -17,19 +40,32 @@ export class GradebookHeader extends React.Component {
|
||||
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
|
||||
className="mb-3"
|
||||
>
|
||||
<span aria-hidden="true">{'<< '}</span> Back to Dashboard
|
||||
<span aria-hidden="true">{'<< '}</span>
|
||||
<FormattedMessage {...messages.backToDashboard} />
|
||||
</a>
|
||||
<h1>Gradebook</h1>
|
||||
<h3> {this.props.courseId}</h3>
|
||||
<h1>
|
||||
<FormattedMessage {...messages.gradebook} />
|
||||
</h1>
|
||||
<div className="subtitle-row d-flex justify-content-between align-items-center">
|
||||
<h3>{this.props.courseId}</h3>
|
||||
{ this.props.showBulkManagement && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={this.handleToggleViewClick}
|
||||
>
|
||||
<FormattedMessage {...this.toggleViewMessage} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{this.props.areGradesFrozen
|
||||
&& (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||
<FormattedMessage {...messages.frozenWarning} />
|
||||
</div>
|
||||
)}
|
||||
{(this.props.canUserViewGradebook === false) && (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
You are not authorized to view the gradebook for this course.
|
||||
<FormattedMessage {...messages.unauthorizedWarning} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -42,19 +78,29 @@ GradebookHeader.defaultProps = {
|
||||
courseId: '',
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: false,
|
||||
showBulkManagement: false,
|
||||
};
|
||||
|
||||
GradebookHeader.propTypes = {
|
||||
// redux
|
||||
activeView: PropTypes.string.isRequired,
|
||||
courseId: PropTypes.string,
|
||||
areGradesFrozen: PropTypes.bool,
|
||||
canUserViewGradebook: PropTypes.bool,
|
||||
setView: PropTypes.func.isRequired,
|
||||
showBulkManagement: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
activeView: selectors.app.activeView(state),
|
||||
courseId: selectors.app.courseId(state),
|
||||
areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
|
||||
canUserViewGradebook: selectors.roles.canUserViewGradebook(state),
|
||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(GradebookHeader);
|
||||
export const mapDispatchToProps = {
|
||||
setView: actions.app.setView,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradebookHeader);
|
||||
|
||||
36
src/components/GradebookHeader/messages.js
Normal file
36
src/components/GradebookHeader/messages.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
backToDashboard: {
|
||||
id: 'gradebook.GradebookHeader.backButton',
|
||||
defaultMessage: 'Back to Dashboard',
|
||||
description: 'Button text to take user back to LMS dashboard in Gradebook Header',
|
||||
},
|
||||
gradebook: {
|
||||
id: 'gradebook.GradebookHeader.appLabel',
|
||||
defaultMessage: 'Gradebook',
|
||||
description: 'Top-level app title in Gradebook Header component',
|
||||
},
|
||||
frozenWarning: {
|
||||
id: 'gradebook.GradebookHeader.frozenWarning',
|
||||
defaultMessage: 'The grades for this course are now frozen. Editing of grades is no longer allowed.',
|
||||
description: 'Warning message in Gradebook Header for frozen messages',
|
||||
},
|
||||
unauthorizedWarning: {
|
||||
id: 'gradebook.GradebookHeader.unauthorizedWarning',
|
||||
defaultMessage: 'You are not authorized to view the gradebook for this course.',
|
||||
description: 'Warning message in Gradebook Header when user is not allowed to view the app',
|
||||
},
|
||||
toActivityLog: {
|
||||
id: 'gradebook.GradebookHeader.toActivityLogButton',
|
||||
defaultMessage: 'View Bulk Management History',
|
||||
description: 'Button text for button navigating to Bulk Managment Activity Log',
|
||||
},
|
||||
toGradesView: {
|
||||
id: 'gradebook.GradebookHeader.toGradesView',
|
||||
defaultMessage: 'Return to Gradebook',
|
||||
description: 'Button text for button navigating to Grades view.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,37 +1,119 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { GradebookHeader, mapStateToProps } from '.';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import { views } from 'data/constants/app';
|
||||
import messages from './messages';
|
||||
import { GradebookHeader, mapDispatchToProps, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setView: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { courseId: jest.fn(state => ({ courseId: state })) },
|
||||
app: {
|
||||
activeView: jest.fn(state => ({ aciveView: state })),
|
||||
courseId: jest.fn(state => ({ courseId: state })),
|
||||
},
|
||||
assignmentTypes: { areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })) },
|
||||
roles: { canUserViewGradebook: jest.fn(state => ({ canUserViewGradebook: state })) },
|
||||
root: { showBulkManagement: jest.fn(state => ({ showBulkManagement: state })) },
|
||||
},
|
||||
}));
|
||||
|
||||
const courseId = 'fakeID';
|
||||
describe('GradebookHeader component', () => {
|
||||
const props = {
|
||||
activeView: views.grades,
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: false,
|
||||
courseId,
|
||||
showBulkManagement: false,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.setView = jest.fn();
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookHeader {...props} />);
|
||||
el.instance().handleToggleViewClick = jest.fn().mockName('this.handleToggleViewClick');
|
||||
});
|
||||
describe('default values (grades frozen, cannot view).', () => {
|
||||
test('unauthorized warning, but no grades frozen warning', () => {
|
||||
const props = { courseId, areGradesFrozen: false, canUserViewGradebook: false };
|
||||
expect(shallow(<GradebookHeader {...props} />)).toMatchSnapshot();
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('grades frozen, cannot view', () => {
|
||||
test('unauthorized warning, and grades frozen warning.', () => {
|
||||
const props = { courseId, areGradesFrozen: true, canUserViewGradebook: false };
|
||||
expect(shallow(<GradebookHeader {...props} />)).toMatchSnapshot();
|
||||
el.setProps({ areGradesFrozen: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('grades frozen, can view.', () => {
|
||||
test('grades frozen warning but no unauthorized warning', () => {
|
||||
const props = { courseId, areGradesFrozen: true, canUserViewGradebook: true };
|
||||
expect(shallow(<GradebookHeader {...props} />)).toMatchSnapshot();
|
||||
el.setProps({ areGradesFrozen: true, canUserViewGradebook: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('show bulk management, active view is grades view', () => {
|
||||
test('toggle view button to activity log', () => {
|
||||
el.setProps({ showBulkManagement: true });
|
||||
expect(el.find(Button).getElement()).toEqual((
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={el.instance().handleToggleViewClick}
|
||||
>
|
||||
<FormattedMessage {...messages.toActivityLog} />
|
||||
</Button>
|
||||
));
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('show bulk management, active view is bulkManagementHistory view', () => {
|
||||
test('toggle view button to grades', () => {
|
||||
el.setProps({ showBulkManagement: true, activeView: views.bulkManagementHistory });
|
||||
expect(el.find(Button).getElement()).toEqual((
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={el.instance().handleToggleViewClick}
|
||||
>
|
||||
<FormattedMessage {...messages.toGradesView} />
|
||||
</Button>
|
||||
));
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookHeader {...props} />);
|
||||
});
|
||||
describe('handleToggleViewClick', () => {
|
||||
test('calls setView with activity view if activeView is grades', () => {
|
||||
el.instance().handleToggleViewClick();
|
||||
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
});
|
||||
test('calls setView with grades view if activeView is bulkManagementHistory', () => {
|
||||
el.setProps({ activeView: views.bulkManagementHistory });
|
||||
el.instance().handleToggleViewClick();
|
||||
expect(props.setView).toHaveBeenCalledWith(views.grades);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,6 +123,9 @@ describe('GradebookHeader component', () => {
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('activeView from app.activeView', () => {
|
||||
expect(mapped.activeView).toEqual(selectors.app.activeView(testState));
|
||||
});
|
||||
test('courseId from app.courseId', () => {
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
@@ -54,5 +139,14 @@ describe('GradebookHeader component', () => {
|
||||
mapped.canUserViewGradebook,
|
||||
).toEqual(selectors.roles.canUserViewGradebook(testState));
|
||||
});
|
||||
test('showBulkManagement from root showBulkManagement selector', () => {
|
||||
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setView from actions.app.setView', () => {
|
||||
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { StatefulButton, Icon } from '@edx/paragon';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
export const basicButtonProps = () => ({
|
||||
variant: 'outline-primary',
|
||||
icons: {
|
||||
default: <Icon className="fa fa-download mr-2" />,
|
||||
pending: <Icon className="fa fa-spinner fa-spin mr-2" />,
|
||||
},
|
||||
disabledStates: ['pending'],
|
||||
className: 'ml-2',
|
||||
});
|
||||
|
||||
export const buttonStates = StrictDict({
|
||||
pending: 'pending',
|
||||
default: 'default',
|
||||
});
|
||||
|
||||
/**
|
||||
* <BulkManagementControls />
|
||||
* Provides download buttons for Bulk Management and Intervention reports, only if
|
||||
* showBulkManagement is set in redus.
|
||||
*/
|
||||
export class BulkManagementControls extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.buttonProps = this.buttonProps.bind(this);
|
||||
this.handleClickDownloadInterventions = this.handleClickDownloadInterventions.bind(this);
|
||||
this.handleClickExportGrades = this.handleClickExportGrades.bind(this);
|
||||
}
|
||||
|
||||
buttonProps(label) {
|
||||
return {
|
||||
labels: { default: label, pending: label },
|
||||
state: this.props.showSpinner ? 'pending' : 'default',
|
||||
...basicButtonProps(),
|
||||
};
|
||||
}
|
||||
|
||||
handleClickDownloadInterventions() {
|
||||
this.props.downloadInterventionReport();
|
||||
window.location.assign(this.props.interventionExportUrl);
|
||||
}
|
||||
|
||||
// At present, we don't store label and value in google analytics. By setting the label
|
||||
// property of the below events, I want to verify that we can set the label of google anlatyics
|
||||
// The following properties of a google analytics event are:
|
||||
// category (used), name(used), label(not used), value(not used)
|
||||
handleClickExportGrades() {
|
||||
this.props.downloadBulkGradesReport();
|
||||
window.location.assign(this.props.gradeExportUrl);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.showBulkManagement && (
|
||||
<div>
|
||||
<StatefulButton
|
||||
{...this.buttonProps('Bulk Management')}
|
||||
onClick={this.handleClickExportGrades}
|
||||
/>
|
||||
<StatefulButton
|
||||
{...this.buttonProps('Interventions')}
|
||||
onClick={this.handleClickDownloadInterventions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BulkManagementControls.defaultProps = {
|
||||
showBulkManagement: false,
|
||||
showSpinner: false,
|
||||
};
|
||||
|
||||
BulkManagementControls.propTypes = {
|
||||
// redux
|
||||
downloadBulkGradesReport: PropTypes.func.isRequired,
|
||||
downloadInterventionReport: PropTypes.func.isRequired,
|
||||
gradeExportUrl: PropTypes.string.isRequired,
|
||||
interventionExportUrl: PropTypes.string.isRequired,
|
||||
showSpinner: PropTypes.bool,
|
||||
showBulkManagement: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
gradeExportUrl: selectors.root.gradeExportUrl(state),
|
||||
interventionExportUrl: selectors.root.interventionExportUrl(state),
|
||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
||||
showSpinner: selectors.root.shouldShowSpinner(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
|
||||
downloadInterventionReport: actions.grades.downloadReport.intervention,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);
|
||||
@@ -1,47 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
|
||||
<Table
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "date",
|
||||
"label": "Date",
|
||||
},
|
||||
Object {
|
||||
"key": "grader",
|
||||
"label": "Grader",
|
||||
},
|
||||
Object {
|
||||
"key": "reason",
|
||||
"label": "Reason",
|
||||
},
|
||||
Object {
|
||||
"key": "adjustedGrade",
|
||||
"label": "Adjusted grade",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"adjustedGrade": 0,
|
||||
"date": "yesterday",
|
||||
"grader": "me",
|
||||
"reason": "you ate my sandwich",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": 20,
|
||||
"date": "today",
|
||||
"grader": "me",
|
||||
"reason": "you brought me a new sandwich",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": <AdjustedGradeInput />,
|
||||
"date": "todaaaaaay",
|
||||
"reason": <ReasonInput />,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -1,51 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label="Assignment"
|
||||
value="Qwerty"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label="Student"
|
||||
value="Uiop"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label="Original Grade"
|
||||
value={20}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label="Current Grade"
|
||||
value={2}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label="Assignment"
|
||||
value="Qwerty"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label="Student"
|
||||
value="Uiop"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label="Original Grade"
|
||||
value={20}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label="Current Grade"
|
||||
value={2}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,75 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
<Modal
|
||||
body={
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog="Weve been trying to contact you regarding..."
|
||||
dismissible={false}
|
||||
open={true}
|
||||
/>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
Showing most recent actions (max 5). To see more, please contact support.
|
||||
</div>
|
||||
<div>
|
||||
Note: Once you save, your changes will be visible to students.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
Save Grade
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
closeText="Cancel"
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
open={true}
|
||||
title="Edit Grades"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<Modal
|
||||
body={
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog=""
|
||||
dismissible={false}
|
||||
open={false}
|
||||
/>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
Showing most recent actions (max 5). To see more, please contact support.
|
||||
</div>
|
||||
<div>
|
||||
Note: Once you save, your changes will be visible to students.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
Save Grade
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
closeText="Cancel"
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
open={false}
|
||||
title="Edit Grades"
|
||||
/>
|
||||
`;
|
||||
@@ -1,56 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1`] = `
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
Total Grade values are always displayed as a percentage.
|
||||
</Tooltip>
|
||||
`;
|
||||
|
||||
exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
key="left-basic"
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
Total Grade values are always displayed as a percentage.
|
||||
</Tooltip>
|
||||
}
|
||||
placement="left"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<div>
|
||||
Total Grade (%)
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
screenReaderText="Total Grade values are always displayed as a percentage."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
Username
|
||||
</div>
|
||||
<div
|
||||
className="font-weight-normal student-key"
|
||||
>
|
||||
Student Key*
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,37 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SearchControls Component Snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<h4>
|
||||
Step 1: Filter the Grade Report
|
||||
</h4>
|
||||
<div
|
||||
className="d-flex justify-content-between"
|
||||
>
|
||||
<Button
|
||||
className="btn-primary align-self-start"
|
||||
id="edit-filters-btn"
|
||||
onClick={[MockFunction toggleFilterDrawer]}
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-filter"
|
||||
/>
|
||||
Edit Filters
|
||||
</Button>
|
||||
<div>
|
||||
<SearchField
|
||||
inputLabel="Search for a learner"
|
||||
onChange={[MockFunction onChange]}
|
||||
onClear={[MockFunction onClear]}
|
||||
onSubmit={[MockFunction fetchGrades]}
|
||||
value="alice"
|
||||
/>
|
||||
<small
|
||||
className="form-text text-muted search-help-text"
|
||||
>
|
||||
Search by username, email, or student key
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -1,19 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UsersLabel component snapshot - displays label with number of filtered users out of total 1`] = `
|
||||
<Fragment>
|
||||
Showing
|
||||
<span
|
||||
className="font-weight-bold"
|
||||
>
|
||||
23
|
||||
</span>
|
||||
of
|
||||
<span
|
||||
className="font-weight-bold"
|
||||
>
|
||||
140
|
||||
</span>
|
||||
total learners
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,28 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradesTab Component snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<SpinnerIcon />
|
||||
<SearchControls />
|
||||
<FilterBadges
|
||||
handleClose={[MockFunction this.handleFilterBadgeClose]}
|
||||
/>
|
||||
<StatusAlerts />
|
||||
<h4>
|
||||
Step 2: View or Modify Individual Grades
|
||||
</h4>
|
||||
<UsersLabel />
|
||||
<div
|
||||
className="d-flex justify-content-between align-items-center mb-2"
|
||||
>
|
||||
<ScoreViewInput />
|
||||
<BulkManagementControls />
|
||||
</div>
|
||||
<GradebookTable />
|
||||
<PageButtons />
|
||||
<p>
|
||||
* available for learners in the Master's track only
|
||||
</p>
|
||||
<EditModal />
|
||||
</React.Fragment>
|
||||
`;
|
||||
71
src/components/GradesView/BulkManagementControls.jsx
Normal file
71
src/components/GradesView/BulkManagementControls.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { views } from 'data/constants/app';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import NetworkButton from 'components/NetworkButton';
|
||||
import ImportGradesButton from './ImportGradesButton';
|
||||
|
||||
import messages from './BulkManagementControls.messages';
|
||||
|
||||
/**
|
||||
* <BulkManagementControls />
|
||||
* Provides download buttons for Bulk Management and Intervention reports, only if
|
||||
* showBulkManagement is set in redus.
|
||||
*/
|
||||
export class BulkManagementControls extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickExportGrades = this.handleClickExportGrades.bind(this);
|
||||
this.handleViewActivityLog = this.handleViewActivityLog.bind(this);
|
||||
}
|
||||
|
||||
handleClickExportGrades() {
|
||||
this.props.downloadBulkGradesReport();
|
||||
window.location.assign(this.props.gradeExportUrl);
|
||||
}
|
||||
|
||||
handleViewActivityLog() {
|
||||
this.props.setView(views.bulkManagementHistory);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.showBulkManagement && (
|
||||
<div className="d-flex">
|
||||
<NetworkButton
|
||||
label={messages.downloadGradesBtn}
|
||||
onClick={this.handleClickExportGrades}
|
||||
/>
|
||||
<ImportGradesButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BulkManagementControls.defaultProps = {
|
||||
showBulkManagement: false,
|
||||
};
|
||||
|
||||
BulkManagementControls.propTypes = {
|
||||
// redux
|
||||
downloadBulkGradesReport: PropTypes.func.isRequired,
|
||||
gradeExportUrl: PropTypes.string.isRequired,
|
||||
showBulkManagement: PropTypes.bool,
|
||||
setView: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
gradeExportUrl: selectors.root.gradeExportUrl(state),
|
||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
|
||||
setView: actions.app.setView,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);
|
||||
11
src/components/GradesView/BulkManagementControls.messages.js
Normal file
11
src/components/GradesView/BulkManagementControls.messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
downloadGradesBtn: {
|
||||
id: 'gradebook.GradesView.BulkManagementControls.bulkManagementLabel',
|
||||
defaultMessage: 'Download Grades',
|
||||
description: 'Button text for bulk grades download control in GradesView',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -3,15 +3,16 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import { views } from 'data/constants/app';
|
||||
|
||||
import {
|
||||
BulkManagementControls,
|
||||
basicButtonProps,
|
||||
buttonStates,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './BulkManagementControls';
|
||||
|
||||
jest.mock('./ImportGradesButton', () => 'ImportGradesButton');
|
||||
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -19,13 +20,13 @@ jest.mock('data/selectors', () => ({
|
||||
gradeExportUrl: (state) => ({ gradeExportUrl: state }),
|
||||
interventionExportUrl: (state) => ({ interventionExportUrl: state }),
|
||||
showBulkManagement: (state) => ({ showBulkManagement: state }),
|
||||
shouldShowSpinner: (state) => ({ showSpinner: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setView: jest.fn() },
|
||||
grades: {
|
||||
downloadReport: {
|
||||
bulkGrades: jest.fn(),
|
||||
@@ -47,22 +48,12 @@ describe('BulkManagementControls', () => {
|
||||
...props,
|
||||
downloadBulkGradesReport: jest.fn(),
|
||||
downloadInterventionReport: jest.fn(),
|
||||
setView: jest.fn(),
|
||||
};
|
||||
});
|
||||
test('snapshot - empty if showBulkManagement is not truthy', () => {
|
||||
expect(shallow(<BulkManagementControls {...props} />)).toEqual({});
|
||||
});
|
||||
test('snapshot - buttonProps for each button ("Bulk Management" and "Interventions")', () => {
|
||||
el = shallow(<BulkManagementControls {...props} showBulkManagement />);
|
||||
jest.spyOn(el.instance(), 'buttonProps').mockImplementation(
|
||||
value => ({ buttonProps: value }),
|
||||
);
|
||||
jest.spyOn(el.instance(), 'handleClickExportGrades').mockName('this.handleClickExportGrades');
|
||||
jest.spyOn(
|
||||
el.instance(),
|
||||
'handleClickDownloadInterventions',
|
||||
).mockName('this.handleClickDownloadInterventions');
|
||||
});
|
||||
describe('behavior', () => {
|
||||
const oldWindowLocation = window.location;
|
||||
|
||||
@@ -87,37 +78,10 @@ describe('BulkManagementControls', () => {
|
||||
// restore `window.location` to the `jsdom` `Location` object
|
||||
window.location = oldWindowLocation;
|
||||
});
|
||||
|
||||
describe('buttonProps', () => {
|
||||
test('loads default and pending labels based on passed string', () => {
|
||||
const label = 'Fake Label';
|
||||
const { labels, state, ...rest } = el.instance().buttonProps(label);
|
||||
expect(rest).toEqual(basicButtonProps());
|
||||
expect(labels).toEqual({ default: label, pending: label });
|
||||
});
|
||||
test('loads pending state if props.showSpinner', () => {
|
||||
const label = 'Fake Label';
|
||||
el.setProps({ showSpinner: true });
|
||||
const { labels, state, ...rest } = el.instance().buttonProps(label);
|
||||
expect(state).toEqual(buttonStates.pending);
|
||||
expect(rest).toEqual(basicButtonProps());
|
||||
});
|
||||
test('loads default state if not props.showSpinner', () => {
|
||||
const label = 'Fake Label';
|
||||
const { labels, state, ...rest } = el.instance().buttonProps(label);
|
||||
expect(state).toEqual(buttonStates.default);
|
||||
expect(rest).toEqual(basicButtonProps());
|
||||
});
|
||||
});
|
||||
describe('handleClickDownloadInterventions', () => {
|
||||
const assertions = [
|
||||
'calls props.downloadInterventionReport',
|
||||
'sets location to props.interventionsExportUrl',
|
||||
];
|
||||
it(assertions.join(' and '), () => {
|
||||
el.instance().handleClickDownloadInterventions();
|
||||
expect(props.downloadInterventionReport).toHaveBeenCalled();
|
||||
expect(window.location.assign).toHaveBeenCalledWith(props.interventionExportUrl);
|
||||
describe('handleViewActivityLog', () => {
|
||||
it('calls props.setView(views.bulkManagementHistory)', () => {
|
||||
el.instance().handleViewActivityLog();
|
||||
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
});
|
||||
});
|
||||
describe('handleClickExportGrades', () => {
|
||||
@@ -143,15 +107,9 @@ describe('BulkManagementControls', () => {
|
||||
test('gradeExportUrl from root.gradeExportUrl', () => {
|
||||
expect(mapped.gradeExportUrl).toEqual(selectors.root.gradeExportUrl(testState));
|
||||
});
|
||||
test('interventionExportUrl from root.interventionExportUrl', () => {
|
||||
expect(mapped.interventionExportUrl).toEqual(selectors.root.interventionExportUrl(testState));
|
||||
});
|
||||
test('showBulkManagement from root.showBulkManagement', () => {
|
||||
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
|
||||
});
|
||||
test('showSpinner from root.shouldShowSpinner', () => {
|
||||
expect(mapped.showSpinner).toEqual(selectors.root.shouldShowSpinner(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('downloadBulkGradesReport from actions.grades.downloadReport.bulkGrades', () => {
|
||||
@@ -159,10 +117,8 @@ describe('BulkManagementControls', () => {
|
||||
mapDispatchToProps.downloadBulkGradesReport,
|
||||
).toEqual(actions.grades.downloadReport.bulkGrades);
|
||||
});
|
||||
test('downloadInterventionReport from actions.grades.downloadReport.invervention', () => {
|
||||
expect(
|
||||
mapDispatchToProps.downloadInterventionReport,
|
||||
).toEqual(actions.grades.downloadReport.intervention);
|
||||
test('setView from actions.app.setView', () => {
|
||||
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,7 @@ HistoryHeader.defaultProps = {
|
||||
};
|
||||
HistoryHeader.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,11 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import messages from './messages';
|
||||
import HistoryHeader from './HistoryHeader';
|
||||
|
||||
/**
|
||||
@@ -18,22 +22,22 @@ export const ModalHeaders = ({
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label="Assignment"
|
||||
label={<FormattedMessage {...messages.assignmentHeader} />}
|
||||
value={modalState.assignmentName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label="Student"
|
||||
label={<FormattedMessage {...messages.studentHeader} />}
|
||||
value={modalState.updateUserName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label="Original Grade"
|
||||
label={<FormattedMessage {...messages.originalGradeHeader} />}
|
||||
value={originalGrade}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label="Current Grade"
|
||||
label={<FormattedMessage {...messages.currentGradeHeader} />}
|
||||
value={currentGrade}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
|
||||
<Table
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "date",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Date"
|
||||
description="Edit Modal Override Table Date column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.dateHeader"
|
||||
/>,
|
||||
},
|
||||
Object {
|
||||
"key": "grader",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Grader"
|
||||
description="Edit Modal Override Table Grader column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.graderHeader"
|
||||
/>,
|
||||
},
|
||||
Object {
|
||||
"key": "reason",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Reason"
|
||||
description="Edit Modal Override Table Reason column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.reasonHeader"
|
||||
/>,
|
||||
},
|
||||
Object {
|
||||
"key": "adjustedGrade",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Adjusted grade"
|
||||
description="Edit Modal Override Table Adjusted grade column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader"
|
||||
/>,
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"adjustedGrade": 0,
|
||||
"date": "yesterday",
|
||||
"grader": "me",
|
||||
"reason": "you ate my sandwich",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": 20,
|
||||
"date": "today",
|
||||
"grader": "me",
|
||||
"reason": "you brought me a new sandwich",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": <AdjustedGradeInput />,
|
||||
"date": "todaaaaaay",
|
||||
"reason": <ReasonInput />,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -4,18 +4,15 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import messages from './messages';
|
||||
import ReasonInput from './ReasonInput';
|
||||
import AdjustedGradeInput from './AdjustedGradeInput';
|
||||
|
||||
const GRADE_OVERRIDE_HISTORY_COLUMNS = [
|
||||
{ label: 'Date', key: 'date' },
|
||||
{ label: 'Grader', key: 'grader' },
|
||||
{ label: 'Reason', key: 'reason' },
|
||||
{ label: 'Adjusted grade', key: 'adjustedGrade' },
|
||||
];
|
||||
|
||||
/**
|
||||
* <OverrideTable />
|
||||
* Table containing previous grade override entries, and an "edit" row
|
||||
@@ -31,7 +28,15 @@ export const OverrideTable = ({
|
||||
}
|
||||
return (
|
||||
<Table
|
||||
columns={GRADE_OVERRIDE_HISTORY_COLUMNS}
|
||||
columns={[
|
||||
{ label: <FormattedMessage {...messages.dateHeader} />, key: columns.date },
|
||||
{ label: <FormattedMessage {...messages.graderHeader} />, key: columns.grader },
|
||||
{ label: <FormattedMessage {...messages.reasonHeader} />, key: columns.reason },
|
||||
{
|
||||
label: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
key: columns.adjustedGrade,
|
||||
},
|
||||
]}
|
||||
data={[
|
||||
...gradeOverrides,
|
||||
{
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
adjustedGradeHeader: {
|
||||
id: 'gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader',
|
||||
defaultMessage: 'Adjusted grade',
|
||||
description: 'Edit Modal Override Table Adjusted grade column header',
|
||||
},
|
||||
dateHeader: {
|
||||
id: 'gradebook.GradesView.EditModal.Overrides.dateHeader',
|
||||
defaultMessage: 'Date',
|
||||
description: 'Edit Modal Override Table Date column header',
|
||||
},
|
||||
graderHeader: {
|
||||
id: 'gradebook.GradesView.EditModal.Overrides.graderHeader',
|
||||
defaultMessage: 'Grader',
|
||||
description: 'Edit Modal Override Table Grader column header',
|
||||
},
|
||||
reasonHeader: {
|
||||
id: 'gradebook.GradesView.EditModal.Overrides.reasonHeader',
|
||||
defaultMessage: 'Reason',
|
||||
description: 'Edit Modal Override Table Reason column header',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,99 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Edit Modal Assignment header"
|
||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
||||
/>
|
||||
}
|
||||
value="Qwerty"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Student"
|
||||
description="Edit Modal Student header"
|
||||
id="gradebook.GradesView.EditModal.headers.student"
|
||||
/>
|
||||
}
|
||||
value="Uiop"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Original Grade"
|
||||
description="Edit Modal Original Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
||||
/>
|
||||
}
|
||||
value={20}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Current Grade"
|
||||
description="Edit Modal Current Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
||||
/>
|
||||
}
|
||||
value={2}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Edit Modal Assignment header"
|
||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
||||
/>
|
||||
}
|
||||
value="Qwerty"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Student"
|
||||
description="Edit Modal Student header"
|
||||
id="gradebook.GradesView.EditModal.headers.student"
|
||||
/>
|
||||
}
|
||||
value="Uiop"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Original Grade"
|
||||
description="Edit Modal Original Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
||||
/>
|
||||
}
|
||||
value={20}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Current Grade"
|
||||
description="Edit Modal Current Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
||||
/>
|
||||
}
|
||||
value={2}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
123
src/components/GradesView/EditModal/__snapshots__/test.jsx.snap
Normal file
123
src/components/GradesView/EditModal/__snapshots__/test.jsx.snap
Normal file
@@ -0,0 +1,123 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
<Modal
|
||||
body={
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog="Weve been trying to contact you regarding..."
|
||||
dismissible={false}
|
||||
open={true}
|
||||
/>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
||||
description="Edit Modal visibility hint message"
|
||||
id="gradebook.GradesView.EditModal.contactSupport"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Note: Once you save, your changes will be visible to students."
|
||||
description="Edit Modal saved changes effect hint"
|
||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Grades"
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
closeText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
open={true}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit Grades"
|
||||
description="Edit Modal title"
|
||||
id="gradebook.GradesView.EditModal.title"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<Modal
|
||||
body={
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog=""
|
||||
dismissible={false}
|
||||
open={false}
|
||||
/>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
||||
description="Edit Modal visibility hint message"
|
||||
id="gradebook.GradesView.EditModal.contactSupport"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Note: Once you save, your changes will be visible to students."
|
||||
description="Edit Modal saved changes effect hint"
|
||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
buttons={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Grades"
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
closeText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
open={false}
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit Grades"
|
||||
description="Edit Modal title"
|
||||
id="gradebook.GradesView.EditModal.title"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
Modal,
|
||||
StatusAlert,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from './messages';
|
||||
import OverrideTable from './OverrideTable';
|
||||
import ModalHeaders from './ModalHeaders';
|
||||
|
||||
@@ -46,8 +48,8 @@ export class EditModal extends React.Component {
|
||||
return (
|
||||
<Modal
|
||||
open={this.props.open}
|
||||
title="Edit Grades"
|
||||
closeText="Cancel"
|
||||
title={<FormattedMessage {...messages.title} />}
|
||||
closeText={<FormattedMessage {...messages.closeText} />}
|
||||
body={(
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
@@ -58,15 +60,13 @@ export class EditModal extends React.Component {
|
||||
dismissible={false}
|
||||
/>
|
||||
<OverrideTable />
|
||||
<div>Showing most recent actions (max 5). To see more, please contact
|
||||
support.
|
||||
</div>
|
||||
<div>Note: Once you save, your changes will be visible to students.</div>
|
||||
<div><FormattedMessage {...messages.visibility} /></div>
|
||||
<div><FormattedMessage {...messages.saveVisibility} /></div>
|
||||
</div>
|
||||
)}
|
||||
buttons={[
|
||||
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
|
||||
Save Grade
|
||||
<FormattedMessage {...messages.saveGrade} />
|
||||
</Button>,
|
||||
]}
|
||||
onClose={this.closeAssignmentModal}
|
||||
51
src/components/GradesView/EditModal/messages.js
Normal file
51
src/components/GradesView/EditModal/messages.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
assignmentHeader: {
|
||||
id: 'gradebook.GradesView.EditModal.headers.assignment',
|
||||
defaultMessage: 'Assignment',
|
||||
description: 'Edit Modal Assignment header',
|
||||
},
|
||||
currentGradeHeader: {
|
||||
id: 'gradebook.GradesView.EditModal.headers.currentGrade',
|
||||
defaultMessage: 'Current Grade',
|
||||
description: 'Edit Modal Current Grade header',
|
||||
},
|
||||
originalGradeHeader: {
|
||||
id: 'gradebook.GradesView.EditModal.headers.originalGrade',
|
||||
defaultMessage: 'Original Grade',
|
||||
description: 'Edit Modal Original Grade header',
|
||||
},
|
||||
studentHeader: {
|
||||
id: 'gradebook.GradesView.EditModal.headers.student',
|
||||
defaultMessage: 'Student',
|
||||
description: 'Edit Modal Student header',
|
||||
},
|
||||
title: {
|
||||
id: 'gradebook.GradesView.EditModal.title',
|
||||
defaultMessage: 'Edit Grades',
|
||||
description: 'Edit Modal title',
|
||||
},
|
||||
closeText: {
|
||||
id: 'gradebook.GradesView.EditModal.closeText',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Edit Modal close button text',
|
||||
},
|
||||
visibility: {
|
||||
id: 'gradebook.GradesView.EditModal.contactSupport',
|
||||
defaultMessage: 'Showing most recent actions (max 5). To see more, please contact support',
|
||||
description: 'Edit Modal visibility hint message',
|
||||
},
|
||||
saveVisibility: {
|
||||
id: 'gradebook.GradesView.EditModal.saveVisibility',
|
||||
defaultMessage: 'Note: Once you save, your changes will be visible to students.',
|
||||
description: 'Edit Modal saved changes effect hint',
|
||||
},
|
||||
saveGrade: {
|
||||
id: 'gradebook.GradesView.EditModal.saveGrade',
|
||||
defaultMessage: 'Save Grades',
|
||||
description: 'Edit Modal Save button label',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
@@ -15,7 +16,6 @@ import selectors from 'data/selectors';
|
||||
* @param {string} filterName - api filter name (for redux connector)
|
||||
*/
|
||||
export const FilterBadge = ({
|
||||
handleClose,
|
||||
config: {
|
||||
displayName,
|
||||
isDefault,
|
||||
@@ -23,11 +23,15 @@ export const FilterBadge = ({
|
||||
value,
|
||||
connectedFilters,
|
||||
},
|
||||
handleClose,
|
||||
}) => !isDefault && (
|
||||
<div>
|
||||
<span className="badge badge-info">
|
||||
<span>
|
||||
{displayName}{!hideValue && `: ${value}`}
|
||||
<FormattedMessage {...displayName} />
|
||||
</span>
|
||||
<span>
|
||||
{!hideValue ? `: ${value}` : ''}
|
||||
</span>
|
||||
<Button
|
||||
className="btn-info"
|
||||
@@ -48,7 +52,9 @@ FilterBadge.propTypes = {
|
||||
// redux
|
||||
config: PropTypes.shape({
|
||||
connectedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||
displayName: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.shape({
|
||||
defaultMessage: PropTypes.string,
|
||||
}).isRequired,
|
||||
isDefault: PropTypes.bool.isRequired,
|
||||
hideValue: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import selectors from 'data/selectors';
|
||||
@@ -20,7 +21,9 @@ jest.mock('data/selectors', () => ({
|
||||
describe('FilterBadge', () => {
|
||||
describe('component', () => {
|
||||
const config = {
|
||||
displayName: 'a common name',
|
||||
displayName: {
|
||||
defaultMessage: 'a common name',
|
||||
},
|
||||
isDefault: false,
|
||||
hideValue: false,
|
||||
value: 'a common value',
|
||||
@@ -58,7 +61,11 @@ describe('FilterBadge', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('shows displayName but not value in span', () => {
|
||||
expect(el.find('span.badge').childAt(0).text()).toEqual(config.displayName);
|
||||
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
|
||||
<span>
|
||||
<FormattedMessage {...config.displayName} />
|
||||
</span>,
|
||||
);
|
||||
});
|
||||
it('calls a handleClose event for connected filters on button click', () => {
|
||||
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
|
||||
@@ -72,8 +79,15 @@ describe('FilterBadge', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('shows displayName and value in span', () => {
|
||||
expect(el.find('span.badge').childAt(0).text()).toEqual(
|
||||
`${config.displayName}: ${config.value}`,
|
||||
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
|
||||
<span>
|
||||
<FormattedMessage {...config.displayName} />
|
||||
</span>,
|
||||
);
|
||||
expect(el.find('span.badge').childAt(1).getElement()).toEqual(
|
||||
<span>
|
||||
{`: ${config.value}`}
|
||||
</span>,
|
||||
);
|
||||
});
|
||||
it('calls a handleClose event for connected filters on button click', () => {
|
||||
@@ -8,7 +8,11 @@ exports[`FilterBadge component with non-default value (active) if hideValue is f
|
||||
className="badge badge-info"
|
||||
>
|
||||
<span>
|
||||
a common name
|
||||
<FormattedMessage
|
||||
defaultMessage="a common name"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
: a common value
|
||||
</span>
|
||||
<Button
|
||||
@@ -40,8 +44,11 @@ exports[`FilterBadge component with non-default value (active) if hideValue is t
|
||||
className="badge badge-info"
|
||||
>
|
||||
<span>
|
||||
a common name
|
||||
<FormattedMessage
|
||||
defaultMessage="a common name"
|
||||
/>
|
||||
</span>
|
||||
<span />
|
||||
<Button
|
||||
aria-label="close"
|
||||
className="btn-info"
|
||||
37
src/components/GradesView/FilterMenuToggle.jsx
Normal file
37
src/components/GradesView/FilterMenuToggle.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from './FilterMenuToggle.messages';
|
||||
|
||||
/**
|
||||
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
|
||||
* as well as the search box for searching by username/email.
|
||||
*/
|
||||
export const FilterMenuToggle = ({ toggleFilterDrawer }) => (
|
||||
<Button
|
||||
id="edit-filters-btn"
|
||||
className="btn-primary align-self-start"
|
||||
onClick={toggleFilterDrawer}
|
||||
>
|
||||
<Icon className="fa fa-filter" /> <FormattedMessage {...messages.editFilters} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
FilterMenuToggle.propTypes = {
|
||||
// From Redux
|
||||
toggleFilterDrawer: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = () => ({});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
toggleFilterDrawer: thunkActions.app.filterMenu.toggle,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FilterMenuToggle);
|
||||
11
src/components/GradesView/FilterMenuToggle.messages.js
Normal file
11
src/components/GradesView/FilterMenuToggle.messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
editFilters: {
|
||||
id: 'gradebook.GradesView.editFilterLabel',
|
||||
defaultMessage: 'Edit Filters',
|
||||
description: 'Button text on Grades tab to open/close the Filters tab',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
42
src/components/GradesView/FilterMenuToggle.test.jsx
Normal file
42
src/components/GradesView/FilterMenuToggle.test.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import { FilterMenuToggle, mapDispatchToProps, mapStateToProps } from './FilterMenuToggle';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
filterMenu: { toggle: jest.fn() },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FilterMenuToggle component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const toggleFilterDrawer = jest.fn().mockName('this.props.toggleFilterDrawer');
|
||||
expect(shallow((
|
||||
<FilterMenuToggle {...{ toggleFilterDrawer }} />
|
||||
))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
test('does not connect any selectors', () => {
|
||||
expect(mapStateToProps({ test: 'state' })).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('toggleFilterDrawer from thunkActions.app.filterMenu.toggle', () => {
|
||||
expect(mapDispatchToProps.toggleFilterDrawer).toEqual(
|
||||
thunkActions.app.filterMenu.toggle,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
44
src/components/GradesView/FilteredUsersLabel.jsx
Normal file
44
src/components/GradesView/FilteredUsersLabel.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
/**
|
||||
* <FilteredUsersLabel />
|
||||
* Simple label component displaying the filtered and total users shown
|
||||
*/
|
||||
export const FilteredUsersLabel = ({
|
||||
filteredUsersCount,
|
||||
totalUsersCount,
|
||||
}) => {
|
||||
if (!totalUsersCount) {
|
||||
return null;
|
||||
}
|
||||
const bold = (val) => (<span className="font-weight-bold">{val}</span>);
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="gradebook.GradesTab.usersVisibilityLabel'"
|
||||
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
|
||||
description="Users visibility label"
|
||||
values={{
|
||||
filteredUsers: bold(filteredUsersCount),
|
||||
totalUsers: bold(totalUsersCount),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
FilteredUsersLabel.propTypes = {
|
||||
filteredUsersCount: PropTypes.number.isRequired,
|
||||
totalUsersCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
totalUsersCount: selectors.grades.totalUsersCount(state),
|
||||
filteredUsersCount: selectors.grades.filteredUsersCount(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(FilteredUsersLabel);
|
||||
46
src/components/GradesView/FilteredUsersLabel.test.jsx
Normal file
46
src/components/GradesView/FilteredUsersLabel.test.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { FilteredUsersLabel, mapStateToProps } from './FilteredUsersLabel';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
filteredUsersCount: state => ({ filteredUsersCount: state }),
|
||||
totalUsersCount: state => ({ totalUsersCount: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FilteredUsersLabel', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
filteredUsersCount: 23,
|
||||
totalUsersCount: 140,
|
||||
};
|
||||
it('does not render if totalUsersCount is falsey', () => {
|
||||
expect(shallow(<FilteredUsersLabel {...props} totalUsersCount={0} />)).toEqual({});
|
||||
});
|
||||
test('snapshot - displays label with number of filtered users out of total', () => {
|
||||
expect(shallow(<FilteredUsersLabel {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { a: 'nice', day: 'for', some: 'rain' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('filteredUsersCount from grades.filteredUsersCount', () => {
|
||||
expect(mapped.filteredUsersCount).toEqual(selectors.grades.filteredUsersCount(testState));
|
||||
});
|
||||
test('totalUsersCount from grades.totalUsersCount', () => {
|
||||
expect(mapped.totalUsersCount).toEqual(selectors.grades.totalUsersCount(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user