Compare commits

...

45 Commits

Author SHA1 Message Date
mashal-m
9976321804 fix: fix unit test 2022-12-16 16:00:37 +05:00
mashal-m
41f3704116 refactor: Paragon's message added to index intialize 2022-12-16 15:49:48 +05:00
mashal-m
82792033d5 build: major paragon version upgrade 2022-12-13 13:29:22 +05:00
Muhammad Abdullah Waheed
84cdacd4e8 feat: added new translations in Makefile
And updated all the translations themselves.

Issue: https://github.com/openedx/frontend-wg/issues/119
PR: https://github.com/openedx/frontend-app-gradebook/pull/260
2022-12-09 07:45:06 -03:00
Ghassan Maslamani
4fcc3f863f feat: ensure lms api synced with latest value in config
This change make it possible if LMS url to be changed, that
  the last value will be picked.

  This is simlair openedx/frontend-app-course-authoring/pull/389
  which issue overhangio/tutor-mfe/issues/86, the fixes is needed
  so that dynamic config would work with tutor:
  overhangio/tutor-mfe/pull/69
2022-12-08 18:21:19 +00:00
mashal-m
79679c23f2 refactor: migrate off paragon modal derpecation 2022-11-30 14:05:03 +00:00
mashal-m
9b2436991b refactor: migrate off paragon modal derpecation 2022-11-30 14:05:03 +00:00
edX requirements bot
c95f2d6b22 fix: -t flag added in pull translation command (#285) 2022-11-30 16:46:27 +05:00
Kyle McCormick
4f43e65f03 fix: use getConfig in order to support runtime configuration (#286)
Before, gradebook was reading config from `process.env`
directly, which locked the app into using only
static (build-time) configuration. In order to
enable dynamic (runtime) configuration, we update
gradebook to use frontend-platform's standard
configuration interface: `mergeConfig()` and `getConfig()`.

Bumps version from 1.5.0 to 1.6.0.
(I would normally just do a patch release for a fix, but the version
 was hasn't been bumped for a while, so adding in full runtime
 configuration support seemed like it warranted a proper
 minor version bump.)

Co-authored-by: Ghassan Maslamani <ghassan.maslamani@gmail.com>
2022-11-28 11:14:32 -05:00
ihor-romaniuk
50bf7d236a fix: add an asterisk to the 'Email' name of column 2022-11-23 10:45:36 -05:00
Diana Olarte
d2723e5bc1 feat: allow runtime configuration (#253)
Allows frontend-app-gradebook to be configured at
runtime using the LMS's new MFE Configuration API.

Part of https://github.com/openedx/frontend-wg/issues/103
2022-10-17 12:12:42 -04:00
Simon Chen
03fa143fc1 Merge pull request #266 from fennec-tech/rtl-use-backslash-to-write-fractions
RTL: use backslash to write fractions (grades)
2022-09-29 15:02:24 -04:00
Simon Chen
075846f869 Merge pull request #265 from fennec-tech/rtl-fix-percentage-direction
RTL: fix (%) symbol to follow text direction
2022-09-29 14:49:14 -04:00
Abderraouf Mehdi Bouhali
1208d27d92 fix(rtl): use backslash to write fractions (grades) 2022-09-29 11:34:30 +01:00
Abderraouf Mehdi Bouhali
e345716bd4 fix(rtl): force % symbol to follow text direction 2022-09-29 10:11:26 +01:00
Maman Khan
2121a63c83 Removing codecov and coveralls packages (#245)
* fix: removed coveralls and codecov packages with update in ci uploader

* fix: pinning npm to version 8.5.x

Co-authored-by: Abdullah Waheed <abdullah.waheed@arbisoft.com>
2022-09-28 10:47:57 -04:00
Matt Hughes
47cab71b3c fix: do not squish the table with word wrapping
we should handle word wrapping down at the level of specific cells
which we can opt-in to non-default word-wrapping
2022-07-12 10:54:00 -04:00
Leangseu Kim
2d8af2ec00 fix: update and resolve dependencies 2022-07-07 13:23:23 -04:00
Leangseu Kim
d55abbe91e fix: search blur state for every single charater input 2022-07-07 10:05:35 -04:00
Abdullah Waheed
a75f365bdd refactor: migrated StatusAlert to Alert in GradesBook component
refactor: migrated StatusAlert to Alert in StatusAlerts component

refactor: updated unit tests mocks to set snapshot issues
2022-06-30 21:35:01 -04:00
Muhammad Abdullah Waheed
bbb7e895a5 Paragon table deprecation updation (#246)
* refactor: updated table deprecation of HistoryTable

* refactor: updated table deprecation of OverrideTable

* refactor: updated table deprecation of GradebookTable

* refactor: updated unit tests of OverrideTable according to new changes

* fix: error for css

* fix: strict dictionary error for react component

* chore: update css and handle syntax errors

* chore: update unit test

* fix: remove datatable row status and update tests

* fix: test coverage

Co-authored-by: Leangseu Kim <lkim@edx.org>
Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
2022-06-30 14:22:29 -04:00
Muhammad Abdullah Waheed
bf70fd1450 fix: added specific npm version to fix peerDependency issue (#247) 2022-06-16 15:06:30 +05:00
Jawayria
af2ece8290 Merge pull request #242 from openedx/jenkins/version-check-c6a4685
feat: Add package-lock file version check
2022-05-06 15:56:23 +05:00
edX requirements bot
620827d772 feat: Add package-lock file version check 2022-04-29 08:48:48 -04:00
edX requirements bot
c6a4685bf5 chore!: Dropped support for Node 12 (#239)
* chore!: Dropped support for Node 12

* fix: dropped Node 12

Co-authored-by: Jawayria <39649635+Jawayria@users.noreply.github.com>
2022-04-14 12:53:54 -04:00
Leangseu Kim
8dd2237f9c chore: update package lock after update to node 16 2022-04-12 10:36:36 -04:00
Usama Sadiq
97c58157f8 Merge pull request #238 from openedx/jenkins/transifex-client-migration-f07a96c
fix: transifex migration to new client
2022-04-06 12:42:11 +05:00
UsamaSadiq
ce093efba4 build: update transifex pull translations command 2022-04-05 15:26:42 +05:00
Jawayria
799ef5b8a1 Merge pull request #224 from openedx/jawayria/node-16
feat: added support for node 16
2022-03-31 12:13:11 +05:00
Jawayria
f956351cf7 feat: add support for node 16 2022-03-31 12:01:12 +05:00
edX requirements bot
7772e21c6a fix: transifex migration to new client 2022-03-17 08:47:39 -04:00
Sarina Canelake
f07a96ce58 Merge DEPR automation workflow
Add DEPR workflow automation
2022-02-24 15:21:36 -05:00
Sarina Canelake
f64bc8d4a6 build: add DEPR workflow automation 2022-02-23 14:36:18 -05:00
Michael Vlasov
134dabb710 fix: gradebook table rtl tooltip position (#225)
* fix: gradebook table rtl tooltip position

Co-authored-by: vlasovmichael <mykhailo.vlasov@raccoongang.com>
2022-02-10 10:36:46 -05:00
Matt Hughes
65c25f00b6 Merge pull request #227 from openedx/matthugs/rename-github-group-to-match-org-structure
chore: rename github group to match org. structure
2022-02-09 10:10:49 -05:00
Matt Hughes
31748e246e chore: rename github group to match org. structure 2022-02-09 10:06:03 -05:00
Michael Vlasov
650be29ef9 feat: gradebook main container classname (#226)
* feat: gradebook main container classname

* feat: gradebook main container classname / snapshot tests

Co-authored-by: vlasovmichael <mykhailo.vlasov@raccoongang.com>
2022-02-07 15:22:26 -05:00
Jhony Avella
b713ab5748 feat: enabling header override (#220)
feat: new versions of package.json and lock

fix: tests after adding header component

Fixing tests

change package files

Signed-off-by: Jhony Avella <jhony.avella@edunext.co>
2021-12-21 10:40:22 -05:00
Jhony Avella
5fe80b4a52 fix: build the course id properly when the MFE is deployed in a subdirectory (#213) 2021-12-21 10:38:58 -05:00
Carlos Muniz
9e04813d06 Merge pull request #219 from edx/i18n-string-definitions
docs: Describe messages descriptions better
2021-12-03 09:35:26 -05:00
Carlos Muniz
a0e1a60d23 docs: Describe messages descriptions better
These messages have been updated to be more descriptive.
The message descriptions need to be more descriptive in order to
be easily translated (i18n) into other languages.
2021-12-03 09:23:17 -05:00
Sarina Canelake
68c7944dd5 Merge pull request #217 from regisb/regisb/transifex
feat: add compatibility with transifex
2021-11-29 12:37:26 -05:00
Régis Behmo
f4f6e5551f feat: add compatibility with transifex
Now that the frontend-app-gradebook resource was added to Transifex, we can
start pulling strings fromt there. For now, the project contains very few
translated string.

Strings were pulled by running: make pull_translations

This is for https://github.com/openedx/build-test-release-wg/issues/107
2021-11-22 12:12:29 +01:00
Usama Sadiq
ee99bfdaa4 Merge pull request #212 from edx/usamasadiq/replace-travis-with-github-ci
BOM-2928: Replace Travis with GitHub CI
2021-11-16 16:56:36 +05:00
Usama Sadiq
318ce349fc build: Replace Travis with GitHub CI 2021-10-25 17:12:32 +05:00
100 changed files with 49189 additions and 16769 deletions

2
.env
View File

@@ -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=''

View File

@@ -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
View File

@@ -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

View File

@@ -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

View 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
View 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 }}"

View 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
View 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
View File

@@ -5,6 +5,8 @@ npm-debug.log
coverage
dist/
src/i18n/transifex_input.json
temp/babel-plugin-react-intl
### pyenv ###
.python-version

View File

@@ -1,7 +1,6 @@
.eslintignore
.eslintrc.json
.gitignore
.travis.yml
docker-compose.yml
Dockerfile
Makefile

View File

@@ -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=

View File

@@ -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

6
Makefile Executable file → Normal file
View File

@@ -3,7 +3,7 @@ npm-install-%: ## install specified % npm package
git add package.json
transifex_resource = frontend-app-gradebook
transifex_langs = "ar,fr,es_419,zh_CN"
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
@@ -57,9 +57,9 @@ push_translations:
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --language=$(transifex_langs)
tx pull -t -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

View File

@@ -1,4 +1,4 @@
[![Build Status](https://api.travis-ci.com/edx/frontend-app-gradebook.svg?branch=master)](https://travis-ci.com/edx/frontend-app-gradebook) [![Coveralls](https://img.shields.io/coveralls/edx/frontend-app-gradebook.svg?branch=master)](https://coveralls.io/github/edx/frontend-app-gradebook)
[![Build Status](https://api.travis-ci.com/edx/frontend-app-gradebook.svg?branch=master)](https://travis-ci.com/edx/frontend-app-gradebook)
[![npm_version](https://img.shields.io/npm/v/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)
[![npm_downloads](https://img.shields.io/npm/dt/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)
[![license](https://img.shields.io/npm/l/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)

View File

@@ -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).
![travis-buddy](https://i.imgur.com/VsR2TTs.png)
## 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
View 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`.

View File

@@ -11,5 +11,6 @@ module.exports = createConfig('jest', {
coveragePathIgnorePatterns: [
'src/segment.js',
'src/postcss.config.js',
'testUtils', // don't unit test jest mocking tools
],
});

63042
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.47",
"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": "^20.20.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"
}
}

View File

@@ -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;

View File

@@ -9,6 +9,7 @@ $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/GradesView/GradesView";

View File

@@ -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', () => {

View File

@@ -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>
`;

View File

@@ -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 { bulkManagementColumns } from 'data/constants/app';
import selectors from 'data/selectors';
@@ -30,14 +30,13 @@ export const mapHistoryRows = ({
export const HistoryTable = ({
bulkManagementHistory,
}) => (
<>
<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: [],

View File

@@ -1,7 +1,7 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from 'enzyme';
import { Table } from '@edx/paragon';
import { DataTable } from '@edx/paragon';
import selectors from 'data/selectors';
import { bulkManagementColumns } from 'data/constants/app';
@@ -9,13 +9,12 @@ import { bulkManagementColumns } from 'data/constants/app';
import ResultsSummary from './ResultsSummary';
import { HistoryTable, mapStateToProps } from './HistoryTable';
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: {
@@ -62,7 +61,7 @@ describe('HistoryTable', () => {
describe('history table', () => {
let table;
beforeEach(() => {
table = el.find(Table);
table = el.find(DataTable);
});
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => {
const fieldAssertions = [

View File

@@ -42,78 +42,77 @@ Array [
`;
exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
<Fragment>
<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>
<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}
/>
`;

View File

@@ -17,6 +17,7 @@ exports[`PercentGroup Component snapshots basic snapshot 1`] = `
</FormLabel>
<ForwardRef
as="input"
autoResize={false}
disabled={false}
max={100}
min={0}
@@ -52,6 +53,7 @@ exports[`PercentGroup Component snapshots disabled 1`] = `
</FormLabel>
<ForwardRef
as="input"
autoResize={false}
disabled={true}
max={100}
min={0}

View File

@@ -17,6 +17,7 @@ exports[`SelectGroup Component snapshots basic snapshot 1`] = `
</FormLabel>
<ForwardRef
as="select"
autoResize={false}
disabled={false}
onChange={[MockFunction props.onChange]}
plaintext={false}
@@ -62,6 +63,7 @@ exports[`SelectGroup Component snapshots disabled 1`] = `
</FormLabel>
<ForwardRef
as="select"
autoResize={false}
disabled={true}
onChange={[MockFunction props.onChange]}
plaintext={false}

View File

@@ -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

View File

@@ -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'),
};
});

View File

@@ -2,10 +2,10 @@ 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';
@@ -25,7 +25,7 @@ export class GradebookHeader extends React.Component {
}
lmsInstructorDashboardUrl = courseId => (
`${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`
`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
);
handleToggleViewClick() {

View File

@@ -4,7 +4,7 @@ const messages = defineMessages({
downloadGradesBtn: {
id: 'gradebook.GradesView.BulkManagementControls.bulkManagementLabel',
defaultMessage: 'Download Grades',
description: 'Button text for bulk grades download control in GradesView',
description: 'A labeled button that allows an admin user to download course grades all at once (in bulk).',
},
});

View File

@@ -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>
);
}

View File

@@ -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.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.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.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.GradesView.EditModal.Overrides.adjustedGradeHeader"
/>,
"accessor": "adjustedGrade",
},
]
}
@@ -59,5 +59,6 @@ exports[`OverrideTable Component snapshots basic snapshot shows a row for each e
},
]
}
itemCount={2}
/>
`;

View File

@@ -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}
/>
);
};

View File

@@ -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');

View File

@@ -1,16 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
<Modal
body={
exports[`EditModal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
<ModalDialog
closeLabel="Close"
hasCloseButton={true}
isBlocking={false}
isFullscreenOnMobile={true}
isFullscreenScroll={false}
isOpen={true}
onClose={[MockFunction this.closeAssignmentModal]}
size="xl"
variant="default"
>
<ModalDialogHeader
as="div"
>
<ModalDialogTitle
as="h2"
>
<FormattedMessage
defaultMessage="Edit Grades"
description="Edit Modal title"
id="gradebook.GradesView.EditModal.title"
/>
</ModalDialogTitle>
</ModalDialogHeader>
<ModalDialogBody
as="div"
>
<div>
<ModalHeaders />
<StatusAlert
alertType="danger"
dialog="Weve been trying to contact you regarding..."
<ForwardRef
dismissible={false}
open={true}
/>
onClose={[Function]}
show={true}
stacked={false}
transition={
Object {
"$$typeof": Symbol(react.forward_ref),
"defaultProps": Object {
"appear": false,
"in": false,
"mountOnEnter": false,
"timeout": 300,
"unmountOnExit": false,
},
"displayName": "Fade",
"render": [Function],
}
}
variant="danger"
>
Weve been trying to contact you regarding...
</ForwardRef>
<OverrideTable />
<div>
<FormattedMessage
@@ -27,10 +69,48 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
/>
</div>
</div>
}
buttons={
Array [
<Button
</ModalDialogBody>
<ModalDialogFooter
as="div"
>
<ActionRow
as="div"
isStacked={false}
>
<ForwardRef
as={
Object {
"$$typeof": Symbol(react.forward_ref),
"Deprecated": [Function],
"defaultProps": Object {
"active": false,
"children": undefined,
"className": undefined,
"disabled": false,
"iconAfter": undefined,
"iconBefore": undefined,
"variant": "primary",
},
"propTypes": Object {
"children": [Function],
"className": [Function],
"iconAfter": [Function],
"iconBefore": [Function],
},
"render": [Function],
}
}
variant="tertiary"
>
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesView.EditModal.closeText"
/>
</ForwardRef>
<ForwardRef
active={false}
disabled={false}
onClick={[MockFunction this.handleAdjustedGradeClick]}
variant="primary"
>
@@ -39,39 +119,65 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
description="Edit Modal Save button label"
id="gradebook.GradesView.EditModal.saveGrade"
/>
</Button>,
]
}
closeText={
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesView.EditModal.closeText"
/>
}
onClose={[MockFunction this.closeAssignmentModal]}
open={true}
title={
<FormattedMessage
defaultMessage="Edit Grades"
description="Edit Modal title"
id="gradebook.GradesView.EditModal.title"
/>
}
/>
</ForwardRef>
</ActionRow>
</ModalDialogFooter>
</ModalDialog>
`;
exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
<Modal
body={
exports[`EditModal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
<ModalDialog
closeLabel="Close"
hasCloseButton={true}
isBlocking={false}
isFullscreenOnMobile={true}
isFullscreenScroll={false}
isOpen={true}
onClose={[MockFunction this.closeAssignmentModal]}
size="xl"
variant="default"
>
<ModalDialogHeader
as="div"
>
<ModalDialogTitle
as="h2"
>
<FormattedMessage
defaultMessage="Edit Grades"
description="Edit Modal title"
id="gradebook.GradesView.EditModal.title"
/>
</ModalDialogTitle>
</ModalDialogHeader>
<ModalDialogBody
as="div"
>
<div>
<ModalHeaders />
<StatusAlert
alertType="danger"
dialog=""
<ForwardRef
dismissible={false}
open={false}
/>
onClose={[Function]}
show={false}
stacked={false}
transition={
Object {
"$$typeof": Symbol(react.forward_ref),
"defaultProps": Object {
"appear": false,
"in": false,
"mountOnEnter": false,
"timeout": 300,
"unmountOnExit": false,
},
"displayName": "Fade",
"render": [Function],
}
}
variant="danger"
>
</ForwardRef>
<OverrideTable />
<div>
<FormattedMessage
@@ -88,10 +194,48 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
/>
</div>
</div>
}
buttons={
Array [
<Button
</ModalDialogBody>
<ModalDialogFooter
as="div"
>
<ActionRow
as="div"
isStacked={false}
>
<ForwardRef
as={
Object {
"$$typeof": Symbol(react.forward_ref),
"Deprecated": [Function],
"defaultProps": Object {
"active": false,
"children": undefined,
"className": undefined,
"disabled": false,
"iconAfter": undefined,
"iconBefore": undefined,
"variant": "primary",
},
"propTypes": Object {
"children": [Function],
"className": [Function],
"iconAfter": [Function],
"iconBefore": [Function],
},
"render": [Function],
}
}
variant="tertiary"
>
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesView.EditModal.closeText"
/>
</ForwardRef>
<ForwardRef
active={false}
disabled={false}
onClick={[MockFunction this.handleAdjustedGradeClick]}
variant="primary"
>
@@ -100,24 +244,8 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
description="Edit Modal Save button label"
id="gradebook.GradesView.EditModal.saveGrade"
/>
</Button>,
]
}
closeText={
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesView.EditModal.closeText"
/>
}
onClose={[MockFunction this.closeAssignmentModal]}
open={false}
title={
<FormattedMessage
defaultMessage="Edit Grades"
description="Edit Modal title"
id="gradebook.GradesView.EditModal.title"
/>
}
/>
</ForwardRef>
</ActionRow>
</ModalDialogFooter>
</ModalDialog>
`;

View File

@@ -5,8 +5,9 @@ import { connect } from 'react-redux';
import {
Button,
Modal,
StatusAlert,
Alert,
ModalDialog,
ActionRow,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
@@ -46,31 +47,45 @@ export class EditModal extends React.Component {
render() {
return (
<Modal
open={this.props.open}
title={<FormattedMessage {...messages.title} />}
closeText={<FormattedMessage {...messages.closeText} />}
body={(
<ModalDialog
isOpen
onClose={this.closeAssignmentModal}
size="xl"
hasCloseButton
isFullscreenOnMobile
>
<ModalDialog.Header>
<ModalDialog.Title>
<FormattedMessage {...messages.title} />
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.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>
</div>
)}
buttons={[
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
<FormattedMessage {...messages.saveGrade} />
</Button>,
]}
onClose={this.closeAssignmentModal}
/>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
<FormattedMessage {...messages.closeText} />
</ModalDialog.CloseButton>
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
<FormattedMessage {...messages.saveGrade} />
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
}
}

View File

@@ -9,15 +9,11 @@ import {
EditModal,
mapDispatchToProps,
mapStateToProps,
} from '.';
}
from '.';
jest.mock('./OverrideTable', () => 'OverrideTable');
jest.mock('./ModalHeaders', () => 'ModalHeaders');
jest.mock('@edx/paragon', () => ({
Button: () => 'Button',
Modal: () => 'Modal',
StatusAlert: () => 'StatusAlert',
}));
jest.mock('data/actions', () => ({
__esModule: true,
default: {
@@ -44,7 +40,7 @@ jest.mock('data/selectors', () => ({
},
},
}));
describe('EditMoal', () => {
describe('EditModal', () => {
let props;
beforeEach(() => {
props = {

View File

@@ -4,7 +4,7 @@ const messages = defineMessages({
editFilters: {
id: 'gradebook.GradesView.editFilterLabel',
defaultMessage: 'Edit Filters',
description: 'Button text on Grades tab to open/close the Filters tab',
description: 'A labeled button in the Grades tab that opens/closes the Filters tab, allowing the grades to be filtered',
},
});

View File

@@ -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} />

View File

@@ -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();
});
});

View File

@@ -80,3 +80,99 @@ exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
</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>
`;

View File

@@ -4,48 +4,58 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
<div
className="gradebook-container"
>
<div
className="gbook"
<DataTable
RowStatusComponent={[MockFunction this.nullMethod]}
columns={
Array [
Object {
"Header": <UsernameLabelReplacement />,
"accessor": "Username",
},
Object {
"Header": <FormattedMessage
defaultMessage="Email*"
description="Gradebook table email column header"
id="gradebook.GradesView.table.headings.email"
/>,
"accessor": "Email",
},
Object {
"Header": "field1",
"accessor": "field1",
},
Object {
"Header": "field2",
"accessor": "field2",
},
Object {
"Header": <TotalGradeLabelReplacement />,
"accessor": "Total Grade (%)",
},
]
}
data={
Array [
"mappedRow: 1",
"mappedRow: 2",
"mappedRow: 3",
]
}
hasFixedColumnWidths={true}
itemCount={3}
rowHeaderColumnKey="username"
>
<Table
columns={
Array [
Object {
"key": "Username",
"label": <UsernameLabelReplacement />,
},
Object {
"key": "Email",
"label": <FormattedMessage
defaultMessage="Email"
description="Gradebook table email column header"
id="gradebook.GradesView.table.headings.email"
/>,
},
Object {
"key": "field1",
"label": "field1",
},
Object {
"key": "field2",
"label": "field2",
},
Object {
"key": "Total Grade (%)",
"label": <TotalGradeLabelReplacement />,
},
]
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable
content={
<FormattedMessage
defaultMessage="No results found"
description="Gradebook table message when no learner results were found"
id="gradebook.GradesView.table.noResultsFound"
/>
}
data={
Array [
"mappedRow: 1",
"mappedRow: 2",
"mappedRow: 3",
]
}
hasFixedColumnWidths={true}
rowHeaderColumnKey="username"
/>
</div>
</DataTable>
</div>
`;

View File

@@ -3,8 +3,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Table } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { DataTable } from '@edx/paragon';
import { FormattedMessage, getLocale, isRtl } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import { Headings } from 'data/constants/grades';
@@ -27,6 +27,7 @@ export class GradebookTable extends React.Component {
super(props);
this.mapHeaders = this.mapHeaders.bind(this);
this.mapRows = this.mapRows.bind(this);
this.nullMethod = this.nullMethod.bind(this);
}
mapHeaders(heading) {
@@ -40,7 +41,7 @@ export class GradebookTable extends React.Component {
} else {
label = heading;
}
return { label, key: heading };
return { Header: label, accessor: heading };
}
mapRows(entry) {
@@ -49,7 +50,7 @@ export class GradebookTable extends React.Component {
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
),
[Headings.email]: (<Fields.Email email={entry.email} />),
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}%`,
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${isRtl(getLocale()) ? '\u200f' : ''}%`,
};
entry.section_breakdown.forEach(subsection => {
dataRow[subsection.label] = (
@@ -59,17 +60,25 @@ export class GradebookTable extends React.Component {
return dataRow;
}
nullMethod() {
return null;
}
render() {
return (
<div className="gradebook-container">
<div className="gbook">
<Table
columns={this.props.headings.map(this.mapHeaders)}
data={this.props.grades.map(this.mapRows)}
rowHeaderColumnKey="username"
hasFixedColumnWidths
/>
</div>
<DataTable
columns={this.props.headings.map(this.mapHeaders)}
data={this.props.grades.map(this.mapRows)}
rowHeaderColumnKey="username"
hasFixedColumnWidths
itemCount={this.props.grades.length}
RowStatusComponent={this.nullMethod}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content={<FormattedMessage {...messages.noResultsFound} />} />
</DataTable>
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
emailHeading: {
id: 'gradebook.GradesView.table.headings.email',
defaultMessage: 'Email',
defaultMessage: 'Email*',
description: 'Gradebook table email column header',
},
totalGradeHeading: {
@@ -31,6 +31,11 @@ const messages = defineMessages({
defaultMessage: 'Total Grade values are always displayed as a percentage',
description: 'Gradebook table message that total grades are displayed in percent format',
},
noResultsFound: {
id: 'gradebook.GradesView.table.noResultsFound',
defaultMessage: 'No results found',
description: 'Gradebook table message when no learner results were found',
},
});
export default messages;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Table } from '@edx/paragon';
import { DataTable } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
@@ -11,8 +11,12 @@ import Fields from './Fields';
import messages from './messages';
import { GradebookTable, mapStateToProps } from '.';
jest.mock('@edx/paragon', () => ({
Table: () => 'Table',
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
DataTable: {
Table: 'DataTable.Table',
TableControlBar: 'DataTable.TableControlBar',
EmptyTable: 'DataTable.EmptyTable',
},
}));
jest.mock('./Fields', () => ({
__esModule: true,
@@ -79,40 +83,45 @@ describe('GradebookTable', () => {
};
test('snapshot - fields1 and 2 between email and totalGrade, mocked rows', () => {
el = shallow(<GradebookTable {...props} />);
el.instance().nullMethod = jest.fn().mockName('this.nullMethod');
el.instance().mapRows = (entry) => `mappedRow: ${entry.percent}`;
expect(el.instance().render()).toMatchSnapshot();
});
test('null method returns null for stub component', () => {
el = shallow(<GradebookTable {...props} />);
expect(el.instance().nullMethod()).toEqual(null);
});
describe('table columns (mapHeaders)', () => {
let headings;
beforeEach(() => {
el = shallow(<GradebookTable {...props} />);
headings = el.find(Table).props().columns;
headings = el.find(DataTable).props().columns;
});
test('username sets key and replaces label with component', () => {
test('username sets key and replaces Header with component', () => {
const heading = headings[0];
expect(heading.key).toEqual(Headings.username);
expect(heading.label.type).toEqual(LabelReplacements.UsernameLabelReplacement);
expect(heading.accessor).toEqual(Headings.username);
expect(heading.Header.type).toEqual(LabelReplacements.UsernameLabelReplacement);
});
test('email sets key and label from header', () => {
test('email sets key and Header from header', () => {
const heading = headings[1];
expect(heading.key).toEqual(Headings.email);
expect(heading.label).toEqual(<FormattedMessage {...messages.emailHeading} />);
expect(heading.accessor).toEqual(Headings.email);
expect(heading.Header).toEqual(<FormattedMessage {...messages.emailHeading} />);
});
test('subsections set key and label from header', () => {
expect(headings[2]).toEqual({ key: fields.field1, label: fields.field1 });
expect(headings[3]).toEqual({ key: fields.field2, label: fields.field2 });
test('subsections set key and Header from header', () => {
expect(headings[2]).toEqual({ accessor: fields.field1, Header: fields.field1 });
expect(headings[3]).toEqual({ accessor: fields.field2, Header: fields.field2 });
});
test('totalGrade sets key and replaces label with component', () => {
test('totalGrade sets key and replaces Header with component', () => {
const heading = headings[4];
expect(heading.key).toEqual(Headings.totalGrade);
expect(heading.label.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
expect(heading.accessor).toEqual(Headings.totalGrade);
expect(heading.Header.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
});
});
describe('table data (mapRows)', () => {
let rows;
beforeEach(() => {
el = shallow(<GradebookTable {...props} />);
rows = el.find(Table).props().data;
rows = el.find(DataTable).props().data;
});
describe.each([0, 1, 2])('gradeEntry($percent)', (gradeIndex) => {
let row;

View File

@@ -31,7 +31,7 @@
.import-grades-btn {
margin-left: 20px;
}
.intervention-report-description: {
.intervention-report-description {
margin-right: 40px;
}
h4.step-message-1 {
@@ -67,104 +67,9 @@
overflow-x: auto;
height: 600px;
overflow-y: auto;
word-break: break-word;
position: relative;
}
.gbook {
width: 100%;
.grade-button {
text-decoration: underline;
}
.student-key {
font-size: 14px;
}
#courseGradeTooltipIcon {
float: right;
}
.table thead tr {
min-height: 60px;
&:nth-child(1) {
position: sticky;
top: 0;
z-index: 10;
background-color: white;
th {
background-color: white;
border-bottom: 1px solid $gray_200;
}
}
}
thead, tbody, tr, td, th {
display: block;
word-break: break-word;
}
.table tr th:first-child {
border-bottom: none;
}
.table tr th:first-child,
.table tr td:first-child {
position: sticky;
left: 0;
z-index: 1; // to float over the following children in the side-scrolling case
background: white;
}
.table tr {
th:nth-child(1),
td:nth-child(1),
th:nth-child(2),
td:nth-child(2) {
width: 240px;
}
th:nth-last-of-type(1) {
width: 150px;
}
th, td {
width: 120px;
}
}
.table tbody th {
font-weight: normal;
}
.table {
overflow-x: hidden;
height: 100%;
tbody {
overflow-y: auto;
display: block;
}
thead, tbody tr {
display: table;
table-layout: fixed;
}
th {
vertical-align: top;
font-size: 14px;
}
}
.link-style {
color: #0075b4;
&:hover, &:focus {
color: #004368;
text-decoration: underline;
}
}
}
.form-group, .pgn__form-group {
label {
font-weight: bold;
@@ -207,3 +112,13 @@
select#ScoreView.form-control {
padding-right: 26px;
}
[dir=rtl] #course-grade-tooltip .arrow {
right: initial;
left: 0;
&:before {
border-width: 0.4rem 0.4rem 0.4rem 0;
border-right-color: $black;
}
}

View File

@@ -4,12 +4,12 @@ const messages = defineMessages({
csvUploadLabel: {
id: 'gradebook.BulkManagementHistoryView.csvUploadLabel',
defaultMessage: 'Upload Grade CSV',
description: 'Button in BulkManagementHistoryView Alerts',
description: 'A labeled button to upload a CSV containing course grades.',
},
importGradesBtnText: {
id: 'gradebook.GradesView.importGradesBtnText',
defaultMessage: 'Import Grades',
description: 'Button in BulkManagement Tab File Upload Form',
description: 'A labeled button to import grades in the BulkManagement Tab File Upload Form',
},
});

View File

@@ -4,12 +4,12 @@ const messages = defineMessages({
description: {
id: 'gradebook.GradesView.ImportSuccessToast.description',
defaultMessage: 'Import Successful! Grades will be updated momentarily.',
description: 'Import Success Toast description',
description: 'A message congratulating a successful Import of grades',
},
showHistoryViewBtn: {
id: 'gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn',
defaultMessage: 'View Activity Log',
description: 'Button text for action that loads Bulk Management Activity Log view',
description: 'The text on a button that loads a view of the Bulk Management Activity Log',
},
});

View File

@@ -4,17 +4,17 @@ const messages = defineMessages({
title: {
id: 'gradebook.GradesView.InterventionsReport.title',
defaultMessage: 'Interventions Report',
description: 'Intervention report subsection label',
description: 'The title for the Intervention report subsection',
},
description: {
id: 'gradebook.GradesView.InterventionsReport.description',
defaultMessage: 'Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.',
description: 'Intervention report subsection description',
description: 'The description for the Intervention report subsection',
},
downloadBtn: {
id: 'gradebook.GradesView.InterventionsReport.downloadBtn',
defaultMessage: 'Download Interventions',
description: 'Button text for intervention report download control in GradesView',
description: 'The labeled button to download the Intervention report from the Grades View',
},
});

View File

@@ -4,17 +4,17 @@ const messages = defineMessages({
scoreView: {
id: 'gradebook.GradesView.scoreViewLabel',
defaultMessage: 'Score View',
description: 'Score format select dropdown label',
description: 'The label for the dropdown list that allows a user to select the Score format',
},
absolute: {
id: 'gradebook.GradesView.absoluteOption',
defaultMessage: 'Absolute',
description: 'Score format select dropdown option',
description: 'A label within the Score Format dropdown list for the Absolute Grade Score option',
},
percent: {
id: 'gradebook.GradesView.percentOption',
defaultMessage: 'Percent',
description: 'Score format select dropdown option',
description: 'A label within the Score Format dropdown list for the Percent Grade Score option',
},
});

View File

@@ -18,13 +18,14 @@ import messages from './SearchControls.messages';
export class SearchControls extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onClear = this.onClear.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
/** Changing the search value stores the key in Gradebook. Currently unused */
onChange(searchValue) {
this.props.setSearchValue(searchValue);
onBlur(e) {
this.props.setSearchValue(e.target.value);
}
onClear() {
@@ -32,13 +33,18 @@ export class SearchControls extends React.Component {
this.props.fetchGrades();
}
onSubmit(searchValue) {
this.props.setSearchValue(searchValue);
this.props.fetchGrades();
}
render() {
return (
<div>
<SearchField
onSubmit={this.props.fetchGrades}
onSubmit={this.onSubmit}
inputLabel={<FormattedMessage {...messages.label} />}
onChange={this.onChange}
onBlur={this.onBlur}
onClear={this.onClear}
value={this.props.searchValue}
/>

View File

@@ -4,12 +4,12 @@ const messages = defineMessages({
label: {
id: 'gradebook.GradesView.search.label',
defaultMessage: 'Search for a learner',
description: 'Search description label',
description: 'Text prompting a user to use this functionality to search for a learner',
},
hint: {
id: 'gradebook.GradesView.search.hint',
defaultMessage: 'Search by username, email, or student key',
description: 'Search hint label',
description: 'A hint explaining the ways a user can search',
},
});

View File

@@ -4,7 +4,11 @@ import { shallow } from 'enzyme';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import { mapDispatchToProps, mapStateToProps, SearchControls } from './SearchControls';
import {
mapDispatchToProps,
mapStateToProps,
SearchControls,
} from './SearchControls';
jest.mock('@edx/paragon', () => ({
Icon: 'Icon',
@@ -15,7 +19,7 @@ jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
searchValue: jest.fn(state => ({ searchValue: state })),
searchValue: jest.fn((state) => ({ searchValue: state })),
},
},
}));
@@ -52,26 +56,45 @@ describe('SearchControls', () => {
describe('Snapshots', () => {
test('basic snapshot', () => {
const wrapper = searchControls();
wrapper.instance().onChange = jest.fn().mockName('onChange');
wrapper.instance().onBlur = jest.fn().mockName('onBlur');
wrapper.instance().onClear = jest.fn().mockName('onClear');
wrapper.instance().onSubmit = jest.fn().mockName('onSubmit');
expect(wrapper.instance().render()).toMatchSnapshot();
});
});
describe('onChange', () => {
it('saves the changed search value to Gradebook state', () => {
const wrapper = searchControls();
wrapper.instance().onChange('bob');
expect(props.setSearchValue).toHaveBeenCalledWith('bob');
describe('Behavior', () => {
describe('onBlur', () => {
it('saves the search value to Gradebook state but do not fetch grade', () => {
const wrapper = searchControls();
const event = {
target: {
value: 'bob',
},
};
wrapper.instance().onBlur(event);
expect(props.setSearchValue).toHaveBeenCalledWith('bob');
expect(props.fetchGrades).not.toHaveBeenCalled();
});
});
});
describe('onChange', () => {
it('sets search value to empty string and calls fetchGrades', () => {
const wrapper = searchControls();
wrapper.instance().onClear();
expect(props.setSearchValue).toHaveBeenCalledWith('');
expect(props.fetchGrades).toHaveBeenCalled();
describe('onClear', () => {
it('sets search value to empty string and calls fetchGrades', () => {
const wrapper = searchControls();
wrapper.instance().onClear();
expect(props.setSearchValue).toHaveBeenCalledWith('');
expect(props.fetchGrades).toHaveBeenCalled();
});
});
describe('onSubmit', () => {
it('sets search value to input and calls fetchGrades', () => {
const wrapper = searchControls();
wrapper.instance().onSubmit('John');
expect(props.setSearchValue).toHaveBeenCalledWith('John');
expect(props.fetchGrades).toHaveBeenCalled();
});
});
});

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StatusAlert } from '@edx/paragon';
import { Alert } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
@@ -40,18 +40,20 @@ export class StatusAlerts extends React.Component {
render() {
return (
<>
<StatusAlert
alertType="success"
dialog={<FormattedMessage {...messages.editSuccessAlert} />}
<Alert
variant="success"
onClose={this.props.handleCloseSuccessBanner}
open={this.props.showSuccessBanner}
/>
<StatusAlert
alertType="danger"
dialog={this.courseGradeFilterAlertDialogText}
show={this.props.showSuccessBanner}
>
<FormattedMessage {...messages.editSuccessAlert} />
</Alert>
<Alert
variant="danger"
dismissible={false}
open={this.isCourseGradeFilterAlertOpen}
/>
show={this.isCourseGradeFilterAlertOpen}
>
{this.courseGradeFilterAlertDialogText}
</Alert>
</>
);
}

View File

@@ -4,17 +4,17 @@ const messages = defineMessages({
editSuccessAlert: {
id: 'gradebook.GradesView.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',
description: 'An alert text for successfully editing a grade',
},
maxGradeInvalid: {
id: 'gradebook.GradesView.maxCourseGradeInvalid',
defaultMessage: 'Maximum course grade must be between 0 and 100',
description: 'Alert text for invalid maximum course grade',
description: 'An alert text for selecting a maximum course grade greater than 100',
},
minGradeInvalid: {
id: 'gradebook.GradesView.minCourseGradeInvalid',
defaultMessage: 'Minimum course grade must be between 0 and 100',
description: 'Alert text for invalid minimum course grade',
description: 'An alert text for selecting a minimum course grade less than 0',
},
});

View File

@@ -13,7 +13,7 @@ import {
} from './StatusAlerts';
jest.mock('@edx/paragon', () => ({
StatusAlert: 'StatusAlert',
Alert: 'Alert',
}));
jest.mock('data/selectors', () => ({
__esModule: true,

View File

@@ -12,7 +12,7 @@ exports[`FilterMenuToggle component snapshots basic snapshot 1`] = `
<FormattedMessage
defaultMessage="Edit Filters"
description="Button text on Grades tab to open/close the Filters tab"
description="A labeled button in the Grades tab that opens/closes the Filters tab, allowing the grades to be filtered"
id="gradebook.GradesView.editFilterLabel"
/>
</Button>

View File

@@ -15,11 +15,12 @@ exports[`ImportGradesButton component snapshot snapshot - loads export form w/ a
>
<ForwardRef
as="input"
autoResize={false}
className="d-none"
label={
<FormattedMessage
defaultMessage="Upload Grade CSV"
description="Button in BulkManagementHistoryView Alerts"
description="A labeled button to upload a CSV containing course grades."
id="gradebook.BulkManagementHistoryView.csvUploadLabel"
/>
}
@@ -35,7 +36,7 @@ exports[`ImportGradesButton component snapshot snapshot - loads export form w/ a
label={
Object {
"defaultMessage": "Import Grades",
"description": "Button in BulkManagement Tab File Upload Form",
"description": "A labeled button to import grades in the BulkManagement Tab File Upload Form",
"id": "gradebook.GradesView.importGradesBtnText",
}
}

View File

@@ -7,7 +7,7 @@ exports[`InterventionsReport component snapshots snapshot 1`] = `
>
<FormattedMessage
defaultMessage="Interventions Report"
description="Intervention report subsection label"
description="The title for the Intervention report subsection"
id="gradebook.GradesView.InterventionsReport.title"
/>
</h4>
@@ -19,7 +19,7 @@ exports[`InterventionsReport component snapshots snapshot 1`] = `
>
<FormattedMessage
defaultMessage="Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits."
description="Intervention report subsection description"
description="The description for the Intervention report subsection"
id="gradebook.GradesView.InterventionsReport.description"
/>
</div>
@@ -27,7 +27,7 @@ exports[`InterventionsReport component snapshots snapshot 1`] = `
label={
Object {
"defaultMessage": "Download Interventions",
"description": "Button text for intervention report download control in GradesView",
"description": "The labeled button to download the Intervention report from the Grades View",
"id": "gradebook.GradesView.InterventionsReport.downloadBtn",
}
}

View File

@@ -7,7 +7,7 @@ exports[`ScoreViewInput component snapshot - select box with percent and absolut
<FormLabel>
<FormattedMessage
defaultMessage="Score View"
description="Score format select dropdown label"
description="The label for the dropdown list that allows a user to select the Score format"
id="gradebook.GradesView.scoreViewLabel"
/>
:

View File

@@ -6,13 +6,13 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = `
inputLabel={
<FormattedMessage
defaultMessage="Search for a learner"
description="Search description label"
description="Text prompting a user to use this functionality to search for a learner"
id="gradebook.GradesView.search.label"
/>
}
onChange={[MockFunction onChange]}
onBlur={[MockFunction onBlur]}
onClear={[MockFunction onClear]}
onSubmit={[MockFunction fetchGrades]}
onSubmit={[MockFunction onSubmit]}
value="alice"
/>
<small
@@ -20,7 +20,7 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = `
>
<FormattedMessage
defaultMessage="Search by username, email, or student key"
description="Search hint label"
description="A hint explaining the ways a user can search"
id="gradebook.GradesView.search.hint"
/>
</small>

View File

@@ -2,23 +2,23 @@
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.GradesView.editSuccessAlert"
/>
}
<Alert
onClose={[MockFunction handleCloseSuccessBanner]}
open={true}
/>
<StatusAlert
alertType="danger"
dialog="the quiCk brown does somEthing or other"
show={true}
variant="success"
>
<FormattedMessage
defaultMessage="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
description="An alert text for successfully editing a grade"
id="gradebook.GradesView.editSuccessAlert"
/>
</Alert>
<Alert
dismissible={false}
open={false}
/>
show={false}
variant="danger"
>
the quiCk brown does somEthing or other
</Alert>
</React.Fragment>
`;

View File

@@ -2,7 +2,7 @@
exports[`WithSidebar Component snapshots basic snapshot 1`] = `
<div
className="d-flex sidebar-container"
className="d-flex sidebar-container page-gradebook"
>
<aside
className="sidebar-class-names"

View File

@@ -32,7 +32,7 @@ export class WithSidebar extends React.Component {
render() {
return (
<div className="d-flex sidebar-container">
<div className="d-flex sidebar-container page-gradebook">
<aside className={this.sidebarClassNames} onTransitionEnd={this.props.handleSlideDone}>
{ this.props.sidebar }
</aside>

View File

@@ -1,16 +0,0 @@
const configuration = {
BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
SECURE_COOKIES: process.env.NODE_ENV !== 'development',
SEGMENT_KEY: process.env.SEGMENT_KEY,
ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
};
const features = {};
export { configuration, features };

View File

@@ -32,26 +32,26 @@ export const localFilterKeys = StrictDict({
*/
export const bulkManagementColumns = [
{
key: 'filename',
label: 'Gradebook',
accessor: 'filename',
Header: 'Gradebook',
columnSortable: false,
width: 'col-5',
},
{
key: 'resultsSummary',
label: 'Download Summary',
accessor: 'resultsSummary',
Header: 'Download Summary',
columnSortable: false,
width: 'col',
},
{
key: 'user',
label: 'Who',
accessor: 'user',
Header: 'Who',
columnSortable: false,
width: 'col-1',
},
{
key: 'timeUploaded',
label: 'When',
accessor: 'timeUploaded',
Header: 'When',
columnSortable: false,
width: 'col',
},

View File

@@ -4,37 +4,37 @@ const messages = defineMessages({
assignment: {
id: 'gradebook.GradesTab.FilterBadges.assignment',
defaultMessage: 'Assignment',
description: 'Assignment FilterBadge label',
description: 'A label describing the notification under the "Edit Filters" button that shows by which of the course\'s assignments the view is being filtered.',
},
assignmentGrade: {
id: 'gradebook.GradesTab.FilterBadges.assignmentGrade',
defaultMessage: 'Assignment Grade',
description: 'Assignment Grade FilterBadge label',
description: 'A label describing the notification under the "Edit Filters" button that shows that the view is being filtered to include assignment grades within the alloted range.',
},
assignmentType: {
id: 'gradebook.GradesTab.FilterBadges.assignmentType',
defaultMessage: 'Assignment Type',
description: 'Assignment Type FilterBadge label',
description: 'A label describing the notification under the "Edit Filters" button that shows by which of the course\'s assignment types the view is being filtered.',
},
cohort: {
id: 'gradebook.GradesTab.FilterBadges.cohort',
defaultMessage: 'Cohort',
description: 'Cohort FilterBadge label',
description: 'A label describing the notification under the "Edit Filters" button that shows by which of the course\'s cohorts the view is being filtered.',
},
courseGrade: {
id: 'gradebook.GradesTab.FilterBadges.courseGrade',
defaultMessage: 'Course Grade',
description: 'Course Grade FilterBadge label',
description: 'A label describing the notification under the "Edit Filters" button that shows that the view is being filtered to include course grades within the alloted range.',
},
includeCourseRoleMembers: {
id: 'gradebook.GradesTab.FilterBadges.includeCourseRoleMembers',
defaultMessage: 'Include Course Team Members',
description: 'Include Course Team Members FilterBadge label',
description: 'A label describing the notification under the "Edit Filters" button that shows that the view is being filtered to include course team members.',
},
track: {
id: 'gradebook.GradesTab.FilterBadges.track',
defaultMessage: 'Track',
description: 'Track FilterBadge label',
description: 'A label describing the notification under the "Edit Filters" button that shows by which of the course\'s tracks the view is being filtered.',
},
});

View File

@@ -131,7 +131,7 @@ describe('app reducer', () => {
const mockDate = new Date(8675309);
let dateSpy;
beforeEach(() => {
dateSpy = jest.spyOn(global, 'Date').mockReturnValue(mockDate);
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
});
afterEach(() => {
dateSpy.mockRestore();

View File

@@ -3,6 +3,7 @@ import { StrictDict } from 'utils';
import { Headings, GradeFormats } from 'data/constants/grades';
import { formatDateForDisplay } from 'data/actions/utils';
import { getLocale, isRtl } from '@edx/frontend-platform/i18n';
import simpleSelectorFactory from '../utils';
import * as module from './grades';
@@ -156,7 +157,7 @@ export const subsectionGrade = StrictDict({
[GradeFormats.absolute]: (subsection) => {
const earned = module.roundGrade(subsection.score_earned);
const possible = module.roundGrade(subsection.score_possible);
return subsection.attempted ? `${earned}/${possible}` : `${earned}`;
return subsection.attempted ? `${earned}${isRtl(getLocale()) ? '\\' : '/'}${possible}` : `${earned}`;
},
/**
* subsectionGrade.percent(subsection)

View File

@@ -14,10 +14,10 @@ const { get, post, stringifyUrl } = utils;
/*********************************************************************************
* GET Actions
*********************************************************************************/
const assignmentTypes = () => get(urls.assignmentTypes);
const cohorts = () => get(urls.cohorts);
const roles = () => get(urls.roles);
const tracks = () => get(urls.tracks);
const assignmentTypes = () => get(urls.getAssignmentTypesUrl());
const cohorts = () => get(urls.getCohortsUrl());
const roles = () => get(urls.getRolesUrl());
const tracks = () => get(urls.getTracksUrl());
/**
* fetch.gradebookData(searchText, cohort, track, options)
@@ -45,7 +45,7 @@ const gradebookData = (searchText, cohort, track, options = {}) => {
[paramKeys.assignmentGradeMax]: options.assignmentGradeMax,
[paramKeys.assignmentGradeMin]: options.assignmentGradeMin,
};
return get(stringifyUrl(urls.gradebook, queryParams));
return get(stringifyUrl(urls.getGradebookUrl(), queryParams));
};
/**
@@ -53,7 +53,7 @@ const gradebookData = (searchText, cohort, track, options = {}) => {
* fetches bulk operation history and raises an error if the operation fails
* @return {Promise} - get response
*/
const gradeBulkOperationHistory = () => get(urls.bulkHistory)
const gradeBulkOperationHistory = () => get(urls.getBulkHistoryUrl())
.then(response => response.data)
.catch(() => Promise.reject(Error(messages.errors.unhandledResponse)));
@@ -87,7 +87,7 @@ const gradeOverrideHistory = (subsectionId, userId) => (
* }
* @return {Promise} - post response
*/
const updateGradebookData = (updateData) => post(urls.bulkUpdate, updateData);
const updateGradebookData = (updateData) => post(urls.getBulkUpdateUrl(), updateData);
/**
* uploadGradeCsv(formData)

View File

@@ -35,28 +35,28 @@ describe('lms service api', () => {
describe('fetch.assignmentTypes', () => {
testSimpleFetch(
api.fetch.assignmentTypes,
urls.assignmentTypes,
urls.getAssignmentTypesUrl(),
'fetches from urls.assignmentTypes',
);
});
describe('fetch.cohorts', () => {
testSimpleFetch(
api.fetch.cohorts,
urls.cohorts,
urls.getCohortsUrl(),
'fetches from urls.cohorts',
);
});
describe('fetch.roles', () => {
testSimpleFetch(
api.fetch.roles,
urls.roles,
urls.getRolesUrl(),
'fetches from urls.roles',
);
});
describe('fetch.tracks', () => {
testSimpleFetch(
api.fetch.tracks,
urls.tracks,
urls.getTracksUrl(),
'fetches from urls.tracks',
);
});
@@ -98,7 +98,7 @@ describe('lms service api', () => {
});
test('loads only passed values if options is empty', () => (
api.fetch.gradebookData(searchText, cohort, track).then(({ data }) => {
expect(data).toEqual(utils.stringifyUrl(urls.gradebook, {
expect(data).toEqual(utils.stringifyUrl(urls.getGradebookUrl(), {
[paramKeys.pageSize]: pageSize,
[paramKeys.userContains]: searchText,
[paramKeys.cohortId]: cohort,
@@ -114,7 +114,7 @@ describe('lms service api', () => {
));
test('loads ["all"] for excludedCorseRoles if not includeCourseRoles', () => (
api.fetch.gradebookData(searchText, cohort, track, options).then(({ data }) => {
expect(data).toEqual(utils.stringifyUrl(urls.gradebook, {
expect(data).toEqual(utils.stringifyUrl(urls.getGradebookUrl(), {
[paramKeys.pageSize]: pageSize,
[paramKeys.userContains]: searchText,
[paramKeys.cohortId]: cohort,
@@ -130,7 +130,7 @@ describe('lms service api', () => {
));
test('loads null for excludedCorseRoles if includeCourseRoles', () => (
api.fetch.gradebookData(searchText, cohort, track, options).then(({ data }) => {
expect(data).toEqual(utils.stringifyUrl(urls.gradebook, {
expect(data).toEqual(utils.stringifyUrl(urls.getGradebookUrl(), {
[paramKeys.pageSize]: pageSize,
[paramKeys.userContains]: searchText,
[paramKeys.cohortId]: cohort,
@@ -153,7 +153,7 @@ describe('lms service api', () => {
});
it('fetches from urls.bulkHistory and returns the data', () => (
api.fetch.gradeBulkOperationHistory().then(url => {
expect(url).toEqual(urls.bulkHistory);
expect(url).toEqual(urls.getBulkHistoryUrl());
})
));
});
@@ -195,7 +195,7 @@ describe('lms service api', () => {
});
test('posts to urls.bulkUpdate with passed data', () => (
api.updateGradebookData(updateData).then(({ data }) => {
expect(data).toEqual({ url: urls.bulkUpdate, data: updateData });
expect(data).toEqual({ url: urls.getBulkUpdateUrl(), data: updateData });
})
));
});

View File

@@ -1,59 +1,54 @@
import { getConfig } from '@edx/frontend-platform';
import { StrictDict } from 'utils';
import { configuration } from 'config';
import { historyRecordLimit } from './constants';
import { filterQuery, stringifyUrl } from './utils';
const baseUrl = `${configuration.LMS_BASE_URL}`;
const courseId = window.location.pathname.slice(1);
const api = `${baseUrl}/api/`;
const bulkGrades = `${api}bulk_grades/course/${courseId}/`;
const enrollment = `${api}enrollment/v1/`;
const grades = `${api}grades/v1/`;
const gradebook = `${grades}gradebook/${courseId}/`;
const bulkUpdate = `${gradebook}bulk-update`;
const intervention = `${bulkGrades}intervention/`;
const cohorts = `${baseUrl}/courses/${courseId}/cohorts/`;
const tracks = `${enrollment}course/${courseId}?include_expired=1`;
const bulkHistory = `${bulkGrades}history/`;
const assignmentTypes = stringifyUrl(`${gradebook}grading-info`, { graded_only: true });
const roles = stringifyUrl(`${enrollment}roles/`, { courseId });
const courseId = window.location.pathname.split('/').filter(Boolean).pop() || '';
export const getUrlPrefix = () => `${getConfig().LMS_BASE_URL}/api/`;
export const getBulkGradesUrl = () => `${getUrlPrefix()}bulk_grades/course/${courseId}/`;
export const getEnrollmentUrl = () => `${getUrlPrefix()}enrollment/v1/`;
export const getGradesUrl = () => `${getUrlPrefix()}grades/v1/`;
export const getGradebookUrl = () => `${getGradesUrl()}gradebook/${courseId}/`;
export const getBulkUpdateUrl = () => `${getGradebookUrl()}bulk-update`;
export const getInterventionUrl = () => `${getBulkGradesUrl()}intervention/`;
export const getCohortsUrl = () => `${getUrlPrefix()}courses/${courseId}/cohorts/`;
export const getTracksUrl = () => `${getEnrollmentUrl()}course/${courseId}?include_expired=1`;
export const getBulkHistoryUrl = () => `${getBulkUpdateUrl()}history/`;
export const getAssignmentTypesUrl = () => stringifyUrl(`${getGradebookUrl()}grading-info`, { graded_only: true });
export const getRolesUrl = () => stringifyUrl(`${getEnrollmentUrl()}roles/`, { courseId });
/**
* bulkGradesUrlByCourseAndRow(courseId, rowId)
* returns the bulkGrades url with the given rowId.
* @param {string} rowId - row/error identifier
* @return {string} - bulk grades fetch url
*/
export const bulkGradesUrlByRow = (rowId) => stringifyUrl(bulkGrades, { error_id: rowId });
export const bulkGradesUrlByRow = (rowId) => stringifyUrl(getBulkGradesUrl(), { error_id: rowId });
export const gradeCsvUrl = (options = {}) => stringifyUrl(bulkGrades, filterQuery(options));
export const gradeCsvUrl = (options = {}) => stringifyUrl(getBulkGradesUrl(), filterQuery(options));
export const interventionExportCsvUrl = (options = {}) => (
stringifyUrl(intervention, filterQuery(options))
stringifyUrl(getInterventionUrl(), filterQuery(options))
);
export const sectionOverrideHistoryUrl = (subsectionId, userId) => stringifyUrl(
`${grades}subsection/${subsectionId}/`,
`${getGradesUrl()}subsection/${subsectionId}/`,
{ user_id: userId, history_record_limit: historyRecordLimit },
);
export default StrictDict({
assignmentTypes,
bulkGrades,
bulkHistory,
bulkUpdate,
cohorts,
enrollment,
grades,
gradebook,
intervention,
roles,
tracks,
getUrlPrefix,
getBulkGradesUrl,
getEnrollmentUrl,
getGradesUrl,
getGradebookUrl,
getBulkUpdateUrl,
getInterventionUrl,
getCohortsUrl,
getTracksUrl,
getBulkHistoryUrl,
getAssignmentTypesUrl,
getRolesUrl,
bulkGradesUrlByRow,
gradeCsvUrl,
interventionExportCsvUrl,

View File

@@ -17,7 +17,7 @@ describe('lms api url methods', () => {
it('returns bulkGrades url with error_id', () => {
const id = 'heyo';
expect(bulkGradesUrlByRow(id)).toEqual(
utils.stringifyUrl(urls.bulkGrades, { error_id: id }),
utils.stringifyUrl(urls.getBulkGradesUrl(), { error_id: id }),
);
});
});
@@ -25,12 +25,12 @@ describe('lms api url methods', () => {
it('returns bulkGrades with filterQuery-loaded options as query', () => {
const options = { some: 'fun', query: 'options' };
expect(gradeCsvUrl(options)).toEqual(
utils.stringifyUrl(urls.bulkGrades, utils.filterQuery(options)),
utils.stringifyUrl(urls.getBulkGradesUrl(), utils.filterQuery(options)),
);
});
it('defaults options to empty object', () => {
expect(gradeCsvUrl()).toEqual(
utils.stringifyUrl(urls.bulkGrades, utils.filterQuery({})),
utils.stringifyUrl(urls.getBulkGradesUrl(), utils.filterQuery({})),
);
});
});
@@ -38,12 +38,12 @@ describe('lms api url methods', () => {
it('returns intervention url with filterQuery-loaded options as query', () => {
const options = { some: 'fun', query: 'options' };
expect(interventionExportCsvUrl(options)).toEqual(
utils.stringifyUrl(urls.intervention, utils.filterQuery(options)),
utils.stringifyUrl(urls.getInterventionUrl(), utils.filterQuery(options)),
);
});
it('defaults options to empty object', () => {
expect(interventionExportCsvUrl()).toEqual(
utils.stringifyUrl(urls.intervention, utils.filterQuery({})),
utils.stringifyUrl(urls.getInterventionUrl(), utils.filterQuery({})),
);
});
});
@@ -53,7 +53,7 @@ describe('lms api url methods', () => {
const userId = 'Tom';
expect(sectionOverrideHistoryUrl(subsectionId, userId)).toEqual(
utils.stringifyUrl(
`${urls.grades}subsection/${subsectionId}/`,
`${urls.getGradesUrl()}subsection/${subsectionId}/`,
{ user_id: userId, history_record_limit: historyRecordLimit },
),
);

View File

@@ -4,19 +4,19 @@ import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProductio
import { createLogger } from 'redux-logger';
import { createMiddleware } from 'redux-beacon';
import Segment from '@redux-beacon/segment';
import { getConfig } from '@edx/frontend-platform';
import actions from './actions';
import selectors from './selectors';
import reducers from './reducers';
import eventsMap from './services/segment/mapping';
import { configuration } from '../config';
export const createStore = () => {
const loggerMiddleware = createLogger();
const middleware = [thunkMiddleware, loggerMiddleware];
// Conditionally add the segmentMiddleware only if the SEGMENT_KEY environment variable exists.
if (configuration.SEGMENT_KEY) {
if (getConfig().SEGMENT_KEY) {
middleware.push(createMiddleware(eventsMap, Segment()));
}
const store = redux.createStore(

View File

@@ -4,12 +4,12 @@ import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProductio
import { createLogger } from 'redux-logger';
import { createMiddleware } from 'redux-beacon';
import Segment from '@redux-beacon/segment';
import { getConfig } from '@edx/frontend-platform';
import actions from './actions';
import selectors from './selectors';
import reducers from './reducers';
import eventsMap from './services/segment/mapping';
import { configuration } from '../config';
import exportedStore, { createStore } from './store';
@@ -22,10 +22,10 @@ jest.mock('redux-logger', () => ({
createLogger: () => 'logger',
}));
jest.mock('redux-thunk', () => 'thunkMiddleware');
jest.mock('../config', () => ({
configuration: {
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({
SEGMENT_KEY: 'a-fake-segment-key',
},
})),
}));
jest.mock('redux-beacon', () => ({
createMiddleware: jest.fn((map, model) => ({ map, model })),
@@ -60,9 +60,9 @@ describe('store aggregator module', () => {
});
});
describe('if no SEGMENT_KEY', () => {
const key = configuration.SEGMENT_KEY;
const key = getConfig().SEGMENT_KEY;
beforeEach(() => {
configuration.SEGMENT_KEY = false;
getConfig.mockImplementation(() => ({ SEGMENT_KEY: false }));
});
it('exports thunk and logger middleware, composed and applied with dev tools', () => {
expect(createStore().middleware).toEqual(
@@ -70,7 +70,7 @@ describe('store aggregator module', () => {
);
});
afterEach(() => {
configuration.SEGMENT_KEY = key;
getConfig.mockImplementation(() => ({ SEGMENT_KEY: key }));
});
});
});

21
src/head/Head.jsx Normal file
View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const Head = ({ intl }) => (
<Helmet>
<title>
{intl.formatMessage(messages['gradebook.page.title'], { siteName: getConfig().SITE_NAME })}
</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
);
Head.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(Head);

17
src/head/Head.test.jsx Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { mount } from 'enzyme';
import { getConfig } from '@edx/frontend-platform';
import Head from './Head';
describe('Head', () => {
const props = {};
it('should match render title tag and favicon with the site configuration values', () => {
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
const helmet = Helmet.peek();
expect(helmet.title).toEqual(`Gradebook | ${getConfig().SITE_NAME}`);
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL);
});
});

11
src/head/messages.js Normal file
View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'gradebook.page.title': {
id: 'gradebook.page.title',
defaultMessage: 'Gradebook | {siteName}',
description: 'Title tag',
},
});
export default messages;

View File

@@ -1,14 +1,28 @@
import arMessages from './messages/ar.json';
// no need to import en messages-- they are in the defaultMessage field
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import es419Messages from './messages/es_419.json';
import zhcnMessages from './messages/zh_CN.json';
import ptMessages from './messages/pt.json';
import itMessages from './messages/it.json';
import ukMessages from './messages/uk.json';
import deMessages from './messages/de.json';
import ruMessages from './messages/ru.json';
import hiMessages from './messages/hi.json';
import frCAMessages from './messages/fr_CA.json';
// no need to import en messages-- they are in the defaultMessage field
const messages = {
ar: arMessages,
'es-419': es419Messages,
fr: frMessages,
'zh-cn': zhcnMessages,
pt: ptMessages,
it: itMessages,
de: deMessages,
hi: hiMessages,
'fr-ca': frCAMessages,
ru: ruMessages,
uk: ukMessages,
};
export default messages;

View File

@@ -1,2 +1,73 @@
{
}
"gradebook.BulkManagementHistoryView.heading": "سجلّ الإدارة بالجملة",
"gradebook.BulkManagementHistoryView": "فيما يلي سجلّ رفع الدرجات. لتحميل CSV الدرجات أو استيراد درجات بديلة، عد إلى دفتر الدرجات. يرجى الملاحظة أن معالجة الدرجات البديلة قد يأخذ بضع ثوانٍ بعد الاستيراد.",
"gradebook.BulkManagementHistoryView.successDialog": "تتم الآن معالجة CSV. قد يتطلب إتمام رفع الملفات عدة دقائق.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "الواجبات",
"gradebook.GradebookFilters.overallGradeFilterLabel": "الدرجة الكلّية",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "مجموعات الطلبة",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "تضمين أعضاء فريق المساق",
"gradebook.GradebookFilters.assignmentFilterLabel": "الواجب",
"gradebook.GradebookFilters.assignmentTypesLabel": "أنواع الواجبات",
"gradebook.GradebookFilters.maxGradeFilterLabel": " الدرجة القصوى",
"gradebook.GradebookFilters.minGradeFilterLabel": "الدرجة الدنيا",
"gradebook.GradebookFilters.cohorts": "الأفواج",
"gradebook.GradebookFilters.cohortsAll": "جميع الأفواج",
"gradebook.GradebookFilters.tracks": "المسارات",
"gradebook.GradebookFilters.trackAll": "جميع المسارات",
"gradebook.GradebookFilters.closeFilters": "إغلاق المرشحات",
"gradebook.GradebookHeader.backButton": "العودة إلى لوحة المعلومات",
"gradebook.GradebookHeader.appLabel": "دفتر الدرجات",
"gradebook.GradebookHeader.frozenWarning": "درجات هذا المساق الآن مجمدة. لم يعد تعديل الدرجات ممكنا.",
"gradebook.GradebookHeader.unauthorizedWarning": "أنت غير مخوّل بالاطّلاع على دفتر درجات هذا المساق.",
"gradebook.GradebookHeader.toActivityLogButton": "عرض سجلّ الإدارة بالجملة",
"gradebook.GradebookHeader.toGradesView": "العودة إلى دفتر الدرجات",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "تحميل الدرجات",
"gradebook.GradesView.EditModal.headers.assignment": "الواجب",
"gradebook.GradesView.EditModal.headers.currentGrade": "الدرجة الحالية",
"gradebook.GradesView.EditModal.headers.originalGrade": "الدرجة الأصلية",
"gradebook.GradesView.EditModal.headers.student": "الطالب",
"gradebook.GradesView.EditModal.title": "تعديل الدرجات",
"gradebook.GradesView.EditModal.closeText": "إلغاء",
"gradebook.GradesView.EditModal.contactSupport": "يتم إظهار أحدث العمليات فقط (5 على الأكثر). للاطلاع على المزيد، يرجى الاتصال بالدعم.",
"gradebook.GradesView.EditModal.saveVisibility": "ملاحظة: ستظهر تعديلاتك للطلبة بمجرد الحفظ.",
"gradebook.GradesView.EditModal.saveGrade": "حفظ الدرجات",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "الدرجة المعدّلة",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "التاريخ",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "صاحب التقييم",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "السبب",
"gradebook.GradesTab.usersVisibilityLabel'": "عرض {filteredUsers} من إجمالي {totalUsers} متعلمًا",
"gradebook.GradesView.editFilterLabel": "تعديل المرشحات",
"gradebook.GradesView.table.headings.email": "البريد الإلكتروني",
"gradebook.GradesView.table.headings.totalGrade": "الدرجة الإجمالية (%)",
"gradebook.GradesView.table.headings.username": "اسم المستخدم",
"gradebook.GradesView.table.labels.studentKey": "مفتاح الطالب*",
"gradebook.GradesView.table.labels.username": "اسم المستخدم",
"gradebook.GradesView.table.totalGradePercentage": "الدرجات الإجمالية تظهر دائمًا على شكل نسب مئوية",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "رفع درجات بصيغة CSV",
"gradebook.GradesView.importGradesBtnText": "استيراد الدرجات",
"gradebook.GradesView.ImportSuccessToast.description": "نجح الاستيراد! سيتم فورًا تحديث الدرجات.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "عرض سجلّ النشاط",
"gradebook.GradesView.InterventionsReport.title": "تقرير التدخلات",
"gradebook.GradesView.InterventionsReport.description": "هل تحتاج لإيجاد طلبة قد يكونون متأخرين؟ حمّل تقرير التدخلات للحصول على قياسات الالتزام كالزيارات و المحاولات الخاصة بكل فصل.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "تحميل التدخلات",
"gradebook.GradesView.filterHeading": "الخطوة 1: ترشيح تقرير الدرجات",
"gradebook.GradesView.gradebookStepHeading": "الخطوة 2: معاينة و تعديل الدرجات على حدة",
"gradebook.GradesView.mastersHint": "متوفر فقط للمتعلمين المسجلين في مسار الماستر",
"gradebook.GradesView.PageButtons.prevPage": "الصفحة السابقة",
"gradebook.GradesView.PageButtons.nextPage": "الصفحة التالية",
"gradebook.GradesView.scoreViewLabel": "مظهر الدرجة",
"gradebook.GradesView.absoluteOption": "مطلقة",
"gradebook.GradesView.percentOption": "نسبة مئوية",
"gradebook.GradesView.search.label": "البحث عن متعلّم",
"gradebook.GradesView.search.hint": "ابحث حسب اسم المستخدم، البريد الإلكتروني أو مفتاح الطالب",
"gradebook.GradesView.editSuccessAlert": "تم تعديل الدرجة بنجاح. قد تلاحظ تأخّرًا طفيفا في ظهور التحديثات في دفتر الدرجات.",
"gradebook.GradesView.maxCourseGradeInvalid": "الدرجة القصوى يجب أن تكون بين 0 و 100",
"gradebook.GradesView.minCourseGradeInvalid": "الدرجة الدنيا يجب أن تكون بين 0 و 100",
"gradebook.GradesTab.FilterBadges.assignment": "الواجب",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "درجة الواجب",
"gradebook.GradesTab.FilterBadges.assignmentType": "نوع الواجب",
"gradebook.GradesTab.FilterBadges.cohort": "فوج",
"gradebook.GradesTab.FilterBadges.courseGrade": "درجة المساق",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "تضمين أعضاء فريق المساق",
"gradebook.GradesTab.FilterBadges.track": "المسار"
}

73
src/i18n/messages/de.json Normal file
View File

@@ -0,0 +1,73 @@
{
"gradebook.BulkManagementHistoryView.heading": "Bulk Management History",
"gradebook.BulkManagementHistoryView": "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.",
"gradebook.BulkManagementHistoryView.successDialog": "CSV processing. File uploads may take several minutes to complete.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Assignments",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Overall Grade",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Student Groups",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Include Course Team Members",
"gradebook.GradebookFilters.assignmentFilterLabel": "Assignment",
"gradebook.GradebookFilters.assignmentTypesLabel": "Assignment Types",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Max Grade",
"gradebook.GradebookFilters.minGradeFilterLabel": "Min Grade",
"gradebook.GradebookFilters.cohorts": "Cohorts",
"gradebook.GradebookFilters.cohortsAll": "Cohort-All",
"gradebook.GradebookFilters.tracks": "Tracks",
"gradebook.GradebookFilters.trackAll": "Track-All",
"gradebook.GradebookFilters.closeFilters": "Close Filters",
"gradebook.GradebookHeader.backButton": "Back to Dashboard",
"gradebook.GradebookHeader.appLabel": "Gradebook",
"gradebook.GradebookHeader.frozenWarning": "The grades for this course are now frozen. Editing of grades is no longer allowed.",
"gradebook.GradebookHeader.unauthorizedWarning": "You are not authorized to view the gradebook for this course.",
"gradebook.GradebookHeader.toActivityLogButton": "View Bulk Management History",
"gradebook.GradebookHeader.toGradesView": "Return to Gradebook",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Download Grades",
"gradebook.GradesView.EditModal.headers.assignment": "Assignment",
"gradebook.GradesView.EditModal.headers.currentGrade": "Current Grade",
"gradebook.GradesView.EditModal.headers.originalGrade": "Original Grade",
"gradebook.GradesView.EditModal.headers.student": "Student",
"gradebook.GradesView.EditModal.title": "Edit Grades",
"gradebook.GradesView.EditModal.closeText": "Cancel",
"gradebook.GradesView.EditModal.contactSupport": "Showing most recent actions (max 5). To see more, please contact support",
"gradebook.GradesView.EditModal.saveVisibility": "Note: Once you save, your changes will be visible to students.",
"gradebook.GradesView.EditModal.saveGrade": "Save Grades",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Adjusted grade",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Date",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Grader",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
"gradebook.GradesView.editFilterLabel": "Edit Filters",
"gradebook.GradesView.table.headings.email": "Email",
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
"gradebook.GradesView.table.headings.username": "Username",
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
"gradebook.GradesView.table.labels.username": "Username",
"gradebook.GradesView.table.totalGradePercentage": "Total Grade values are always displayed as a percentage",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Upload Grade CSV",
"gradebook.GradesView.importGradesBtnText": "Import Grades",
"gradebook.GradesView.ImportSuccessToast.description": "Import Successful! Grades will be updated momentarily.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "View Activity Log",
"gradebook.GradesView.InterventionsReport.title": "Interventions Report",
"gradebook.GradesView.InterventionsReport.description": "Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Download Interventions",
"gradebook.GradesView.filterHeading": "Step 1: Filter the Grade Report",
"gradebook.GradesView.gradebookStepHeading": "Step 2: View or Modify Individual Grades",
"gradebook.GradesView.mastersHint": "available for learners in the Master's track only",
"gradebook.GradesView.PageButtons.prevPage": "Previous Page",
"gradebook.GradesView.PageButtons.nextPage": "Next Page",
"gradebook.GradesView.scoreViewLabel": "Score View",
"gradebook.GradesView.absoluteOption": "Absolute",
"gradebook.GradesView.percentOption": "Percent",
"gradebook.GradesView.search.label": "Search for a learner",
"gradebook.GradesView.search.hint": "Search by username, email, or student key",
"gradebook.GradesView.editSuccessAlert": "The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.",
"gradebook.GradesView.maxCourseGradeInvalid": "Maximum course grade must be between 0 and 100",
"gradebook.GradesView.minCourseGradeInvalid": "Minimum course grade must be between 0 and 100",
"gradebook.GradesTab.FilterBadges.assignment": "Assignment",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Assignment Grade",
"gradebook.GradesTab.FilterBadges.assignmentType": "Assignment Type",
"gradebook.GradesTab.FilterBadges.cohort": "Cohort",
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
"gradebook.GradesTab.FilterBadges.track": "Track"
}

View File

@@ -1,2 +1,73 @@
{
}
"gradebook.BulkManagementHistoryView.heading": "Historial de administración masiva",
"gradebook.BulkManagementHistoryView": "A continuación se muestra un registro de importaciones de grados anteriores. Para descargar un archivo CSV de su libro de calificaciones e importar las calificaciones para anularlas, regrese al Libro de calificaciones. Tenga en cuenta que, después de importar las calificaciones, es posible que el procesamiento de la anulación demore unos segundos. ",
"gradebook.BulkManagementHistoryView.successDialog": "Procesando archivo CSV. Los archivos subidos pueden tomar algunos minutos para completarse.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Tareas",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Calificación general",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Grupos de estudiantes",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Incluir miembros del equipo del curso",
"gradebook.GradebookFilters.assignmentFilterLabel": "Tarea",
"gradebook.GradebookFilters.assignmentTypesLabel": "Tipos de tareas",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Calificación máxima",
"gradebook.GradebookFilters.minGradeFilterLabel": "Calificación mínima",
"gradebook.GradebookFilters.cohorts": "Cohortes",
"gradebook.GradebookFilters.cohortsAll": "Cohortes-todas",
"gradebook.GradebookFilters.tracks": "Seguimientos",
"gradebook.GradebookFilters.trackAll": " Seguimiento de todo",
"gradebook.GradebookFilters.closeFilters": "Filtros cerrados",
"gradebook.GradebookHeader.backButton": "Volver al Panel de Control",
"gradebook.GradebookHeader.appLabel": "Libro de Calificaciones",
"gradebook.GradebookHeader.frozenWarning": "Las calificaciones para este curso se encuentran congeladas. La edición de las calificaciones ya no se encuentra habilitada. ",
"gradebook.GradebookHeader.unauthorizedWarning": "No tienes autorización para ver el libro de calificaciones de este curso. ",
"gradebook.GradebookHeader.toActivityLogButton": "Ver historial de administración masiva",
"gradebook.GradebookHeader.toGradesView": "Volver al libro de calificaciones.",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Descargar calificaciones",
"gradebook.GradesView.EditModal.headers.assignment": "Tarea",
"gradebook.GradesView.EditModal.headers.currentGrade": "Calificación actual",
"gradebook.GradesView.EditModal.headers.originalGrade": "Calificación original",
"gradebook.GradesView.EditModal.headers.student": "Estudiante",
"gradebook.GradesView.EditModal.title": "Editar calificaciones",
"gradebook.GradesView.EditModal.closeText": "Cancelar",
"gradebook.GradesView.EditModal.contactSupport": "Mostrar las acciones más recientes (máximo 5). Para ver más, por favor contacta a soporte.",
"gradebook.GradesView.EditModal.saveVisibility": "Nota: una vez hayas guardado, tus cambios serán visibles para los estudiantes.",
"gradebook.GradesView.EditModal.saveGrade": "Guardar calificaciones",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Ajustar calificación",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Fecha",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Calificador",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Razón",
"gradebook.GradesTab.usersVisibilityLabel'": "Mostrando {filteredUsers} de {totalUsers} estudiantes en total",
"gradebook.GradesView.editFilterLabel": "Editar filtros ",
"gradebook.GradesView.table.headings.email": "Correo electrónico",
"gradebook.GradesView.table.headings.totalGrade": "Calificación total (%)",
"gradebook.GradesView.table.headings.username": "Nombre de usuario",
"gradebook.GradesView.table.labels.studentKey": "Llave de estudiante*",
"gradebook.GradesView.table.labels.username": "Nombre de usuario",
"gradebook.GradesView.table.totalGradePercentage": "Los valores de las calificaciones totales serán siempre mostrados en porcentajes",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Subir calificación en CSV",
"gradebook.GradesView.importGradesBtnText": "Importar calificaciones",
"gradebook.GradesView.ImportSuccessToast.description": "¡Importación exitosa! Las calificaciones serán actualizadas momentáneamente. ",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "Ver actividad del log",
"gradebook.GradesView.InterventionsReport.title": "Reporte de intervenciones",
"gradebook.GradesView.InterventionsReport.description": "¿Necesitas encontrar estudiantes que no estén teniendo un buen rendimiento? Descarga el reporte de intervenciones para obtener métricas de comportamiento como lo son las visitas e intentos. ",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Descargar de intervenciones",
"gradebook.GradesView.filterHeading": "Paso 1: Filtra el reporte de calificaciones",
"gradebook.GradesView.gradebookStepHeading": "Paso 2: Mira o modifica las calificaciones individuales",
"gradebook.GradesView.mastersHint": "Disponible solo para estudiante en el seguimiento máster",
"gradebook.GradesView.PageButtons.prevPage": "Página anterior",
"gradebook.GradesView.PageButtons.nextPage": "Siguiente página",
"gradebook.GradesView.scoreViewLabel": "Ver puntaje",
"gradebook.GradesView.absoluteOption": "Absoluto",
"gradebook.GradesView.percentOption": "Porcentaje",
"gradebook.GradesView.search.label": "Busca a un estudiante",
"gradebook.GradesView.search.hint": "Busca por nombre de usuario, correo electrónico o llave de estudiante",
"gradebook.GradesView.editSuccessAlert": "La calificación ha sido editada de manera exitosa. Podrás apreciar un retraso mínimo mientras se realizan las actualizaciones en el libro de calificaciones. ",
"gradebook.GradesView.maxCourseGradeInvalid": "La máxima calificación de curso debe estar entre 0 y 100",
"gradebook.GradesView.minCourseGradeInvalid": "La calificación de curso mínima debe estar entre 0 y 100",
"gradebook.GradesTab.FilterBadges.assignment": "Tarea",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Calificación de la tarea",
"gradebook.GradesTab.FilterBadges.assignmentType": "Tipo de tarea",
"gradebook.GradesTab.FilterBadges.cohort": "Cohorte",
"gradebook.GradesTab.FilterBadges.courseGrade": "Calificación del curso",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Incluir miembros del equipo del curso",
"gradebook.GradesTab.FilterBadges.track": "Hacer seguimiento"
}

View File

@@ -1,2 +1,73 @@
{
}
"gradebook.BulkManagementHistoryView.heading": "Historique des changements en lot",
"gradebook.BulkManagementHistoryView": "Ci-dessous se trouve un historique des imports de notes précédents. Pour télécharger un CSV de votre carnet de notes et importer des notes pour les remplacer, retournez au Carnet de Note. Veuillez noter qu'après l'importation des notes, la procédure de remplacement prendra quelques secondes.",
"gradebook.BulkManagementHistoryView.successDialog": "Traitement du CSV en cours. Le téléversement peut prendre plusieurs minutes à compléter.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Devoirs",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Note globale",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Groupe d'étudiants",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Inclure les membres de l'équipe de cours",
"gradebook.GradebookFilters.assignmentFilterLabel": "Devoir",
"gradebook.GradebookFilters.assignmentTypesLabel": "Types de travaux",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Note Maximale",
"gradebook.GradebookFilters.minGradeFilterLabel": "Note Minimale",
"gradebook.GradebookFilters.cohorts": "Cohortes",
"gradebook.GradebookFilters.cohortsAll": "Toutes les cohortes",
"gradebook.GradebookFilters.tracks": "Parcours",
"gradebook.GradebookFilters.trackAll": "Tous les parcours",
"gradebook.GradebookFilters.closeFilters": "Fermer les filtres",
"gradebook.GradebookHeader.backButton": "Retour au tableau de bord",
"gradebook.GradebookHeader.appLabel": "Bulletin de notes",
"gradebook.GradebookHeader.frozenWarning": "Les notes de ce cours sont gelées. Éditer les notes n'est plus permis.",
"gradebook.GradebookHeader.unauthorizedWarning": "Vous n'êtes pas autorisé à voir le livret de notes pour ce cours",
"gradebook.GradebookHeader.toActivityLogButton": "Voir l'historique des changements en lot",
"gradebook.GradebookHeader.toGradesView": "Retourner au carnet de notes",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Télécharger les notes",
"gradebook.GradesView.EditModal.headers.assignment": "Devoir",
"gradebook.GradesView.EditModal.headers.currentGrade": "Note actuelle",
"gradebook.GradesView.EditModal.headers.originalGrade": "Note initiale",
"gradebook.GradesView.EditModal.headers.student": "Étudiant",
"gradebook.GradesView.EditModal.title": "Editez les notes",
"gradebook.GradesView.EditModal.closeText": "Annuler",
"gradebook.GradesView.EditModal.contactSupport": "Affiche les plus récentes actions (max 5). Pour en voir plus, veuillez contacter le support.",
"gradebook.GradesView.EditModal.saveVisibility": "Note : une fois sauvegardés, vos changements seront visibles aux étudiants.",
"gradebook.GradesView.EditModal.saveGrade": "Sauvegarder les notes",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Note ajustée",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Date",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Correcteur",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Motif",
"gradebook.GradesTab.usersVisibilityLabel'": "Affiche {filteredUsers} des {totalUsers} apprenants totaux",
"gradebook.GradesView.editFilterLabel": "Editer les filtres",
"gradebook.GradesView.table.headings.email": "Email",
"gradebook.GradesView.table.headings.totalGrade": "Note totale (%)",
"gradebook.GradesView.table.headings.username": "Nom dutilisateur",
"gradebook.GradesView.table.labels.studentKey": "Clé d'étudiant",
"gradebook.GradesView.table.labels.username": "Nom dutilisateur",
"gradebook.GradesView.table.totalGradePercentage": "La note totale est toujours affiché en pourcentage",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Importer les notes CSV",
"gradebook.GradesView.importGradesBtnText": "Importer les notes",
"gradebook.GradesView.ImportSuccessToast.description": "Import réussi ! Les notes vont être mises à jour.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "Voir le rapport d'activité",
"gradebook.GradesView.InterventionsReport.title": "Rapport d'interventions",
"gradebook.GradesView.InterventionsReport.description": "Besoin de trouver les étudiants en difficulté ? Téléchargez le rapport d'interventions afin d'obtenir les métriques d'engagement telles que le nombre de tentatives ou de visites des sections.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Télécharger les interventions",
"gradebook.GradesView.filterHeading": "Étape 1 : Filtrer le rapport de notes",
"gradebook.GradesView.gradebookStepHeading": "Étape 2 : Voir ou modifier les notes individuelles",
"gradebook.GradesView.mastersHint": "disponible seulement aux apprenants sur le parcours Master",
"gradebook.GradesView.PageButtons.prevPage": "Page précédente",
"gradebook.GradesView.PageButtons.nextPage": "Page suivante",
"gradebook.GradesView.scoreViewLabel": "Vue des notes",
"gradebook.GradesView.absoluteOption": "Absolu",
"gradebook.GradesView.percentOption": "Pourcentage",
"gradebook.GradesView.search.label": "Rechercher un apprenant",
"gradebook.GradesView.search.hint": "Chercher par nom d'utilisateur, email ou clé d'étudiant",
"gradebook.GradesView.editSuccessAlert": "Les notes ont été éditées avec succès. Un délai est possible avant que vous ne constatiez la mise-à-jour dans le carnet de note.",
"gradebook.GradesView.maxCourseGradeInvalid": "La note maximale du cours doit être entre 0 et 100",
"gradebook.GradesView.minCourseGradeInvalid": "La note minimale du cours doit être entre 0 et 100",
"gradebook.GradesTab.FilterBadges.assignment": "Devoir",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Note des devoirs",
"gradebook.GradesTab.FilterBadges.assignmentType": "Type de travail",
"gradebook.GradesTab.FilterBadges.cohort": "Cohorte",
"gradebook.GradesTab.FilterBadges.courseGrade": "Note du cours",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Inclure les membres de l'équipe de cours",
"gradebook.GradesTab.FilterBadges.track": "Parcours"
}

View File

@@ -0,0 +1,73 @@
{
"gradebook.BulkManagementHistoryView.heading": "Historique des changements en lot",
"gradebook.BulkManagementHistoryView": "Ci-dessous se trouve un historique des imports de notes précédents. Pour télécharger un CSV de votre carnet de notes et importer des notes pour les remplacer, retournez au Carnet de Note. Veuillez noter qu'après l'importation des notes, la procédure de remplacement prendra quelques secondes.",
"gradebook.BulkManagementHistoryView.successDialog": "Traitement du CSV en cours. Le téléversement peut prendre plusieurs minutes à compléter.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Devoirs",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Note globale",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Groupes d'étudiant",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Inclure les membres de l'équipe de cours",
"gradebook.GradebookFilters.assignmentFilterLabel": "Devoir",
"gradebook.GradebookFilters.assignmentTypesLabel": "Types de travaux",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Note maximale",
"gradebook.GradebookFilters.minGradeFilterLabel": "Note minimale",
"gradebook.GradebookFilters.cohorts": "Cohortes",
"gradebook.GradebookFilters.cohortsAll": "Toutes les cohortes",
"gradebook.GradebookFilters.tracks": "Parcours",
"gradebook.GradebookFilters.trackAll": "Tous les parcours",
"gradebook.GradebookFilters.closeFilters": "Fermer filtres",
"gradebook.GradebookHeader.backButton": "Retour au tableau de bord",
"gradebook.GradebookHeader.appLabel": "Bulletin de notes",
"gradebook.GradebookHeader.frozenWarning": "Les notes de ce cours sont gelées. Éditer les notes n'est plus permis.",
"gradebook.GradebookHeader.unauthorizedWarning": "Vous n'êtes pas autorisé à voir le carnet de note de ce cours.",
"gradebook.GradebookHeader.toActivityLogButton": "Voir l'historique des changements en lot",
"gradebook.GradebookHeader.toGradesView": "Retourner au carnet de notes",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Télécharger les notes",
"gradebook.GradesView.EditModal.headers.assignment": "Devoir",
"gradebook.GradesView.EditModal.headers.currentGrade": "Note actuelle",
"gradebook.GradesView.EditModal.headers.originalGrade": "Note originale",
"gradebook.GradesView.EditModal.headers.student": "Étudiant",
"gradebook.GradesView.EditModal.title": "Éditer les notes",
"gradebook.GradesView.EditModal.closeText": "Annuler",
"gradebook.GradesView.EditModal.contactSupport": "Affiche les plus récentes actions (max 5). Pour en voir plus, veuillez contacter le support.",
"gradebook.GradesView.EditModal.saveVisibility": "Note : une fois sauvegardés, vos changements seront visibles aux étudiants.",
"gradebook.GradesView.EditModal.saveGrade": "Sauvegarde des notes",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Notes ajustées",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Date",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Correcteur",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Raison",
"gradebook.GradesTab.usersVisibilityLabel'": "Affiche {filteredUsers} des {totalUsers} apprenants totaux",
"gradebook.GradesView.editFilterLabel": "Éditer les filtres",
"gradebook.GradesView.table.headings.email": "Courriel",
"gradebook.GradesView.table.headings.totalGrade": "Note totale (%)",
"gradebook.GradesView.table.headings.username": "Nom d'utilisateur",
"gradebook.GradesView.table.labels.studentKey": "Clé étudiante*",
"gradebook.GradesView.table.labels.username": "Nom d'utilisateur",
"gradebook.GradesView.table.totalGradePercentage": "La valeur des notes totales est toujours affichée en pourcentage",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Téléverser les notes en CSV",
"gradebook.GradesView.importGradesBtnText": "Importer les notes",
"gradebook.GradesView.ImportSuccessToast.description": "Succès de l'importation ! Les notes seront mises à jour momentanément.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "Voir le log des activités",
"gradebook.GradesView.InterventionsReport.title": "Rapport des interventions",
"gradebook.GradesView.InterventionsReport.description": "Besoin de trouver les étudiants en difficulté ? Téléchargez le rapport d'interventions afin d'obtenir les métriques d'engagement telles que le nombre de tentatives ou de visites des sections.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Télécharger les interventions",
"gradebook.GradesView.filterHeading": "Étape 1 : Filter le rapport de notes",
"gradebook.GradesView.gradebookStepHeading": "Étape 2 : Voir ou modifier les notes individuelles",
"gradebook.GradesView.mastersHint": "disponible seulement aux apprenants sur le parcours Master",
"gradebook.GradesView.PageButtons.prevPage": "Page précédente",
"gradebook.GradesView.PageButtons.nextPage": "Page suivante",
"gradebook.GradesView.scoreViewLabel": "Vue des scores",
"gradebook.GradesView.absoluteOption": "Absolu",
"gradebook.GradesView.percentOption": "Pourcentage",
"gradebook.GradesView.search.label": "Chercher pour un apprenant",
"gradebook.GradesView.search.hint": "Chercher par nom d'utilisateur, courriel ou clé étudiante",
"gradebook.GradesView.editSuccessAlert": "Les notes ont été éditées avec succès. Un délai est possible avant que vous ne constatiez la mise-à-jour dans le carnet de note.",
"gradebook.GradesView.maxCourseGradeInvalid": "Note maximale du cours doit être entre 0 et 100",
"gradebook.GradesView.minCourseGradeInvalid": "Note minimale du cours doit être entre 0 et 100",
"gradebook.GradesTab.FilterBadges.assignment": "Devoir",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Note de devoir",
"gradebook.GradesTab.FilterBadges.assignmentType": "Type de devoir",
"gradebook.GradesTab.FilterBadges.cohort": "Cohorte",
"gradebook.GradesTab.FilterBadges.courseGrade": "Note du cours",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Inclure les membres de l'équipe de cours",
"gradebook.GradesTab.FilterBadges.track": "Parcours"
}

73
src/i18n/messages/hi.json Normal file
View File

@@ -0,0 +1,73 @@
{
"gradebook.BulkManagementHistoryView.heading": "Bulk Management History",
"gradebook.BulkManagementHistoryView": "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.",
"gradebook.BulkManagementHistoryView.successDialog": "CSV processing. File uploads may take several minutes to complete.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Assignments",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Overall Grade",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Student Groups",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Include Course Team Members",
"gradebook.GradebookFilters.assignmentFilterLabel": "Assignment",
"gradebook.GradebookFilters.assignmentTypesLabel": "Assignment Types",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Max Grade",
"gradebook.GradebookFilters.minGradeFilterLabel": "Min Grade",
"gradebook.GradebookFilters.cohorts": "Cohorts",
"gradebook.GradebookFilters.cohortsAll": "Cohort-All",
"gradebook.GradebookFilters.tracks": "Tracks",
"gradebook.GradebookFilters.trackAll": "Track-All",
"gradebook.GradebookFilters.closeFilters": "Close Filters",
"gradebook.GradebookHeader.backButton": "Back to Dashboard",
"gradebook.GradebookHeader.appLabel": "Gradebook",
"gradebook.GradebookHeader.frozenWarning": "The grades for this course are now frozen. Editing of grades is no longer allowed.",
"gradebook.GradebookHeader.unauthorizedWarning": "You are not authorized to view the gradebook for this course.",
"gradebook.GradebookHeader.toActivityLogButton": "View Bulk Management History",
"gradebook.GradebookHeader.toGradesView": "Return to Gradebook",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Download Grades",
"gradebook.GradesView.EditModal.headers.assignment": "Assignment",
"gradebook.GradesView.EditModal.headers.currentGrade": "Current Grade",
"gradebook.GradesView.EditModal.headers.originalGrade": "Original Grade",
"gradebook.GradesView.EditModal.headers.student": "Student",
"gradebook.GradesView.EditModal.title": "Edit Grades",
"gradebook.GradesView.EditModal.closeText": "Cancel",
"gradebook.GradesView.EditModal.contactSupport": "Showing most recent actions (max 5). To see more, please contact support",
"gradebook.GradesView.EditModal.saveVisibility": "Note: Once you save, your changes will be visible to students.",
"gradebook.GradesView.EditModal.saveGrade": "Save Grades",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Adjusted grade",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Date",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Grader",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
"gradebook.GradesView.editFilterLabel": "Edit Filters",
"gradebook.GradesView.table.headings.email": "Email",
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
"gradebook.GradesView.table.headings.username": "Username",
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
"gradebook.GradesView.table.labels.username": "Username",
"gradebook.GradesView.table.totalGradePercentage": "Total Grade values are always displayed as a percentage",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Upload Grade CSV",
"gradebook.GradesView.importGradesBtnText": "Import Grades",
"gradebook.GradesView.ImportSuccessToast.description": "Import Successful! Grades will be updated momentarily.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "View Activity Log",
"gradebook.GradesView.InterventionsReport.title": "Interventions Report",
"gradebook.GradesView.InterventionsReport.description": "Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Download Interventions",
"gradebook.GradesView.filterHeading": "Step 1: Filter the Grade Report",
"gradebook.GradesView.gradebookStepHeading": "Step 2: View or Modify Individual Grades",
"gradebook.GradesView.mastersHint": "available for learners in the Master's track only",
"gradebook.GradesView.PageButtons.prevPage": "Previous Page",
"gradebook.GradesView.PageButtons.nextPage": "Next Page",
"gradebook.GradesView.scoreViewLabel": "Score View",
"gradebook.GradesView.absoluteOption": "Absolute",
"gradebook.GradesView.percentOption": "Percent",
"gradebook.GradesView.search.label": "Search for a learner",
"gradebook.GradesView.search.hint": "Search by username, email, or student key",
"gradebook.GradesView.editSuccessAlert": "The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.",
"gradebook.GradesView.maxCourseGradeInvalid": "Maximum course grade must be between 0 and 100",
"gradebook.GradesView.minCourseGradeInvalid": "Minimum course grade must be between 0 and 100",
"gradebook.GradesTab.FilterBadges.assignment": "Assignment",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Assignment Grade",
"gradebook.GradesTab.FilterBadges.assignmentType": "Assignment Type",
"gradebook.GradesTab.FilterBadges.cohort": "Cohort",
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
"gradebook.GradesTab.FilterBadges.track": "Track"
}

73
src/i18n/messages/it.json Normal file
View File

@@ -0,0 +1,73 @@
{
"gradebook.BulkManagementHistoryView.heading": "Bulk Management History",
"gradebook.BulkManagementHistoryView": "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.",
"gradebook.BulkManagementHistoryView.successDialog": "CSV processing. File uploads may take several minutes to complete.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Assignments",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Overall Grade",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Student Groups",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Include Course Team Members",
"gradebook.GradebookFilters.assignmentFilterLabel": "Assignment",
"gradebook.GradebookFilters.assignmentTypesLabel": "Assignment Types",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Max Grade",
"gradebook.GradebookFilters.minGradeFilterLabel": "Min Grade",
"gradebook.GradebookFilters.cohorts": "Cohorts",
"gradebook.GradebookFilters.cohortsAll": "Cohort-All",
"gradebook.GradebookFilters.tracks": "Tracks",
"gradebook.GradebookFilters.trackAll": "Track-All",
"gradebook.GradebookFilters.closeFilters": "Close Filters",
"gradebook.GradebookHeader.backButton": "Back to Dashboard",
"gradebook.GradebookHeader.appLabel": "Gradebook",
"gradebook.GradebookHeader.frozenWarning": "The grades for this course are now frozen. Editing of grades is no longer allowed.",
"gradebook.GradebookHeader.unauthorizedWarning": "You are not authorized to view the gradebook for this course.",
"gradebook.GradebookHeader.toActivityLogButton": "View Bulk Management History",
"gradebook.GradebookHeader.toGradesView": "Return to Gradebook",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Download Grades",
"gradebook.GradesView.EditModal.headers.assignment": "Assignment",
"gradebook.GradesView.EditModal.headers.currentGrade": "Current Grade",
"gradebook.GradesView.EditModal.headers.originalGrade": "Original Grade",
"gradebook.GradesView.EditModal.headers.student": "Student",
"gradebook.GradesView.EditModal.title": "Edit Grades",
"gradebook.GradesView.EditModal.closeText": "Cancel",
"gradebook.GradesView.EditModal.contactSupport": "Showing most recent actions (max 5). To see more, please contact support",
"gradebook.GradesView.EditModal.saveVisibility": "Note: Once you save, your changes will be visible to students.",
"gradebook.GradesView.EditModal.saveGrade": "Save Grades",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Adjusted grade",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Date",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Grader",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
"gradebook.GradesView.editFilterLabel": "Edit Filters",
"gradebook.GradesView.table.headings.email": "Email",
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
"gradebook.GradesView.table.headings.username": "Username",
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
"gradebook.GradesView.table.labels.username": "Username",
"gradebook.GradesView.table.totalGradePercentage": "Total Grade values are always displayed as a percentage",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Upload Grade CSV",
"gradebook.GradesView.importGradesBtnText": "Import Grades",
"gradebook.GradesView.ImportSuccessToast.description": "Import Successful! Grades will be updated momentarily.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "View Activity Log",
"gradebook.GradesView.InterventionsReport.title": "Interventions Report",
"gradebook.GradesView.InterventionsReport.description": "Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Download Interventions",
"gradebook.GradesView.filterHeading": "Step 1: Filter the Grade Report",
"gradebook.GradesView.gradebookStepHeading": "Step 2: View or Modify Individual Grades",
"gradebook.GradesView.mastersHint": "available for learners in the Master's track only",
"gradebook.GradesView.PageButtons.prevPage": "Previous Page",
"gradebook.GradesView.PageButtons.nextPage": "Next Page",
"gradebook.GradesView.scoreViewLabel": "Score View",
"gradebook.GradesView.absoluteOption": "Absolute",
"gradebook.GradesView.percentOption": "Percent",
"gradebook.GradesView.search.label": "Search for a learner",
"gradebook.GradesView.search.hint": "Search by username, email, or student key",
"gradebook.GradesView.editSuccessAlert": "The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.",
"gradebook.GradesView.maxCourseGradeInvalid": "Maximum course grade must be between 0 and 100",
"gradebook.GradesView.minCourseGradeInvalid": "Minimum course grade must be between 0 and 100",
"gradebook.GradesTab.FilterBadges.assignment": "Assignment",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Assignment Grade",
"gradebook.GradesTab.FilterBadges.assignmentType": "Assignment Type",
"gradebook.GradesTab.FilterBadges.cohort": "Cohort",
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
"gradebook.GradesTab.FilterBadges.track": "Track"
}

73
src/i18n/messages/pt.json Normal file
View File

@@ -0,0 +1,73 @@
{
"gradebook.BulkManagementHistoryView.heading": "Bulk Management History",
"gradebook.BulkManagementHistoryView": "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.",
"gradebook.BulkManagementHistoryView.successDialog": "CSV processing. File uploads may take several minutes to complete.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Assignments",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Overall Grade",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Student Groups",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Include Course Team Members",
"gradebook.GradebookFilters.assignmentFilterLabel": "Assignment",
"gradebook.GradebookFilters.assignmentTypesLabel": "Assignment Types",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Max Grade",
"gradebook.GradebookFilters.minGradeFilterLabel": "Min Grade",
"gradebook.GradebookFilters.cohorts": "Cohorts",
"gradebook.GradebookFilters.cohortsAll": "Cohort-All",
"gradebook.GradebookFilters.tracks": "Tracks",
"gradebook.GradebookFilters.trackAll": "Track-All",
"gradebook.GradebookFilters.closeFilters": "Close Filters",
"gradebook.GradebookHeader.backButton": "Back to Dashboard",
"gradebook.GradebookHeader.appLabel": "Gradebook",
"gradebook.GradebookHeader.frozenWarning": "The grades for this course are now frozen. Editing of grades is no longer allowed.",
"gradebook.GradebookHeader.unauthorizedWarning": "You are not authorized to view the gradebook for this course.",
"gradebook.GradebookHeader.toActivityLogButton": "View Bulk Management History",
"gradebook.GradebookHeader.toGradesView": "Return to Gradebook",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Download Grades",
"gradebook.GradesView.EditModal.headers.assignment": "Assignment",
"gradebook.GradesView.EditModal.headers.currentGrade": "Current Grade",
"gradebook.GradesView.EditModal.headers.originalGrade": "Original Grade",
"gradebook.GradesView.EditModal.headers.student": "Student",
"gradebook.GradesView.EditModal.title": "Edit Grades",
"gradebook.GradesView.EditModal.closeText": "Cancel",
"gradebook.GradesView.EditModal.contactSupport": "Showing most recent actions (max 5). To see more, please contact support",
"gradebook.GradesView.EditModal.saveVisibility": "Note: Once you save, your changes will be visible to students.",
"gradebook.GradesView.EditModal.saveGrade": "Save Grades",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Adjusted grade",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Date",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Grader",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
"gradebook.GradesView.editFilterLabel": "Edit Filters",
"gradebook.GradesView.table.headings.email": "Email",
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
"gradebook.GradesView.table.headings.username": "Username",
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
"gradebook.GradesView.table.labels.username": "Username",
"gradebook.GradesView.table.totalGradePercentage": "Total Grade values are always displayed as a percentage",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Upload Grade CSV",
"gradebook.GradesView.importGradesBtnText": "Import Grades",
"gradebook.GradesView.ImportSuccessToast.description": "Import Successful! Grades will be updated momentarily.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "View Activity Log",
"gradebook.GradesView.InterventionsReport.title": "Interventions Report",
"gradebook.GradesView.InterventionsReport.description": "Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Download Interventions",
"gradebook.GradesView.filterHeading": "Step 1: Filter the Grade Report",
"gradebook.GradesView.gradebookStepHeading": "Step 2: View or Modify Individual Grades",
"gradebook.GradesView.mastersHint": "available for learners in the Master's track only",
"gradebook.GradesView.PageButtons.prevPage": "Previous Page",
"gradebook.GradesView.PageButtons.nextPage": "Next Page",
"gradebook.GradesView.scoreViewLabel": "Score View",
"gradebook.GradesView.absoluteOption": "Absolute",
"gradebook.GradesView.percentOption": "Percent",
"gradebook.GradesView.search.label": "Search for a learner",
"gradebook.GradesView.search.hint": "Search by username, email, or student key",
"gradebook.GradesView.editSuccessAlert": "The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.",
"gradebook.GradesView.maxCourseGradeInvalid": "Maximum course grade must be between 0 and 100",
"gradebook.GradesView.minCourseGradeInvalid": "Minimum course grade must be between 0 and 100",
"gradebook.GradesTab.FilterBadges.assignment": "Assignment",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Assignment Grade",
"gradebook.GradesTab.FilterBadges.assignmentType": "Assignment Type",
"gradebook.GradesTab.FilterBadges.cohort": "Cohort",
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
"gradebook.GradesTab.FilterBadges.track": "Track"
}

73
src/i18n/messages/ru.json Normal file
View File

@@ -0,0 +1,73 @@
{
"gradebook.BulkManagementHistoryView.heading": "Bulk Management History",
"gradebook.BulkManagementHistoryView": "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.",
"gradebook.BulkManagementHistoryView.successDialog": "CSV processing. File uploads may take several minutes to complete.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Assignments",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Overall Grade",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Student Groups",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Include Course Team Members",
"gradebook.GradebookFilters.assignmentFilterLabel": "Assignment",
"gradebook.GradebookFilters.assignmentTypesLabel": "Assignment Types",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Max Grade",
"gradebook.GradebookFilters.minGradeFilterLabel": "Min Grade",
"gradebook.GradebookFilters.cohorts": "Cohorts",
"gradebook.GradebookFilters.cohortsAll": "Cohort-All",
"gradebook.GradebookFilters.tracks": "Tracks",
"gradebook.GradebookFilters.trackAll": "Track-All",
"gradebook.GradebookFilters.closeFilters": "Close Filters",
"gradebook.GradebookHeader.backButton": "Back to Dashboard",
"gradebook.GradebookHeader.appLabel": "Gradebook",
"gradebook.GradebookHeader.frozenWarning": "The grades for this course are now frozen. Editing of grades is no longer allowed.",
"gradebook.GradebookHeader.unauthorizedWarning": "You are not authorized to view the gradebook for this course.",
"gradebook.GradebookHeader.toActivityLogButton": "View Bulk Management History",
"gradebook.GradebookHeader.toGradesView": "Return to Gradebook",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Download Grades",
"gradebook.GradesView.EditModal.headers.assignment": "Assignment",
"gradebook.GradesView.EditModal.headers.currentGrade": "Current Grade",
"gradebook.GradesView.EditModal.headers.originalGrade": "Original Grade",
"gradebook.GradesView.EditModal.headers.student": "Student",
"gradebook.GradesView.EditModal.title": "Edit Grades",
"gradebook.GradesView.EditModal.closeText": "Cancel",
"gradebook.GradesView.EditModal.contactSupport": "Showing most recent actions (max 5). To see more, please contact support",
"gradebook.GradesView.EditModal.saveVisibility": "Note: Once you save, your changes will be visible to students.",
"gradebook.GradesView.EditModal.saveGrade": "Save Grades",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Adjusted grade",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Date",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Grader",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
"gradebook.GradesView.editFilterLabel": "Edit Filters",
"gradebook.GradesView.table.headings.email": "Email",
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
"gradebook.GradesView.table.headings.username": "Username",
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
"gradebook.GradesView.table.labels.username": "Username",
"gradebook.GradesView.table.totalGradePercentage": "Total Grade values are always displayed as a percentage",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Upload Grade CSV",
"gradebook.GradesView.importGradesBtnText": "Import Grades",
"gradebook.GradesView.ImportSuccessToast.description": "Import Successful! Grades will be updated momentarily.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "View Activity Log",
"gradebook.GradesView.InterventionsReport.title": "Interventions Report",
"gradebook.GradesView.InterventionsReport.description": "Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Download Interventions",
"gradebook.GradesView.filterHeading": "Step 1: Filter the Grade Report",
"gradebook.GradesView.gradebookStepHeading": "Step 2: View or Modify Individual Grades",
"gradebook.GradesView.mastersHint": "available for learners in the Master's track only",
"gradebook.GradesView.PageButtons.prevPage": "Previous Page",
"gradebook.GradesView.PageButtons.nextPage": "Next Page",
"gradebook.GradesView.scoreViewLabel": "Score View",
"gradebook.GradesView.absoluteOption": "Absolute",
"gradebook.GradesView.percentOption": "Percent",
"gradebook.GradesView.search.label": "Search for a learner",
"gradebook.GradesView.search.hint": "Search by username, email, or student key",
"gradebook.GradesView.editSuccessAlert": "The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.",
"gradebook.GradesView.maxCourseGradeInvalid": "Maximum course grade must be between 0 and 100",
"gradebook.GradesView.minCourseGradeInvalid": "Minimum course grade must be between 0 and 100",
"gradebook.GradesTab.FilterBadges.assignment": "Assignment",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Assignment Grade",
"gradebook.GradesTab.FilterBadges.assignmentType": "Assignment Type",
"gradebook.GradesTab.FilterBadges.cohort": "Cohort",
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
"gradebook.GradesTab.FilterBadges.track": "Track"
}

73
src/i18n/messages/uk.json Normal file
View File

@@ -0,0 +1,73 @@
{
"gradebook.BulkManagementHistoryView.heading": "Bulk Management History",
"gradebook.BulkManagementHistoryView": "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.",
"gradebook.BulkManagementHistoryView.successDialog": "CSV processing. File uploads may take several minutes to complete.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Assignments",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Overall Grade",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Student Groups",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Include Course Team Members",
"gradebook.GradebookFilters.assignmentFilterLabel": "Assignment",
"gradebook.GradebookFilters.assignmentTypesLabel": "Assignment Types",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Max Grade",
"gradebook.GradebookFilters.minGradeFilterLabel": "Min Grade",
"gradebook.GradebookFilters.cohorts": "Cohorts",
"gradebook.GradebookFilters.cohortsAll": "Cohort-All",
"gradebook.GradebookFilters.tracks": "Tracks",
"gradebook.GradebookFilters.trackAll": "Track-All",
"gradebook.GradebookFilters.closeFilters": "Close Filters",
"gradebook.GradebookHeader.backButton": "Back to Dashboard",
"gradebook.GradebookHeader.appLabel": "Gradebook",
"gradebook.GradebookHeader.frozenWarning": "The grades for this course are now frozen. Editing of grades is no longer allowed.",
"gradebook.GradebookHeader.unauthorizedWarning": "You are not authorized to view the gradebook for this course.",
"gradebook.GradebookHeader.toActivityLogButton": "View Bulk Management History",
"gradebook.GradebookHeader.toGradesView": "Return to Gradebook",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Download Grades",
"gradebook.GradesView.EditModal.headers.assignment": "Assignment",
"gradebook.GradesView.EditModal.headers.currentGrade": "Current Grade",
"gradebook.GradesView.EditModal.headers.originalGrade": "Original Grade",
"gradebook.GradesView.EditModal.headers.student": "Student",
"gradebook.GradesView.EditModal.title": "Edit Grades",
"gradebook.GradesView.EditModal.closeText": "Cancel",
"gradebook.GradesView.EditModal.contactSupport": "Showing most recent actions (max 5). To see more, please contact support",
"gradebook.GradesView.EditModal.saveVisibility": "Note: Once you save, your changes will be visible to students.",
"gradebook.GradesView.EditModal.saveGrade": "Save Grades",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Adjusted grade",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Date",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Grader",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
"gradebook.GradesView.editFilterLabel": "Edit Filters",
"gradebook.GradesView.table.headings.email": "Email",
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
"gradebook.GradesView.table.headings.username": "Username",
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
"gradebook.GradesView.table.labels.username": "Username",
"gradebook.GradesView.table.totalGradePercentage": "Total Grade values are always displayed as a percentage",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Upload Grade CSV",
"gradebook.GradesView.importGradesBtnText": "Import Grades",
"gradebook.GradesView.ImportSuccessToast.description": "Import Successful! Grades will be updated momentarily.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "View Activity Log",
"gradebook.GradesView.InterventionsReport.title": "Interventions Report",
"gradebook.GradesView.InterventionsReport.description": "Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Download Interventions",
"gradebook.GradesView.filterHeading": "Step 1: Filter the Grade Report",
"gradebook.GradesView.gradebookStepHeading": "Step 2: View or Modify Individual Grades",
"gradebook.GradesView.mastersHint": "available for learners in the Master's track only",
"gradebook.GradesView.PageButtons.prevPage": "Previous Page",
"gradebook.GradesView.PageButtons.nextPage": "Next Page",
"gradebook.GradesView.scoreViewLabel": "Score View",
"gradebook.GradesView.absoluteOption": "Absolute",
"gradebook.GradesView.percentOption": "Percent",
"gradebook.GradesView.search.label": "Search for a learner",
"gradebook.GradesView.search.hint": "Search by username, email, or student key",
"gradebook.GradesView.editSuccessAlert": "The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.",
"gradebook.GradesView.maxCourseGradeInvalid": "Maximum course grade must be between 0 and 100",
"gradebook.GradesView.minCourseGradeInvalid": "Minimum course grade must be between 0 and 100",
"gradebook.GradesTab.FilterBadges.assignment": "Assignment",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Assignment Grade",
"gradebook.GradesTab.FilterBadges.assignmentType": "Assignment Type",
"gradebook.GradesTab.FilterBadges.cohort": "Cohort",
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
"gradebook.GradesTab.FilterBadges.track": "Track"
}

View File

@@ -1,2 +1,73 @@
{
}
"gradebook.BulkManagementHistoryView.heading": "Bulk Management History",
"gradebook.BulkManagementHistoryView": "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.",
"gradebook.BulkManagementHistoryView.successDialog": "CSV processing. File uploads may take several minutes to complete.",
"gradebook.GradebookFilters.assignmentsFilterLabel": "Assignments",
"gradebook.GradebookFilters.overallGradeFilterLabel": "Overall Grade",
"gradebook.GradebookFilters.studentGroupsFilterLabel": "Student Groups",
"gradebook.GradebookFilters.includeCourseTeamMembersFilterLabel": "Include Course Team Members",
"gradebook.GradebookFilters.assignmentFilterLabel": "Assignment",
"gradebook.GradebookFilters.assignmentTypesLabel": "Assignment Types",
"gradebook.GradebookFilters.maxGradeFilterLabel": "Max Grade",
"gradebook.GradebookFilters.minGradeFilterLabel": "Min Grade",
"gradebook.GradebookFilters.cohorts": "Cohorts",
"gradebook.GradebookFilters.cohortsAll": "Cohort-All",
"gradebook.GradebookFilters.tracks": "Tracks",
"gradebook.GradebookFilters.trackAll": "Track-All",
"gradebook.GradebookFilters.closeFilters": "Close Filters",
"gradebook.GradebookHeader.backButton": "Back to Dashboard",
"gradebook.GradebookHeader.appLabel": "Gradebook",
"gradebook.GradebookHeader.frozenWarning": "The grades for this course are now frozen. Editing of grades is no longer allowed.",
"gradebook.GradebookHeader.unauthorizedWarning": "You are not authorized to view the gradebook for this course.",
"gradebook.GradebookHeader.toActivityLogButton": "View Bulk Management History",
"gradebook.GradebookHeader.toGradesView": "Return to Gradebook",
"gradebook.GradesView.BulkManagementControls.bulkManagementLabel": "Download Grades",
"gradebook.GradesView.EditModal.headers.assignment": "Assignment",
"gradebook.GradesView.EditModal.headers.currentGrade": "Current Grade",
"gradebook.GradesView.EditModal.headers.originalGrade": "Original Grade",
"gradebook.GradesView.EditModal.headers.student": "Student",
"gradebook.GradesView.EditModal.title": "Edit Grades",
"gradebook.GradesView.EditModal.closeText": "Cancel",
"gradebook.GradesView.EditModal.contactSupport": "Showing most recent actions (max 5). To see more, please contact support",
"gradebook.GradesView.EditModal.saveVisibility": "Note: Once you save, your changes will be visible to students.",
"gradebook.GradesView.EditModal.saveGrade": "Save Grades",
"gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader": "Adjusted grade",
"gradebook.GradesView.EditModal.Overrides.dateHeader": "Date",
"gradebook.GradesView.EditModal.Overrides.graderHeader": "Grader",
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
"gradebook.GradesView.editFilterLabel": "Edit Filters",
"gradebook.GradesView.table.headings.email": "Email",
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
"gradebook.GradesView.table.headings.username": "Username",
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
"gradebook.GradesView.table.labels.username": "Username",
"gradebook.GradesView.table.totalGradePercentage": "Total Grade values are always displayed as a percentage",
"gradebook.BulkManagementHistoryView.csvUploadLabel": "Upload Grade CSV",
"gradebook.GradesView.importGradesBtnText": "Import Grades",
"gradebook.GradesView.ImportSuccessToast.description": "Import Successful! Grades will be updated momentarily.",
"gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn": "View Activity Log",
"gradebook.GradesView.InterventionsReport.title": "Interventions Report",
"gradebook.GradesView.InterventionsReport.description": "Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.",
"gradebook.GradesView.InterventionsReport.downloadBtn": "Download Interventions",
"gradebook.GradesView.filterHeading": "Step 1: Filter the Grade Report",
"gradebook.GradesView.gradebookStepHeading": "Step 2: View or Modify Individual Grades",
"gradebook.GradesView.mastersHint": "available for learners in the Master's track only",
"gradebook.GradesView.PageButtons.prevPage": "Previous Page",
"gradebook.GradesView.PageButtons.nextPage": "Next Page",
"gradebook.GradesView.scoreViewLabel": "Score View",
"gradebook.GradesView.absoluteOption": "Absolute",
"gradebook.GradesView.percentOption": "Percent",
"gradebook.GradesView.search.label": "Search for a learner",
"gradebook.GradesView.search.hint": "Search by username, email, or student key",
"gradebook.GradesView.editSuccessAlert": "The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.",
"gradebook.GradesView.maxCourseGradeInvalid": "Maximum course grade must be between 0 and 100",
"gradebook.GradesView.minCourseGradeInvalid": "Minimum course grade must be between 0 and 100",
"gradebook.GradesTab.FilterBadges.assignment": "Assignment",
"gradebook.GradesTab.FilterBadges.assignmentGrade": "Assignment Grade",
"gradebook.GradesTab.FilterBadges.assignmentType": "Assignment Type",
"gradebook.GradesTab.FilterBadges.cohort": "Cohort",
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
"gradebook.GradesTab.FilterBadges.track": "Track"
}

View File

@@ -1,8 +0,0 @@
{
"gradebook.BulkManagementTab.csvUploadLabel": "Upload Grade CSV",
"gradebook.BulkManagementTab.heading": "Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload.",
"gradebook.BulkManagementTab.hint1": "Results appear in the table below.",
"gradebook.BulkManagementTab.hint2": "Grade processing may take a few seconds.",
"gradebook.BulkManagementTab.importBtnText": "Import Grades",
"gradebook.BulkManagementTab.successDialog": "CSV processing. File uploads may take several minutes to complete."
}

View File

@@ -7,10 +7,12 @@ import ReactDOM from 'react-dom';
import {
APP_READY,
initialize,
mergeConfig,
subscribe,
} from '@edx/frontend-platform';
import { messages as headerMessages } from '@edx/frontend-component-header';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import { messages as paragonMessages } from '@edx/paragon';
import appMessages from './i18n';
import App from './App';
@@ -19,9 +21,27 @@ subscribe(APP_READY, () => {
});
initialize({
handlers: {
config: () => {
mergeConfig({
BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
SECURE_COOKIES: process.env.NODE_ENV !== 'development',
SEGMENT_KEY: process.env.SEGMENT_KEY,
ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
});
},
},
messages: [
appMessages,
headerMessages,
footerMessages,
paragonMessages,
],
requireAuthenticatedUser: true,
});

View File

@@ -4,10 +4,12 @@ import ReactDOM from 'react-dom';
import {
APP_READY,
initialize,
mergeConfig,
subscribe,
} from '@edx/frontend-platform';
import { messages as headerMessages } from '@edx/frontend-component-header';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import { messages as paragonMessages } from '@edx/paragon';
import appMessages from './i18n';
import App from './App';
import '.';
@@ -18,8 +20,12 @@ jest.mock('react-dom', () => ({
jest.mock('@edx/frontend-platform', () => ({
APP_READY: 'app-is-ready-key',
initialize: jest.fn(),
mergeConfig: jest.fn(),
subscribe: jest.fn(),
}));
jest.mock('@edx/frontend-component-header', () => ({
messages: ['some', 'messages'],
}));
jest.mock('@edx/frontend-component-footer', () => ({
messages: ['some', 'messages'],
}));
@@ -42,10 +48,23 @@ describe('app registry', () => {
ReactDOM.render(<App />, document.getElementById('root')),
);
});
test('initialize is called with footerMessages and requireAuthenticatedUser', () => {
test('initialize is called with requireAuthenticatedUser, messages, and a config handler', () => {
expect(initialize).toHaveBeenCalledWith({
messages: [appMessages, footerMessages],
messages: [appMessages, headerMessages, footerMessages, paragonMessages],
requireAuthenticatedUser: true,
handlers: {
config: expect.any(Function),
},
});
});
test('initialize config loads LMS_BASE_URL from env', () => {
const oldEnv = process.env;
const initializeArg = initialize.mock.calls[0][0];
process.env = { ...oldEnv, LMS_BASE_URL: 'http://example.com/fake' };
initializeArg.handlers.config();
expect(mergeConfig).toHaveBeenCalledWith(
expect.objectContaining({ LMS_BASE_URL: 'http://example.com/fake' }),
);
process.env = oldEnv;
});
});

View File

@@ -1,6 +1,6 @@
// The code in this file is from Segment's website:
// https://segment.com/docs/sources/website/analytics.js/quickstart/
import { configuration } from './config';
import { getConfig } from '@edx/frontend-platform';
(function () {
// Create a queue, but don't obliterate an existing one!
@@ -81,5 +81,5 @@ import { configuration } from './config';
// Load Analytics.js with your key, which will automatically
// load the tools you've enabled for your account. Boosh!
analytics.load(configuration.SEGMENT_KEY);
analytics.load(getConfig().SEGMENT_KEY);
}());

View File

@@ -8,6 +8,8 @@ Enzyme.configure({ adapter: new Adapter() });
// These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing
process.env.LMS_BASE_URL = 'http://localhost:18000';
process.env.SITE_NAME = 'localhost';
process.env.FAVICON_URL = 'http://localhost:18000/favicon.ico';
jest.mock('@edx/frontend-platform/i18n', () => {
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
@@ -19,5 +21,6 @@ jest.mock('@edx/frontend-platform/i18n', () => {
}),
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
getLocale: jest.fn(),
};
});

187
src/testUtils.js Normal file
View File

@@ -0,0 +1,187 @@
import react from 'react';
import { StrictDict } from 'utils';
/**
* Mocked formatMessage provided by react-intl
*/
export const formatMessage = (msg, values) => {
let message = msg.defaultMessage;
if (values === undefined) {
return message;
}
Object.keys(values).forEach((key) => {
// eslint-disable-next-line
message = message.replace(`{${key}}`, values[key]);
});
return message;
};
/**
* Mock a single component, or a nested component so that its children render nicely
* in snapshots.
* @param {string} name - parent component name
* @param {obj} contents - object of child components with intended component
* render name.
* @return {func} - mock component with nested children.
*
* usage:
* mockNestedComponent('Card', { Body: 'Card.Body', Form: { Control: { Feedback: 'Form.Control.Feedback' }}... });
* mockNestedComponent('IconButton', 'IconButton');
*/
export const mockNestedComponent = (name, contents) => {
if (typeof contents !== 'object') {
return contents;
}
const fn = () => name;
Object.defineProperty(fn, 'name', { value: name });
Object.keys(contents).forEach((nestedName) => {
const value = contents[nestedName];
fn[nestedName] = typeof value !== 'object'
? value
: mockNestedComponent(`${name}.${nestedName}`, value);
});
return fn;
};
/**
* Mock a module of components. nested components will be rendered nicely in snapshots.
* @param {obj} mapping - component module mock config.
* @return {obj} - module of flat and nested components that will render nicely in snapshots.
* usage:
* mockNestedComponents({
* Card: { Body: 'Card.Body' },
* IconButton: 'IconButton',
* })
*/
export const mockNestedComponents = (mapping) => Object.entries(mapping).reduce(
(obj, [name, value]) => ({
...obj,
[name]: mockNestedComponent(name, value),
}),
{},
);
/**
* Mock utility for working with useState in a hooks module.
* Expects/requires an object containing the state object in order to ensure
* the mock behavior works appropriately.
*
* Expected format:
* hooks = { state: { <key>: (val) => React.createRef(val), ... } }
*
* Returns a utility for mocking useState and providing access to specific state values
* and setState methods, as well as allowing per-test configuration of useState value returns.
*
* Example usage:
* // hooks.js
* import * as module from './hooks';
* const state = {
* isOpen: (val) => React.useState(val),
* hasDoors: (val) => React.useState(val),
* selected: (val) => React.useState(val),
* };
* ...
* export const exampleHook = () => {
* const [isOpen, setIsOpen] = module.state.isOpen(false);
* if (!isOpen) { return null; }
* return { isOpen, setIsOpen };
* }
* ...
*
* // hooks.test.js
* import * as hooks from './hooks';
* const state = new MockUseState(hooks)
* ...
* describe('state hooks', () => {
* state.testGetter(state.keys.isOpen);
* state.testGetter(state.keys.hasDoors);
* state.testGetter(state.keys.selected);
* });
* describe('exampleHook', () => {
* beforeEach(() => { state.mock(); });
* it('returns null if isOpen is default value', () => {
* expect(hooks.exampleHook()).toEqual(null);
* });
* it('returns isOpen and setIsOpen if isOpen is not null', () => {
* state.mockVal(state.keys.isOpen, true);
* expect(hooks.exampleHook()).toEqual({
* isOpen: true,
* setIsOpen: state.setState[state.keys.isOpen],
* });
* });
* afterEach(() => { state.restore(); });
* });
*
* @param {obj} hooks - hooks module containing a 'state' object
*/
export class MockUseState {
constructor(hooks) {
this.hooks = hooks;
this.oldState = null;
this.setState = {};
this.stateVals = {};
this.mock = this.mock.bind(this);
this.restore = this.restore.bind(this);
this.mockVal = this.mockVal.bind(this);
this.testGetter = this.testGetter.bind(this);
}
/**
* @return {object} - StrictDict of state object keys
*/
get keys() {
return StrictDict(Object.keys(this.hooks.state).reduce(
(obj, key) => ({ ...obj, [key]: key }),
{},
));
}
/**
* Replace the hook module's state object with a mocked version, initialized to default values.
*/
mock() {
this.oldState = this.hooks.state;
Object.keys(this.keys).forEach(key => {
this.hooks.state[key] = jest.fn(val => {
this.stateVals[key] = val;
return [val, this.setState[key]];
});
});
this.setState = Object.keys(this.keys).reduce(
(obj, key) => ({
...obj,
[key]: jest.fn(val => {
this.hooks.state[key] = val;
}),
}),
{},
);
}
/**
* Restore the hook module's state object to the actual code.
*/
restore() {
this.hooks.state = this.oldState;
}
/**
* Mock the state getter associated with a single key to return a specific value one time.
* @param {string} key - state key (from this.keys)
* @param {any} val - new value to be returned by the useState call.
*/
mockVal(key, val) {
this.hooks.state[key].mockReturnValueOnce([val, this.setState[key]]);
}
testGetter(key) {
test(`${key} state getter should return useState passthrough`, () => {
const testValue = 'some value';
const useState = (val) => ({ useState: val });
jest.spyOn(react, 'useState').mockImplementationOnce(useState);
expect(this.hooks.state[key](testValue)).toEqual(useState(testValue));
});
}
}

View File

@@ -4,6 +4,10 @@ const strictGet = (target, name) => {
return target;
}
if (name === '$$typeof') {
return typeof target;
}
if (name in target || name === '_reactFragment') {
return target[name];
}

View File

@@ -45,6 +45,9 @@ describe('StrictDict', () => {
it('allows entry listing', () => {
expect(Object.entries(dict)).toEqual(Object.entries(rawDict));
});
it('allows $$typeof access', () => {
expect(dict.$$typeof).toEqual(typeof rawDict);
});
describe('missing key', () => {
it('logs error with target, name, and error stack', () => {
// eslint-ignore-next-line no-unused-vars