Compare commits
41 Commits
v1.4.31
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff4d0c75dd | ||
|
|
c4846f9ebd | ||
|
|
bccd87fd49 | ||
|
|
03fa143fc1 | ||
|
|
075846f869 | ||
|
|
1208d27d92 | ||
|
|
e345716bd4 | ||
|
|
2121a63c83 | ||
|
|
47cab71b3c | ||
|
|
2d8af2ec00 | ||
|
|
d55abbe91e | ||
|
|
a75f365bdd | ||
|
|
bbb7e895a5 | ||
|
|
bf70fd1450 | ||
|
|
af2ece8290 | ||
|
|
620827d772 | ||
|
|
c6a4685bf5 | ||
|
|
8dd2237f9c | ||
|
|
97c58157f8 | ||
|
|
ce093efba4 | ||
|
|
799ef5b8a1 | ||
|
|
f956351cf7 | ||
|
|
7772e21c6a | ||
|
|
f07a96ce58 | ||
|
|
f64bc8d4a6 | ||
|
|
134dabb710 | ||
|
|
65c25f00b6 | ||
|
|
31748e246e | ||
|
|
650be29ef9 | ||
|
|
b713ab5748 | ||
|
|
5fe80b4a52 | ||
|
|
9e04813d06 | ||
|
|
a0e1a60d23 | ||
|
|
68c7944dd5 | ||
|
|
f4f6e5551f | ||
|
|
ee99bfdaa4 | ||
|
|
318ce349fc | ||
|
|
8f1c89a025 | ||
|
|
a1de3a8612 | ||
|
|
4e26247ac3 | ||
|
|
f21e6da598 |
2
.env
2
.env
@@ -30,3 +30,5 @@ ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -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,3 +37,5 @@ 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'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -3,4 +3,4 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence, they will
|
||||
# be requested for review when someone opens a pull request.
|
||||
* @edx/masters-devs-gta
|
||||
* @openedx/content-aurora
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -26,4 +26,4 @@ Collectively, these should be completed by reviewers of this PR:
|
||||
- [ ] I've tested the new functionality
|
||||
|
||||
|
||||
FYI: @edx/masters-devs-gta
|
||||
FYI: @openedx/content-aurora
|
||||
|
||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Run the workflow that adds new tickets that are either:
|
||||
# - labelled "DEPR"
|
||||
# - title starts with "[DEPR]"
|
||||
# - body starts with "Proposal Date" (this is the first template field)
|
||||
# to the org-wide DEPR project board
|
||||
|
||||
name: Add newly created DEPR issues to the DEPR project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
routeissue:
|
||||
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
64
.github/workflows/ci.yml
vendored
Normal file
64
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: node_js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
npm: [8.5.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install npm 8.5.x
|
||||
run: npm install -g npm@${{ matrix.npm }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Unit Tests
|
||||
run: npm run test
|
||||
|
||||
- name: Validate Package Lock
|
||||
run: make validate-no-uncommitted-package-lock-changes
|
||||
|
||||
- name: Run Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run Test
|
||||
run: npm run test
|
||||
|
||||
- name: Run Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
with:
|
||||
server_address: email-smtp.us-east-1.amazonaws.com
|
||||
server_port: 465
|
||||
username: ${{secrets.EDX_SMTP_USERNAME}}
|
||||
password: ${{secrets.EDX_SMTP_PASSWORD}}
|
||||
subject: CI workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org
|
||||
from: github-actions <github-actions@edx.org>
|
||||
body: CI workflow in ${{github.repository}} failed! For details see "github.com/${{
|
||||
github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
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
|
||||
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#check package-lock file version
|
||||
|
||||
name: Lockfile Version check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
33
.github/workflows/npm-publish.yml
vendored
Normal file
33
.github/workflows/npm-publish.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Release CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Create Build
|
||||
run: npm run build
|
||||
|
||||
- name: Release Package
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
|
||||
run: npm semantic-release
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,8 @@ npm-debug.log
|
||||
coverage
|
||||
|
||||
dist/
|
||||
src/i18n/transifex_input.json
|
||||
temp/babel-plugin-react-intl
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.travis.yml
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
|
||||
28
.travis.yml
28
.travis.yml
@@ -1,28 +0,0 @@
|
||||
language: node_js
|
||||
node_js: 12
|
||||
notifications:
|
||||
email:
|
||||
recipients:
|
||||
- masters-grades@edx.org
|
||||
on_success: never
|
||||
on_failure: always
|
||||
webhooks: https://www.travisbuddy.com/
|
||||
on_success: never
|
||||
before_install:
|
||||
- npm install -g greenkeeper-lockfile@1.14.0
|
||||
install:
|
||||
- npm ci
|
||||
before_script: greenkeeper-lockfile-update
|
||||
after_script: greenkeeper-lockfile-upload
|
||||
script:
|
||||
- make validate-no-uncommitted-package-lock-changes
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run build
|
||||
after_success:
|
||||
- npm run travis-deploy-once "npm run semantic-release"
|
||||
- npm run coveralls
|
||||
env:
|
||||
global:
|
||||
- secure: bBLQZVw1aVUxB7GFNXGrdKeztyFrCCJusVgFcSuej9S4qmj9/jrVsEc9dEcH+BMS+b49+SvILoxzd6ZYLaRygQLzevnO1/dX596DeCKVK48PTTZRsNyafaSMCkxNKqEmRcA9hYL52xJJ5GpKo7ViWsFy8VFgUfZEJxQi8/lYbfQ1vlXRpo2LJfJh09v85roSXdQmajyGJ1Dz6elcwUX5B+BgXmIHizJXUMfFci61xTEZmgKtfeCiwFQA5pCvVMHBQhgySqT2N3eRESzRt2jAfAdcRKBYXS0rwKymdlL1ZF349Jm8xwtqm19Fwsut21181Lnn6FmccMWhQ7man3WH1xfT0ahmHNs1KJMyZcwRJd/gDfbd6iD3LB9Pt9hEQ00Qh/m7MYeahMxTEL9bp2TyILi8cTP91jeBUHCExCdv2jRrUQEnUS5vZUYRdM8CR2DLoLmNh3APndKzwgr5U8rh6RdhbQBJp97Hb/YYVrBiP2atLJAaYPY/xEQHK/YoXelQgiZ6wHBMV+tF/L0ZRn7KyVWdkbBKWfbEjRKbEJD9WD+V7HayMR81tm5CSqlrG8mTvSy2boIGiX14GV11ZEfMj5bjb6W41BW+QGqQerZvmwk/4ywe304X85PD0OBhIYPRzeLIi0Gt6lD1aOpVxgm4M03tdgYQzCPWRPq32CB+1IA=
|
||||
- secure: w1d/E+cc4+Bf017Jpp9YsKBzLSZw9sqKZGeM2tNrO6eJZbMJqfKTmfUrRw8BoLh1Z8YRkHF7RADDy3ln7XEdeAX3j9OoC3Cz0zN6iDX6TPcI461NuOIscJYb4tyFcuWm6FhgVlBAlo/BI3q+zqKwjfWuDaORpk6+haacCmvTe5V0vWhY+MYT7M+LfnKeKVzhI4magGt8jPTE21oziIFwCqCCjJc4+AmsWoWTzU0Q7Db0DZiJnLXFfXybLbkedAgJmcSgEGZCSpaZIOkX0/Lbazsz1Ky4KASfkrYT1Z5iKQ8TE3skmx1IIu+1egN8iBbdrY+NhvV24RkT+rpUvD7TBIHTrjQ5JYLe0kGjN70vG7YlKgjNSyTjkrEd7fCKpuIol3DVjBRz3tV5aCl0t/A8mIPqKyNI94MamWsExpqsxgcb9vBVno5caZvD8ZXNrGNqanB3MSoLGxZTLKif9u+AZfLnB3xtjaiJg3/BNoWaOBPlp/M6BvGIGHElwvLrAhUvl8wzrwJcQQWpmRMh0b6enr6Y7ox/mGGs7NBCT+CNKEsWeCfY4thZzgi6/GocXyqdTpXMkNSI1PDoPmi+vKafBd+7aAYbcUlJBTU6TAxyncln0tF2JF+ghTZ0v8nNzEQ9VmV4ddyoOHx6YnHvEcenWZGMROQnMCVifyDbaHpPbPI=
|
||||
@@ -1,8 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[edx-platform.frontend-app-gradebook]
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-gradebook]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
type = KEYVALUEJSON
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -57,9 +57,9 @@ push_translations:
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://travis-ci.com/edx/frontend-app-gradebook) [](https://coveralls.io/github/edx/frontend-app-gradebook)
|
||||
[](https://travis-ci.com/edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
@@ -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
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Travis Configuration
|
||||
|
||||
Your project might have different build requirements - however, this project's `.travis.yml` configuration is supposed to represent a good starting point.
|
||||
|
||||
## Node JS Version
|
||||
|
||||
The minimum `Node` and `npm` versions that edX supports is `8.9.3` and `5.5.1`, respectively.
|
||||
|
||||
## Caching node_modules
|
||||
|
||||
While [the `Travis` blog](https://blog.travis-ci.com/2016-11-21-travis-ci-now-supports-yarn) recommends
|
||||
|
||||
```yaml
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
```
|
||||
|
||||
this causes issues when testing different versions of `Node` because [`node_modules` will store the compiled native modules](https://stackoverflow.com/a/42523517/5225575).
|
||||
|
||||
Caching the `~/.npm` directory avoids storing these native modules.
|
||||
|
||||
## Notifications
|
||||
|
||||
This project uses a service called [`TravisBuddy`](https://www.travisbuddy.com/), which provides Travis build context within a PR via webhooks (configured only to add feedback for build failures).
|
||||
|
||||

|
||||
|
||||
## Installing `greenkeeper-lockfile`
|
||||
|
||||
As explained in [the `Greenkeeper` documentation](https://greenkeeper.io/docs.html#greenkeeper-step-by-step), `Greenkeeper` is a service that keeps track of your project's dependencies, and will, for example, automatically open PRs with an updated `package.json` file when the latest version of a dependency is a major version ahead of the existing dependency version in your `package.json` file.
|
||||
|
||||
This automated updating is great, but `Greenkeeper` does not update your `package-lock.json` file, just your `package.json` file. This makes sense, as the only way to update the `package-lock.json` file would be to run `npm install` when building your project, using the latest `package.json`, and then committing the updated `package-lock.json` file.
|
||||
|
||||
This is essentially what you have to do manually when `Greenkeeper` opens a PR - `git checkout` the branch, `npm install` locally, `git commit` the `package-lock.json` changes, and then `git push` those changes to the `Greenkeeper` branch on `origin`. It's fun probably only the first time, and even then it gets old, fast.
|
||||
|
||||
What [`greenkeeper-lockfile`](https://github.com/greenkeeperio/greenkeeper-lockfile) does is that it automates the previous steps as part of the build process.
|
||||
|
||||
It will
|
||||
* Check that the branch is a `Greenkeeper` branch
|
||||
* Update the lockfile
|
||||
* Push a commit with the updated lockfile back to the Greenkeeper branch
|
||||
|
||||
This is why it's important to install `greenkeeper-lockfile` in the `before_install` step, and since it's used exclusively only in the Travis Build, why it's not part of the package's dependencies.
|
||||
|
||||
## Scripts
|
||||
|
||||
Most of the `script`s are self-explanatory - you probably want to fail a build if there are linting violations, or if any tests don't pass, or if it cannot compile your files.
|
||||
|
||||
However, there are a couple additional `script`s that might seem less self-explanatory.
|
||||
|
||||
### What the heck is `make validate-no-uncommitted-package-lock-changes`?
|
||||
|
||||
There are only two requirements for a good `make target` name
|
||||
|
||||
1. Definitely make it really verbose so people can't remember what it's called
|
||||
2. Definitely don't not use a double-negative
|
||||
|
||||
What `make validate-no-uncommitted-package-lock-changes` does is `git diff`s for any `package-lock.json` file changes in your project. It's important to remember that all build `script`s are executed in Travis *after* the `install` step (aka post-`npm install`).
|
||||
|
||||
This is important because `npm` uses the pinned dependencies in your `package-lock.json` file to build the `node_modules` directory. However, the dependencies defined within the `package.json` file can be modified manually, for example, to become misaligned with the dependencies defined within the `package-lock.json`. So when `npm install` executes, the `package-lock.json` file will be updated to mirror the modified `package.json` changes.
|
||||
|
||||
However, when these changes surface within a Travis build, this indicates differing dependency expectations between the committed `package.json` file and the `package-lock.json` file, which is a good reason to fail a build.
|
||||
|
||||
### What is this `npm run is-es5` check?
|
||||
|
||||
This project outputs production files to the `dist` folder. The `npm script`, `npm run is-es5`, checks the JavaScript files in the `dist` folder to make sure that they are `ES5`-compliant.
|
||||
|
||||
This check is important because `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/) - particularly for `IE11`.
|
||||
|
||||
### `deploy` step
|
||||
|
||||
How your project deploys will probably differ between the cookie cutter and your own application.
|
||||
|
||||
For demonstrational purposes, the cookie cutter deploys to GitHub pages using [`Travis`'s GitHub pages configuration](https://docs.travis-ci.com/user/deployment/pages/).
|
||||
|
||||
Your application might deploy to an `S3` bucket or to `npm`.
|
||||
40
documentation/CI.md
Executable file
40
documentation/CI.md
Executable file
@@ -0,0 +1,40 @@
|
||||
# CI Configuration
|
||||
|
||||
Your project might have different build requirements - however, this project's `.github/ci.yml` configuration is supposed to represent a good starting point.
|
||||
|
||||
## Node JS Version
|
||||
|
||||
The minimum `Node` and `npm` versions that edX supports is `8.9.3` and `5.5.1`, respectively.
|
||||
|
||||
## Scripts
|
||||
|
||||
Most of the `script`s are self-explanatory - you probably want to fail a build if there are linting violations, or if any tests don't pass, or if it cannot compile your files.
|
||||
|
||||
However, there are a couple additional `script`s that might seem less self-explanatory.
|
||||
|
||||
### What the heck is `make validate-no-uncommitted-package-lock-changes`?
|
||||
|
||||
There are only two requirements for a good `make target` name
|
||||
|
||||
1. Definitely make it really verbose so people can't remember what it's called
|
||||
2. Definitely don't not use a double-negative
|
||||
|
||||
What `make validate-no-uncommitted-package-lock-changes` does is `git diff`s for any `package-lock.json` file changes in your project. It's important to remember that all build `script`s are executed in CI *after* the `install` step (aka post-`npm install`).
|
||||
|
||||
This is important because `npm` uses the pinned dependencies in your `package-lock.json` file to build the `node_modules` directory. However, the dependencies defined within the `package.json` file can be modified manually, for example, to become misaligned with the dependencies defined within the `package-lock.json`. So when `npm install` executes, the `package-lock.json` file will be updated to mirror the modified `package.json` changes.
|
||||
|
||||
However, when these changes surface within a CI build, this indicates differing dependency expectations between the committed `package.json` file and the `package-lock.json` file, which is a good reason to fail a build.
|
||||
|
||||
### What is this `npm run is-es5` check?
|
||||
|
||||
This project outputs production files to the `dist` folder. The `npm script`, `npm run is-es5`, checks the JavaScript files in the `dist` folder to make sure that they are `ES5`-compliant.
|
||||
|
||||
This check is important because `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/) - particularly for `IE11`.
|
||||
|
||||
### `deploy` step
|
||||
|
||||
How your project deploys will probably differ between the cookie cutter and your own application.
|
||||
|
||||
For demonstrational purposes, the cookie cutter deploys to GitHub pages using [ GitHUb CI ].
|
||||
|
||||
Your application might deploy to an `S3` bucket or to `npm`.
|
||||
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 |
@@ -70,31 +70,38 @@ Confirm the following workflows:
|
||||
|
||||
- [ ] *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.
|
||||
|
||||
@@ -11,5 +11,6 @@ module.exports = createConfig('jest', {
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
'testUtils', // don't unit test jest mocking tools
|
||||
],
|
||||
});
|
||||
|
||||
62878
package-lock.json
generated
62878
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.4.46",
|
||||
"version": "1.6.0",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -8,7 +8,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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/",
|
||||
@@ -17,8 +16,7 @@
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"watch-tests": "jest --watch",
|
||||
"travis-deploy-once": "travis-deploy-once"
|
||||
"watch-tests": "jest --watch"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -26,11 +24,17 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"not ie > 0",
|
||||
"not ie_mob > 0"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "10.1.1",
|
||||
"@edx/frontend-platform": "1.9.5",
|
||||
"@edx/paragon": "14.16.4",
|
||||
"@edx/frontend-component-footer": "^11.1.1",
|
||||
"@edx/frontend-component-header": "^3.1.1",
|
||||
"@edx/frontend-platform": "2.5.0",
|
||||
"@edx/paragon": "19.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
@@ -44,13 +48,13 @@
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "4.10.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "6.13.0",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^2.9.0",
|
||||
"react-redux": "^5.1.1",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
@@ -60,14 +64,14 @@
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"sass": "^1.49.0",
|
||||
"util": "^0.12.3",
|
||||
"whatwg-fetch": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.5.2",
|
||||
"@edx/frontend-build": "9.1.1",
|
||||
"axios": "0.21.1",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"codecov": "^3.6.1",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"es-check": "^2.3.0",
|
||||
"fetch-mock": "^6.5.2",
|
||||
@@ -78,7 +82,6 @@
|
||||
"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"
|
||||
"semantic-release": "^17.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
44
src/App.jsx
44
src/App.jsx
@@ -1,36 +1,36 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
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 EdxHeader from 'components/EdxHeader';
|
||||
import './App.scss';
|
||||
import Head from './head/Head';
|
||||
|
||||
const App = () => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<EdxHeader />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routePath}
|
||||
component={GradebookPage}
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
<AppProvider store={store}>
|
||||
<Head />
|
||||
<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";
|
||||
|
||||
@@ -2,28 +2,25 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
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 EdxHeader from 'components/EdxHeader';
|
||||
|
||||
import App from './App';
|
||||
import Head from './head/Head';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
BrowserRouter: () => 'BrowserRouter',
|
||||
Route: () => 'Route',
|
||||
Switch: () => 'Switch',
|
||||
}));
|
||||
jest.mock('react-redux', () => ({
|
||||
Provider: () => 'Provider',
|
||||
}));
|
||||
jest.mock('react-intl', () => ({
|
||||
IntlProvider: () => 'IntlProvider',
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppProvider: () => 'AppProvider',
|
||||
}));
|
||||
jest.mock('data/constants/app', () => ({
|
||||
routePath: '/:courseId',
|
||||
@@ -31,7 +28,7 @@ jest.mock('data/constants/app', () => ({
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
jest.mock('data/store', () => 'testStore');
|
||||
jest.mock('containers/GradebookPage', () => 'GradebookPage');
|
||||
jest.mock('components/EdxHeader', () => 'EdxHeader');
|
||||
jest.mock('@edx/frontend-component-header', () => 'Header');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
@@ -45,28 +42,25 @@ describe('App router component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.childAt(0).childAt(0);
|
||||
router = el.childAt(1);
|
||||
});
|
||||
describe('IntlProvider', () => {
|
||||
test('outer-wrapper component', () => {
|
||||
expect(el.type()).toBe(IntlProvider);
|
||||
});
|
||||
test('"en" locale', () => {
|
||||
expect(el.props().locale).toEqual('en');
|
||||
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('Provider, inside IntlProvider', () => {
|
||||
test('first child, passed the redux store props', () => {
|
||||
expect(el.childAt(0).type()).toBe(Provider);
|
||||
expect(el.childAt(0).props().store).toEqual(store);
|
||||
describe('Head', () => {
|
||||
test('first child of AppProvider', () => {
|
||||
expect(el.childAt(0).type()).toBe(Head);
|
||||
});
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('first child of Provider', () => {
|
||||
test('second child of AppProvider', () => {
|
||||
expect(router.type()).toBe(Router);
|
||||
});
|
||||
test('EdxHeader is above/outside-of the routing', () => {
|
||||
expect(router.childAt(0).childAt(0).type()).toBe(EdxHeader);
|
||||
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', () => {
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
<AppProvider
|
||||
store="testStore"
|
||||
>
|
||||
<Provider
|
||||
store="testStore"
|
||||
>
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<EdxHeader />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="GradebookPage"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="GradebookPage"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</AppProvider>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.bulk-management-history-view {
|
||||
.help-text {
|
||||
margin-bottom: 40px;
|
||||
max-width: 70%;
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,12 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import ResultsSummary from './ResultsSummary';
|
||||
import messages from './messages';
|
||||
|
||||
export const mapHistoryRows = ({
|
||||
resultsSummary,
|
||||
@@ -32,20 +30,13 @@ export const mapHistoryRows = ({
|
||||
export const HistoryTable = ({
|
||||
bulkManagementHistory,
|
||||
}) => (
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage {...messages.hint1} />
|
||||
<br />
|
||||
<FormattedMessage {...messages.hint2} />
|
||||
</p>
|
||||
|
||||
<Table
|
||||
data={bulkManagementHistory.map(mapHistoryRows)}
|
||||
hasFixedColumnWidths
|
||||
columns={bulkManagementColumns}
|
||||
className="table-striped"
|
||||
/>
|
||||
</>
|
||||
<DataTable
|
||||
data={bulkManagementHistory.map(mapHistoryRows)}
|
||||
hasFixedColumnWidths
|
||||
columns={bulkManagementColumns}
|
||||
className="table-striped"
|
||||
itemCount={bulkManagementHistory.length}
|
||||
/>
|
||||
);
|
||||
HistoryTable.defaultProps = {
|
||||
bulkManagementHistory: [],
|
||||
@@ -1,23 +1,20 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Table } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
|
||||
import ResultsSummary from './ResultsSummary';
|
||||
import { HistoryTable, mapStateToProps } from './HistoryTable';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Table: () => 'Table',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -58,23 +55,13 @@ 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).getElement()).toEqual(<FormattedMessage {...messages.hint1} />);
|
||||
expect(hints.childAt(1).is('br')).toEqual(true);
|
||||
expect(hints.childAt(2).getElement()).toEqual(<FormattedMessage {...messages.hint2} />);
|
||||
});
|
||||
describe('history table', () => {
|
||||
let table;
|
||||
beforeEach(() => {
|
||||
table = el.find(Table);
|
||||
table = el.find(DataTable);
|
||||
});
|
||||
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => {
|
||||
const fieldAssertions = [
|
||||
@@ -15,7 +15,7 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - b
|
||||
<FormattedMessage
|
||||
defaultMessage="CSV processing. File uploads may take several minutes to complete."
|
||||
description="Success Dialog message in BulkManagement Tab File Upload Form"
|
||||
id="gradebook.BulkManagementTab.successDialog"
|
||||
id="gradebook.BulkManagementHistoryView.successDialog"
|
||||
/>
|
||||
</Alert>
|
||||
</Fragment>
|
||||
@@ -38,7 +38,7 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - d
|
||||
<FormattedMessage
|
||||
defaultMessage="CSV processing. File uploads may take several minutes to complete."
|
||||
description="Success Dialog message in BulkManagement Tab File Upload Form"
|
||||
id="gradebook.BulkManagementTab.successDialog"
|
||||
id="gradebook.BulkManagementHistoryView.successDialog"
|
||||
/>
|
||||
</Alert>
|
||||
</Fragment>
|
||||
@@ -0,0 +1,118 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HistoryTable component snapshot history table data (from bulkManagementHistory.map(this.formatHistoryRow) snapshot: maps resultsSummay to ResultsSummary, wraps filename and user, forwards the rest 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
|
||||
<DataTable
|
||||
className="table-striped"
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": "Gradebook",
|
||||
"accessor": "filename",
|
||||
"columnSortable": false,
|
||||
"width": "col-5",
|
||||
},
|
||||
Object {
|
||||
"Header": "Download Summary",
|
||||
"accessor": "resultsSummary",
|
||||
"columnSortable": false,
|
||||
"width": "col",
|
||||
},
|
||||
Object {
|
||||
"Header": "Who",
|
||||
"accessor": "user",
|
||||
"columnSortable": false,
|
||||
"width": "col-1",
|
||||
},
|
||||
Object {
|
||||
"Header": "When",
|
||||
"accessor": "timeUploaded",
|
||||
"columnSortable": false,
|
||||
"width": "col",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -4,20 +4,21 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import BulkManagementAlerts from './BulkManagementAlerts';
|
||||
import FileUploadForm from './FileUploadForm';
|
||||
import HistoryTable from './HistoryTable';
|
||||
|
||||
/**
|
||||
* <BulkManagementTab />
|
||||
* <BulkManagementHistoryView />
|
||||
* top-level view for managing uploads of bulk management override csvs.
|
||||
*/
|
||||
export const BulkManagementTab = () => (
|
||||
<div>
|
||||
<h4><FormattedMessage {...(messages.heading)} /></h4>
|
||||
export const BulkManagementHistoryView = () => (
|
||||
<div className="bulk-management-history-view">
|
||||
<h4><FormattedMessage {...messages.heading} /></h4>
|
||||
<p className="help-text">
|
||||
<FormattedMessage {...messages.helpText} />
|
||||
</p>
|
||||
<BulkManagementAlerts />
|
||||
<FileUploadForm />
|
||||
<HistoryTable />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default BulkManagementTab;
|
||||
export default BulkManagementHistoryView;
|
||||
@@ -3,27 +3,24 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
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(', ')}`, () => {
|
||||
@@ -39,8 +36,7 @@ describe('BulkManagementTab', () => {
|
||||
});
|
||||
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,132 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HistoryTable component snapshot history table data (from bulkManagementHistory.map(this.formatHistoryRow) snapshot: maps resultsSummay to ResultsSummary, wraps filename and user, forwards the rest 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`HistoryTable component snapshot snapshot - loads hints display, formatted table 1`] = `
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Results appear in the table below."
|
||||
description="Hint text on BulkManagement Tab History Table"
|
||||
id="gradebook.BulkManagementTab.hint1"
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
defaultMessage="Grade processing may take a few seconds."
|
||||
description="Hint text on BulkManagement Tab History Table"
|
||||
id="gradebook.BulkManagementTab.hint2"
|
||||
/>
|
||||
</p>
|
||||
<Table
|
||||
className="table-striped"
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "filename",
|
||||
"label": "Gradebook",
|
||||
"width": "col-5",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "resultsSummary",
|
||||
"label": "Download Summary",
|
||||
"width": "col",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "user",
|
||||
"label": "Who",
|
||||
"width": "col-1",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "timeUploaded",
|
||||
"label": "When",
|
||||
"width": "col",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,16 +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>
|
||||
<FormattedMessage
|
||||
defaultMessage="Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload."
|
||||
description="Heading text for BulkManagement Tab"
|
||||
id="gradebook.BulkManagementTab.heading"
|
||||
/>
|
||||
</h4>
|
||||
<BulkManagementAlerts />
|
||||
<FileUploadForm />
|
||||
<HistoryTable />
|
||||
</div>
|
||||
`;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
csvUploadLabel: {
|
||||
id: 'gradebook.BulkManagementTab.csvUploadLabel',
|
||||
defaultMessage: 'Upload Grade CSV',
|
||||
description: 'Button in BulkManagementTab Alerts',
|
||||
},
|
||||
heading: {
|
||||
id: 'gradebook.BulkManagementTab.heading',
|
||||
defaultMessage: 'Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload.',
|
||||
description: 'Heading text for BulkManagement Tab',
|
||||
},
|
||||
hint1: {
|
||||
id: 'gradebook.BulkManagementTab.hint1',
|
||||
defaultMessage: 'Results appear in the table below.',
|
||||
description: 'Hint text on BulkManagement Tab History Table',
|
||||
},
|
||||
hint2: {
|
||||
id: 'gradebook.BulkManagementTab.hint2',
|
||||
defaultMessage: 'Grade processing may take a few seconds.',
|
||||
description: 'Hint text on BulkManagement Tab History Table',
|
||||
},
|
||||
importBtnText: {
|
||||
id: 'gradebook.BulkManagementTab.importBtnText',
|
||||
defaultMessage: 'Import Grades',
|
||||
description: 'Button in BulkManagement Tab File Upload Form',
|
||||
},
|
||||
successDialog: {
|
||||
id: 'gradebook.BulkManagementTab.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;
|
||||
@@ -31,14 +31,14 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Connect(AssignmentTypeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<AssignmentTypeFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
<Connect(AssignmentFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<AssignmentFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
<Connect(AssignmentGradeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<AssignmentGradeFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
@@ -53,8 +53,8 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Connect(CourseGradeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<CourseGradeFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
@@ -68,8 +68,8 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InjectIntl(ShimmedIntlComponent)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<StudentGroupsFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
|
||||
@@ -22,6 +22,11 @@ jest.mock('@edx/paragon', () => ({
|
||||
jest.mock('@edx/paragon/icons', () => ({
|
||||
Close: 'paragon.icons.Close',
|
||||
}));
|
||||
jest.mock('./AssignmentTypeFilter', () => 'AssignmentTypeFilter');
|
||||
jest.mock('./AssignmentFilter', () => 'AssignmentFilter');
|
||||
jest.mock('./AssignmentGradeFilter', () => 'AssignmentGradeFilter');
|
||||
jest.mock('./CourseGradeFilter', () => 'CourseGradeFilter');
|
||||
jest.mock('./StudentGroupsFilter', () => 'StudentGroupsFilter');
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -50,7 +55,7 @@ describe('GradebookFilters', () => {
|
||||
closeMenu: jest.fn().mockName('this.props.closeMenu'),
|
||||
fetchGrades: jest.fn(),
|
||||
updateIncludeCourseRoleMembers: jest.fn(),
|
||||
updateQueryParams: jest.fn(),
|
||||
updateQueryParams: jest.fn().mockName('this.props.updateQueryParams'),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -26,9 +26,13 @@ exports[`GradebookHeader component snapshots default values (grades frozen, cann
|
||||
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"
|
||||
@@ -68,9 +72,13 @@ exports[`GradebookHeader component snapshots grades frozen, can view. grades fro
|
||||
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"
|
||||
@@ -110,9 +118,13 @@ exports[`GradebookHeader component snapshots grades frozen, cannot view unauthor
|
||||
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"
|
||||
@@ -135,3 +147,115 @@ exports[`GradebookHeader component snapshots grades frozen, cannot view unauthor
|
||||
</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,18 +2,37 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
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`
|
||||
`${getConfig().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">
|
||||
@@ -27,7 +46,17 @@ export class GradebookHeader extends React.Component {
|
||||
<h1>
|
||||
<FormattedMessage {...messages.gradebook} />
|
||||
</h1>
|
||||
<h3>{this.props.courseId}</h3>
|
||||
<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">
|
||||
@@ -49,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);
|
||||
|
||||
@@ -21,6 +21,16 @@ const messages = defineMessages({
|
||||
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,42 +1,119 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import { GradebookHeader, mapStateToProps } from '.';
|
||||
import { views } from 'data/constants/app';
|
||||
import messages from './messages';
|
||||
import { GradebookHeader, mapDispatchToProps, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: messages => messages,
|
||||
FormattedMessage: 'FormattedMessage',
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -46,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));
|
||||
});
|
||||
@@ -59,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,108 +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 { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
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(<FormattedMessage {...messages.bulkManagement} />)}
|
||||
onClick={this.handleClickExportGrades}
|
||||
/>
|
||||
<StatefulButton
|
||||
{...this.buttonProps(<FormattedMessage {...messages.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,82 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1`] = `
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesTab.table.totalGradePercentage"
|
||||
/>
|
||||
</Tooltip>
|
||||
`;
|
||||
|
||||
exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
key="left-basic"
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesTab.table.totalGradePercentage"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="left"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade (%)"
|
||||
description="Gradebook table total grade column header"
|
||||
id="gradebook.GradesTab.table.headings.totalGrade"
|
||||
/>
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
screenReaderText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesTab.table.totalGradePercentage"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Username"
|
||||
description="Gradebook table username column header"
|
||||
id="gradebook.GradesTab.table.headings.username"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="font-weight-normal student-key"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Student Key*"
|
||||
description="Gradebook table Student Key label"
|
||||
id="gradebook.GradesTab.table.labels.studentKey"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,51 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookTable component snapshot - fields1 and 2 between email and totalGrade, mocked rows 1`] = `
|
||||
<div
|
||||
className="gradebook-container"
|
||||
>
|
||||
<div
|
||||
className="gbook"
|
||||
>
|
||||
<Table
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "Username",
|
||||
"label": <UsernameLabelReplacement />,
|
||||
},
|
||||
Object {
|
||||
"key": "Email",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Email"
|
||||
description="Gradebook table email column header"
|
||||
id="gradebook.GradesTab.table.headings.email"
|
||||
/>,
|
||||
},
|
||||
Object {
|
||||
"key": "field1",
|
||||
"label": "field1",
|
||||
},
|
||||
Object {
|
||||
"key": "field2",
|
||||
"label": "field2",
|
||||
},
|
||||
Object {
|
||||
"key": "Total Grade (%)",
|
||||
"label": <TotalGradeLabelReplacement />,
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"mappedRow: 1",
|
||||
"mappedRow: 2",
|
||||
"mappedRow: 3",
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
rowHeaderColumnKey="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,56 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SearchControls Component Snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
defaultMessage="Step 1: Filter the Grade Report"
|
||||
description="Filter controls container heading string"
|
||||
id="gradebook.GradesTab.filterHeading"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit Filters"
|
||||
description="Button text on Grades tab to open/close the Filters tab"
|
||||
id="gradebook.GradesTab.editFilterLabel"
|
||||
/>
|
||||
</Button>
|
||||
<div>
|
||||
<SearchField
|
||||
inputLabel={
|
||||
<FormattedMessage
|
||||
defaultMessage="Search for a learner"
|
||||
description="Search description label"
|
||||
id="gradebook.GradesTab.search.label"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction onChange]}
|
||||
onClear={[MockFunction onClear]}
|
||||
onSubmit={[MockFunction fetchGrades]}
|
||||
value="alice"
|
||||
/>
|
||||
<small
|
||||
className="form-text text-muted search-help-text"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Search by username, email, or student key"
|
||||
description="Search hint label"
|
||||
id="gradebook.GradesTab.search.hint"
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -1,24 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusAlerts snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<StatusAlert
|
||||
alertType="success"
|
||||
dialog={
|
||||
<FormattedMessage
|
||||
defaultMessage="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
|
||||
description="Alert text for successful edit action"
|
||||
id="gradebook.GradesTab.editSuccessAlert"
|
||||
/>
|
||||
}
|
||||
onClose={[MockFunction handleCloseSuccessBanner]}
|
||||
open={true}
|
||||
/>
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog="the quiCk brown does somEthing or other"
|
||||
dismissible={false}
|
||||
open={false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -1,76 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
bulkManagement: {
|
||||
id: 'gradebook.GradesTab.BulkManagementControls.bulkManagementLabel',
|
||||
defaultMessage: 'Bulk Management',
|
||||
description: 'Button text for bulk grades download control in GradesTab',
|
||||
},
|
||||
interventions: {
|
||||
id: 'gradebook.GradesTab.BulkManagementControls.interventionsLabel',
|
||||
defaultMessage: 'Interventions',
|
||||
description: 'Button text for intervention report download control in GradesTab',
|
||||
},
|
||||
scoreView: {
|
||||
id: 'gradebook.GradesTab.scoreViewLabel',
|
||||
defaultMessage: 'Score View',
|
||||
description: 'Score format select dropdown label',
|
||||
},
|
||||
absolute: {
|
||||
id: 'gradebook.GradesTab.absoluteOption',
|
||||
defaultMessage: 'Absolute',
|
||||
description: 'Score format select dropdown option',
|
||||
},
|
||||
percent: {
|
||||
id: 'gradebook.GradesTab.percentOption',
|
||||
defaultMessage: 'Percent',
|
||||
description: 'Score format select dropdown option',
|
||||
},
|
||||
filterStepHeading: {
|
||||
id: 'gradebook.GradesTab.filterHeading',
|
||||
defaultMessage: 'Step 1: Filter the Grade Report',
|
||||
description: 'Filter controls container heading string',
|
||||
},
|
||||
editFilters: {
|
||||
id: 'gradebook.GradesTab.editFilterLabel',
|
||||
defaultMessage: 'Edit Filters',
|
||||
description: 'Button text on Grades tab to open/close the Filters tab',
|
||||
},
|
||||
searchLabel: {
|
||||
id: 'gradebook.GradesTab.search.label',
|
||||
defaultMessage: 'Search for a learner',
|
||||
description: 'Search description label',
|
||||
},
|
||||
searchHint: {
|
||||
id: 'gradebook.GradesTab.search.hint',
|
||||
defaultMessage: 'Search by username, email, or student key',
|
||||
description: 'Search hint label',
|
||||
},
|
||||
editSuccessAlert: {
|
||||
id: 'gradebook.GradesTab.editSuccessAlert',
|
||||
defaultMessage: 'The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.',
|
||||
description: 'Alert text for successful edit action',
|
||||
},
|
||||
maxGradeInvalid: {
|
||||
id: 'gradebook.GradesTab.maxCourseGradeInvalid',
|
||||
defaultMessage: 'Maximum course grade must be between 0 and 100',
|
||||
description: 'Alert text for invalid maximum course grade',
|
||||
},
|
||||
minGradeInvalid: {
|
||||
id: 'gradebook.GradesTab.minCourseGradeInvalid',
|
||||
defaultMessage: 'Minimum course grade must be between 0 and 100',
|
||||
description: 'Alert text for invalid minimum course grade',
|
||||
},
|
||||
gradebookStepHeading: {
|
||||
id: 'gradebook.GradesTab.gradebookStepHeading',
|
||||
defaultMessage: 'Step 2: View or Modify Individual Grades',
|
||||
description: 'Alert text for invalid minimum course grade',
|
||||
},
|
||||
mastersHint: {
|
||||
id: 'gradebook.GradesTab.mastersHint',
|
||||
defaultMessage: "available for learners in the Master's track only",
|
||||
description: 'Masters feature availability hint on Grades Tab',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
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: 'A labeled button that allows an admin user to download course grades all at once (in bulk).',
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import { Form } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
/**
|
||||
* <AdjustedGradeInput />
|
||||
@@ -32,7 +33,7 @@ export class AdjustedGradeInput extends React.Component {
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{this.props.possibleGrade && ` / ${this.props.possibleGrade}`}
|
||||
{this.props.possibleGrade && ` ${isRtl(getLocale()) ? '\\' : '/'} ${this.props.possibleGrade}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +1,40 @@
|
||||
// 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
|
||||
<DataTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "date",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Date"
|
||||
description="Edit Modal Override Table Date column header"
|
||||
id="gradebook.GradesTab.EditModal.Overrides.dateHeader"
|
||||
id="gradebook.GradesView.EditModal.Overrides.dateHeader"
|
||||
/>,
|
||||
"accessor": "date",
|
||||
},
|
||||
Object {
|
||||
"key": "grader",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Grader"
|
||||
description="Edit Modal Override Table Grader column header"
|
||||
id="gradebook.GradesTab.EditModal.Overrides.graderHeader"
|
||||
id="gradebook.GradesView.EditModal.Overrides.graderHeader"
|
||||
/>,
|
||||
"accessor": "grader",
|
||||
},
|
||||
Object {
|
||||
"key": "reason",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Reason"
|
||||
description="Edit Modal Override Table Reason column header"
|
||||
id="gradebook.GradesTab.EditModal.Overrides.reasonHeader"
|
||||
id="gradebook.GradesView.EditModal.Overrides.reasonHeader"
|
||||
/>,
|
||||
"accessor": "reason",
|
||||
},
|
||||
Object {
|
||||
"key": "adjustedGrade",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Adjusted grade"
|
||||
description="Edit Modal Override Table Adjusted grade column header"
|
||||
id="gradebook.GradesTab.EditModal.Overrides.adjustedGradeHeader"
|
||||
id="gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader"
|
||||
/>,
|
||||
"accessor": "adjustedGrade",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -59,5 +59,6 @@ exports[`OverrideTable Component snapshots basic snapshot shows a row for each e
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
@@ -27,14 +27,14 @@ export const OverrideTable = ({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Table
|
||||
<DataTable
|
||||
columns={[
|
||||
{ label: <FormattedMessage {...messages.dateHeader} />, key: columns.date },
|
||||
{ label: <FormattedMessage {...messages.graderHeader} />, key: columns.grader },
|
||||
{ label: <FormattedMessage {...messages.reasonHeader} />, key: columns.reason },
|
||||
{ Header: <FormattedMessage {...messages.dateHeader} />, accessor: columns.date },
|
||||
{ Header: <FormattedMessage {...messages.graderHeader} />, accessor: columns.grader },
|
||||
{ Header: <FormattedMessage {...messages.reasonHeader} />, accessor: columns.reason },
|
||||
{
|
||||
label: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
key: columns.adjustedGrade,
|
||||
Header: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
accessor: columns.adjustedGrade,
|
||||
},
|
||||
]}
|
||||
data={[
|
||||
@@ -45,6 +45,7 @@ export const OverrideTable = ({
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
]}
|
||||
itemCount={gradeOverrides.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,22 +2,22 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
adjustedGradeHeader: {
|
||||
id: 'gradebook.GradesTab.EditModal.Overrides.adjustedGradeHeader',
|
||||
id: 'gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader',
|
||||
defaultMessage: 'Adjusted grade',
|
||||
description: 'Edit Modal Override Table Adjusted grade column header',
|
||||
},
|
||||
dateHeader: {
|
||||
id: 'gradebook.GradesTab.EditModal.Overrides.dateHeader',
|
||||
id: 'gradebook.GradesView.EditModal.Overrides.dateHeader',
|
||||
defaultMessage: 'Date',
|
||||
description: 'Edit Modal Override Table Date column header',
|
||||
},
|
||||
graderHeader: {
|
||||
id: 'gradebook.GradesTab.EditModal.Overrides.graderHeader',
|
||||
id: 'gradebook.GradesView.EditModal.Overrides.graderHeader',
|
||||
defaultMessage: 'Grader',
|
||||
description: 'Edit Modal Override Table Grader column header',
|
||||
},
|
||||
reasonHeader: {
|
||||
id: 'gradebook.GradesTab.EditModal.Overrides.reasonHeader',
|
||||
id: 'gradebook.GradesView.EditModal.Overrides.reasonHeader',
|
||||
defaultMessage: 'Reason',
|
||||
description: 'Edit Modal Override Table Reason column header',
|
||||
},
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
mapStateToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({ Table: () => 'Table' }));
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
jest.mock('./ReasonInput', () => 'ReasonInput');
|
||||
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
||||
|
||||
@@ -8,7 +8,7 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Edit Modal Assignment header"
|
||||
id="gradebook.GradesTab.EditModal.headers.assignment"
|
||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
||||
/>
|
||||
}
|
||||
value="Qwerty"
|
||||
@@ -19,7 +19,7 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty
|
||||
<FormattedMessage
|
||||
defaultMessage="Student"
|
||||
description="Edit Modal Student header"
|
||||
id="gradebook.GradesTab.EditModal.headers.student"
|
||||
id="gradebook.GradesView.EditModal.headers.student"
|
||||
/>
|
||||
}
|
||||
value="Uiop"
|
||||
@@ -30,7 +30,7 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty
|
||||
<FormattedMessage
|
||||
defaultMessage="Original Grade"
|
||||
description="Edit Modal Original Grade header"
|
||||
id="gradebook.GradesTab.EditModal.headers.originalGrade"
|
||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
||||
/>
|
||||
}
|
||||
value={20}
|
||||
@@ -41,7 +41,7 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty
|
||||
<FormattedMessage
|
||||
defaultMessage="Current Grade"
|
||||
description="Edit Modal Current Grade header"
|
||||
id="gradebook.GradesTab.EditModal.headers.currentGrade"
|
||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
||||
/>
|
||||
}
|
||||
value={2}
|
||||
@@ -57,7 +57,7 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Edit Modal Assignment header"
|
||||
id="gradebook.GradesTab.EditModal.headers.assignment"
|
||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
||||
/>
|
||||
}
|
||||
value="Qwerty"
|
||||
@@ -68,7 +68,7 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and
|
||||
<FormattedMessage
|
||||
defaultMessage="Student"
|
||||
description="Edit Modal Student header"
|
||||
id="gradebook.GradesTab.EditModal.headers.student"
|
||||
id="gradebook.GradesView.EditModal.headers.student"
|
||||
/>
|
||||
}
|
||||
value="Uiop"
|
||||
@@ -79,7 +79,7 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and
|
||||
<FormattedMessage
|
||||
defaultMessage="Original Grade"
|
||||
description="Edit Modal Original Grade header"
|
||||
id="gradebook.GradesTab.EditModal.headers.originalGrade"
|
||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
||||
/>
|
||||
}
|
||||
value={20}
|
||||
@@ -90,7 +90,7 @@ exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and
|
||||
<FormattedMessage
|
||||
defaultMessage="Current Grade"
|
||||
description="Edit Modal Current Grade header"
|
||||
id="gradebook.GradesTab.EditModal.headers.currentGrade"
|
||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
||||
/>
|
||||
}
|
||||
value={2}
|
||||
@@ -5,25 +5,26 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
|
||||
body={
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog="Weve been trying to contact you regarding..."
|
||||
<Alert
|
||||
dismissible={false}
|
||||
open={true}
|
||||
/>
|
||||
show={true}
|
||||
variant="danger"
|
||||
>
|
||||
Weve been trying to contact you regarding...
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
||||
description="Edit Modal visibility hint message"
|
||||
id="gradebook.GradesTab.EditModal.contactSupport"
|
||||
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.GradesTab.EditModal.saveVisibility"
|
||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +38,7 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Grades"
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesTab.EditModal.saveGrade"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
@@ -46,7 +47,7 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesTab.EditModal.closeText"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
@@ -55,7 +56,7 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit Grades"
|
||||
description="Edit Modal title"
|
||||
id="gradebook.GradesTab.EditModal.title"
|
||||
id="gradebook.GradesView.EditModal.title"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -66,25 +67,26 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
|
||||
body={
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog=""
|
||||
<Alert
|
||||
dismissible={false}
|
||||
open={false}
|
||||
/>
|
||||
show={false}
|
||||
variant="danger"
|
||||
>
|
||||
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
||||
description="Edit Modal visibility hint message"
|
||||
id="gradebook.GradesTab.EditModal.contactSupport"
|
||||
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.GradesTab.EditModal.saveVisibility"
|
||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,7 +100,7 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Grades"
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesTab.EditModal.saveGrade"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
@@ -107,7 +109,7 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesTab.EditModal.closeText"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
@@ -116,7 +118,7 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit Grades"
|
||||
description="Edit Modal title"
|
||||
id="gradebook.GradesTab.EditModal.title"
|
||||
id="gradebook.GradesView.EditModal.title"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
StatusAlert,
|
||||
Alert,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -53,12 +53,13 @@ export class EditModal extends React.Component {
|
||||
body={(
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog={this.props.gradeOverrideHistoryError}
|
||||
open={!!this.props.gradeOverrideHistoryError}
|
||||
<Alert
|
||||
variant="danger"
|
||||
show={!!this.props.gradeOverrideHistoryError}
|
||||
dismissible={false}
|
||||
/>
|
||||
>
|
||||
{this.props.gradeOverrideHistoryError}
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div><FormattedMessage {...messages.visibility} /></div>
|
||||
<div><FormattedMessage {...messages.saveVisibility} /></div>
|
||||
@@ -2,47 +2,47 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
assignmentHeader: {
|
||||
id: 'gradebook.GradesTab.EditModal.headers.assignment',
|
||||
id: 'gradebook.GradesView.EditModal.headers.assignment',
|
||||
defaultMessage: 'Assignment',
|
||||
description: 'Edit Modal Assignment header',
|
||||
},
|
||||
currentGradeHeader: {
|
||||
id: 'gradebook.GradesTab.EditModal.headers.currentGrade',
|
||||
id: 'gradebook.GradesView.EditModal.headers.currentGrade',
|
||||
defaultMessage: 'Current Grade',
|
||||
description: 'Edit Modal Current Grade header',
|
||||
},
|
||||
originalGradeHeader: {
|
||||
id: 'gradebook.GradesTab.EditModal.headers.originalGrade',
|
||||
id: 'gradebook.GradesView.EditModal.headers.originalGrade',
|
||||
defaultMessage: 'Original Grade',
|
||||
description: 'Edit Modal Original Grade header',
|
||||
},
|
||||
studentHeader: {
|
||||
id: 'gradebook.GradesTab.EditModal.headers.student',
|
||||
id: 'gradebook.GradesView.EditModal.headers.student',
|
||||
defaultMessage: 'Student',
|
||||
description: 'Edit Modal Student header',
|
||||
},
|
||||
title: {
|
||||
id: 'gradebook.GradesTab.EditModal.title',
|
||||
id: 'gradebook.GradesView.EditModal.title',
|
||||
defaultMessage: 'Edit Grades',
|
||||
description: 'Edit Modal title',
|
||||
},
|
||||
closeText: {
|
||||
id: 'gradebook.GradesTab.EditModal.closeText',
|
||||
id: 'gradebook.GradesView.EditModal.closeText',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Edit Modal close button text',
|
||||
},
|
||||
visibility: {
|
||||
id: 'gradebook.GradesTab.EditModal.contactSupport',
|
||||
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.GradesTab.EditModal.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.GradesTab.EditModal.saveGrade',
|
||||
id: 'gradebook.GradesView.EditModal.saveGrade',
|
||||
defaultMessage: 'Save Grades',
|
||||
description: 'Edit Modal Save button label',
|
||||
},
|
||||
@@ -16,7 +16,7 @@ jest.mock('./ModalHeaders', () => 'ModalHeaders');
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
Modal: () => 'Modal',
|
||||
StatusAlert: () => 'StatusAlert',
|
||||
Alert: () => 'Alert',
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
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: 'A labeled button in the Grades tab that opens/closes the Filters tab, allowing the grades to be filtered',
|
||||
},
|
||||
});
|
||||
|
||||
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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -23,7 +23,7 @@ const TotalGradeLabelReplacement = () => (
|
||||
<OverlayTrigger
|
||||
trigger={['hover', 'focus']}
|
||||
key="left-basic"
|
||||
placement="left"
|
||||
placement={isRtl(getLocale()) ? 'right' : 'left'}
|
||||
overlay={(
|
||||
<Tooltip id="course-grade-tooltip">
|
||||
<FormattedMessage {...messages.totalGradePercentage} />
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { OverlayTrigger } from '@edx/paragon';
|
||||
|
||||
@@ -35,3 +36,17 @@ describe('LabelReplacements', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
let el;
|
||||
test('right to left overlay placement', () => {
|
||||
getLocale.mockImplementation(() => 'en');
|
||||
el = shallow(<TotalGradeLabelReplacement />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('left to right overlay placement', () => {
|
||||
getLocale.mockImplementation(() => 'ar');
|
||||
el = shallow(<TotalGradeLabelReplacement />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1`] = `
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
</Tooltip>
|
||||
`;
|
||||
|
||||
exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
key="left-basic"
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="left"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade (%)"
|
||||
description="Gradebook table total grade column header"
|
||||
id="gradebook.GradesView.table.headings.totalGrade"
|
||||
/>
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
screenReaderText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Username"
|
||||
description="Gradebook table username column header"
|
||||
id="gradebook.GradesView.table.headings.username"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="font-weight-normal student-key"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Student Key*"
|
||||
description="Gradebook table Student Key label"
|
||||
id="gradebook.GradesView.table.labels.studentKey"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`snapshot left to right overlay placement 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
key="left-basic"
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="right"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade (%)"
|
||||
description="Gradebook table total grade column header"
|
||||
id="gradebook.GradesView.table.headings.totalGrade"
|
||||
/>
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
screenReaderText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`snapshot right to left overlay placement 1`] = `
|
||||
<div>
|
||||
<OverlayTrigger
|
||||
key="left-basic"
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="course-grade-tooltip"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="left"
|
||||
trigger={
|
||||
Array [
|
||||
"hover",
|
||||
"focus",
|
||||
]
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade (%)"
|
||||
description="Gradebook table total grade column header"
|
||||
id="gradebook.GradesView.table.headings.totalGrade"
|
||||
/>
|
||||
<div
|
||||
id="courseGradeTooltipIcon"
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-info-circle"
|
||||
screenReaderText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
||||
description="Gradebook table message that total grades are displayed in percent format"
|
||||
id="gradebook.GradesView.table.totalGradePercentage"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user