Compare commits
1 Commits
kdmccormic
...
aed/websoc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0aada7794 |
@@ -16,10 +16,7 @@
|
|||||||
"jsx-a11y/anchor-is-valid": [ "error", {
|
"jsx-a11y/anchor-is-valid": [ "error", {
|
||||||
"components": [ "Link" ],
|
"components": [ "Link" ],
|
||||||
"specialLink": [ "to" ]
|
"specialLink": [ "to" ]
|
||||||
}],
|
}]
|
||||||
// https://github.com/yannickcr/eslint-plugin-react/issues/1754#issuecomment-378838053
|
|
||||||
// tl;dr: this rule is no longer going to cause any user-facing visual weirdness, its original motivation
|
|
||||||
"react/no-did-mount-set-state": "off"
|
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"jest": true
|
"jest": true
|
||||||
|
|||||||
11
.travis.yml
11
.travis.yml
@@ -1,14 +1,21 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js: 12
|
node_js:
|
||||||
|
- lts/*
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- "~/.npm"
|
||||||
notifications:
|
notifications:
|
||||||
email:
|
email:
|
||||||
recipients:
|
recipients:
|
||||||
- masters-grades@edx.org
|
- adusenbery@edx.org
|
||||||
|
- rreilly@edx.org
|
||||||
|
- schen@edx.org
|
||||||
on_success: never
|
on_success: never
|
||||||
on_failure: always
|
on_failure: always
|
||||||
webhooks: https://www.travisbuddy.com/
|
webhooks: https://www.travisbuddy.com/
|
||||||
on_success: never
|
on_success: never
|
||||||
before_install:
|
before_install:
|
||||||
|
- npm install -g npm@latest
|
||||||
- npm install -g greenkeeper-lockfile@1.14.0
|
- npm install -g greenkeeper-lockfile@1.14.0
|
||||||
install:
|
install:
|
||||||
- npm ci
|
- npm ci
|
||||||
|
|||||||
149
LICENSE
149
LICENSE
@@ -1,21 +1,23 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
software and other kinds of works, specifically designed to ensure
|
software and other kinds of works.
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users.
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
To protect your rights, we need to prevent others from denying you
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
you this License which gives you legal permission to copy, distribute
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
and/or modify the software.
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
For example, if you distribute copies of such a program, whether
|
||||||
improvements made in alternate versions of the program, if they
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
receive widespread use, become available for other developers to
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
incorporate. Many developers of free software are heartened and
|
or can get the source code. And you must show them these terms so they
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
know their rights.
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
ensure that, in such cases, the modified source code becomes available
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
to the community. It requires the operator of a network server to
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
that there is no warranty for this free software. For both users' and
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
changed, so that their problems will not be attributed erroneously to
|
||||||
this license.
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -60,7 +72,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU General Public License into a single
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the work with which it is combined will remain governed by version
|
but the special requirements of the GNU Affero General Public License,
|
||||||
3 of the GNU General Public License.
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
the GNU General Public License from time to time. Such new versions will
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
Program specifies that a certain numbered version of the GNU General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
GNU General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as published
|
it under the terms of the GNU General Public License as published by
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
If the program does terminal interaction, make it output a short
|
||||||
network, you should also make sure that it provides a way for users to
|
notice like this when it starts in an interactive mode:
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
<program> Copyright (C) <year> <name of author>
|
||||||
of the code. There are many ways you could offer source, and different
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
solutions will be better for different programs; see section 13 for the
|
This is free software, and you are welcome to redistribute it
|
||||||
specific requirements.
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -1,7 +1,7 @@
|
|||||||
[](https://travis-ci.org/edx/frontend-app-gradebook) [](https://coveralls.io/github/edx/frontend-app-gradebook)
|
[](https://travis-ci.org/edx/gradebook) [](https://coveralls.io/github/edx/gradebook)
|
||||||
[](@edx/frontend-app-gradebook)
|
[](@edx/gradebook)
|
||||||
[](@edx/frontend-app-gradebook)
|
[](@edx/gradebook)
|
||||||
[](@edx/frontend-app-gradebook)
|
[](@edx/gradebook)
|
||||||
[](https://github.com/semantic-release/semantic-release)
|
[](https://github.com/semantic-release/semantic-release)
|
||||||
|
|
||||||
# gradebook
|
# gradebook
|
||||||
@@ -16,7 +16,7 @@ The front-end of our editable Gradebook feature.
|
|||||||
|
|
||||||
To install gradebook into your project:
|
To install gradebook into your project:
|
||||||
```
|
```
|
||||||
npm i --save @edx/frontend-app-gradebook
|
npm i --save @edx/gradebook
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running the UI Standalone
|
## Running the UI Standalone
|
||||||
|
|||||||
@@ -98,10 +98,10 @@ module.exports = Merge.smart(commonConfig, {
|
|||||||
}),
|
}),
|
||||||
new webpack.EnvironmentPlugin({
|
new webpack.EnvironmentPlugin({
|
||||||
NODE_ENV: 'development',
|
NODE_ENV: 'development',
|
||||||
BASE_URL: 'localhost:19000/gradebook',
|
BASE_URL: 'localhost:1994',
|
||||||
LMS_BASE_URL: 'http://localhost:18000',
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
LOGIN_URL: 'http://localhost:18000/login',
|
LOGIN_URL: 'http://localhost:18000/login',
|
||||||
LOGOUT_URL: 'http://localhost:18000/logout',
|
LOGOUT_URL: 'http://localhost:18000/login',
|
||||||
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
|
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
|
||||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
|
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
|
||||||
DATA_API_BASE_URL: 'http://localhost:8000',
|
DATA_API_BASE_URL: 'http://localhost:8000',
|
||||||
@@ -122,13 +122,10 @@ module.exports = Merge.smart(commonConfig, {
|
|||||||
TWITTER_URL: 'https://twitter.com',
|
TWITTER_URL: 'https://twitter.com',
|
||||||
YOU_TUBE_URL: 'https://www.youtube.com',
|
YOU_TUBE_URL: 'https://www.youtube.com',
|
||||||
LINKED_IN_URL: 'https://www.linkedin.com',
|
LINKED_IN_URL: 'https://www.linkedin.com',
|
||||||
|
GOOGLE_PLUS_URL: 'https://plus.google.com',
|
||||||
REDDIT_URL: 'https://www.reddit.com',
|
REDDIT_URL: 'https://www.reddit.com',
|
||||||
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
|
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
|
||||||
GOOGLE_PLAY_URL: 'https://play.google.com/store',
|
GOOGLE_PLAY_URL: 'https://play.google.com/store',
|
||||||
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',
|
|
||||||
}),
|
}),
|
||||||
// when the --hot option is not passed in as part of the command
|
// when the --hot option is not passed in as part of the command
|
||||||
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
|
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
|
||||||
|
|||||||
@@ -137,13 +137,10 @@ module.exports = Merge.smart(commonConfig, {
|
|||||||
TWITTER_URL: null,
|
TWITTER_URL: null,
|
||||||
YOU_TUBE_URL: null,
|
YOU_TUBE_URL: null,
|
||||||
LINKED_IN_URL: null,
|
LINKED_IN_URL: null,
|
||||||
|
GOOGLE_PLUS_URL: null,
|
||||||
REDDIT_URL: null,
|
REDDIT_URL: null,
|
||||||
APPLE_APP_STORE_URL: null,
|
APPLE_APP_STORE_URL: null,
|
||||||
GOOGLE_PLAY_URL: null,
|
GOOGLE_PLAY_URL: null,
|
||||||
ENTERPRISE_MARKETING_URL: null,
|
|
||||||
ENTERPRISE_MARKETING_UTM_SOURCE: null,
|
|
||||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN: null,
|
|
||||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: null,
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
nick: grbk
|
nick: grbk
|
||||||
oeps: {}
|
oeps: {}
|
||||||
owner: schenedx
|
owner: edx/educator-neem
|
||||||
supporting_teams:
|
|
||||||
- masters-devs
|
|
||||||
openedx-release: {ref: master}
|
openedx-release: {ref: master}
|
||||||
|
track-pulls: true
|
||||||
|
|||||||
7399
package-lock.json
generated
7399
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@edx/frontend-app-gradebook",
|
"name": "@edx/gradebook",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/edx/frontend-app-gradebook.git"
|
"url": "git+https://github.com/edx/gradebook.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "NODE_ENV=production BABEL_ENV=production webpack --config=config/webpack.prod.config.js",
|
"build": "NODE_ENV=production BABEL_ENV=production webpack --config=config/webpack.prod.config.js",
|
||||||
"dev-build": "NODE_ENV=development BABEL_ENV=development webpack --config=config/webpack.dev.config.js",
|
|
||||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||||
"is-es5": "es-check es5 ./dist/*.js",
|
"is-es5": "es-check es5 ./dist/*.js",
|
||||||
"lint": "eslint --ext .js --ext .jsx .",
|
"lint": "eslint --ext .js --ext .jsx .",
|
||||||
@@ -16,49 +15,43 @@
|
|||||||
"semantic-release": "semantic-release",
|
"semantic-release": "semantic-release",
|
||||||
"start": "NODE_ENV=development BABEL_ENV=development node_modules/.bin/webpack-dev-server --config=config/webpack.dev.config.js --progress",
|
"start": "NODE_ENV=development BABEL_ENV=development node_modules/.bin/webpack-dev-server --config=config/webpack.dev.config.js --progress",
|
||||||
"test": "jest --coverage --passWithNoTests",
|
"test": "jest --coverage --passWithNoTests",
|
||||||
"watch-tests": "jest --watch",
|
|
||||||
"travis-deploy-once": "travis-deploy-once"
|
"travis-deploy-once": "travis-deploy-once"
|
||||||
},
|
},
|
||||||
"author": "edX",
|
"author": "edX",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"homepage": "https://github.com/edx/frontend-app-gradebook#readme",
|
"homepage": "https://github.com/edx/gradebook#readme",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/edx-bootstrap": "^0.4.3",
|
"@edx/edx-bootstrap": "^0.4.3",
|
||||||
"@edx/frontend-auth": "^4.0.0",
|
"@edx/frontend-auth": "^1.3.0",
|
||||||
"@edx/frontend-component-footer": "^4.1.5",
|
"@edx/frontend-component-footer": "^1.0.0",
|
||||||
"@edx/paragon": "^7.1.5",
|
"@edx/paragon": "^3.8.3",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
|
||||||
"@fortawesome/react-fontawesome": "^0.1.5",
|
|
||||||
"@redux-beacon/segment": "^1.0.0",
|
"@redux-beacon/segment": "^1.0.0",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"email-prop-type": "^1.1.7",
|
"email-prop-type": "^1.1.7",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"history": "^4.10.1",
|
"history": "^4.7.2",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.6.2",
|
||||||
"query-string": "^5.1.1",
|
"query-string": "^5.1.1",
|
||||||
"react": "^16.10.1",
|
"react": "^16.7.0",
|
||||||
"react-dom": "^16.10.1",
|
"react-dom": "^16.7.0",
|
||||||
"react-intl": "^2.9.0",
|
|
||||||
"react-redux": "^5.1.1",
|
"react-redux": "^5.1.1",
|
||||||
"react-router": "^4.3.1",
|
"react-router": "^4.3.1",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
"react-router-redux": "^5.0.0-alpha.9",
|
"react-router-redux": "^5.0.0-alpha.9",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
"redux-beacon": "^2.1.0",
|
"redux-devtools-extension": "^2.13.7",
|
||||||
"redux-devtools-extension": "^2.13.8",
|
"redux-beacon": "^2.0.3",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"whatwg-fetch": "^2.0.4"
|
"whatwg-fetch": "^2.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^9.6.1",
|
"autoprefixer": "^9.4.5",
|
||||||
"axios-mock-adapter": "^1.17.0",
|
"axios-mock-adapter": "^1.16.0",
|
||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.26.0",
|
||||||
"babel-eslint": "^8.2.6",
|
"babel-eslint": "^8.2.6",
|
||||||
"babel-jest": "^22.4.4",
|
"babel-jest": "^22.4.4",
|
||||||
@@ -67,10 +60,10 @@
|
|||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
"babel-preset-env": "^1.7.0",
|
"babel-preset-env": "^1.7.0",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
"codecov": "^3.6.1",
|
"codecov": "^3.1.0",
|
||||||
"css-loader": "^0.28.11",
|
"css-loader": "^0.28.11",
|
||||||
"enzyme": "^3.10.0",
|
"enzyme": "^3.8.0",
|
||||||
"enzyme-adapter-react-16": "^1.14.0",
|
"enzyme-adapter-react-16": "^1.7.1",
|
||||||
"es-check": "^2.3.0",
|
"es-check": "^2.3.0",
|
||||||
"eslint-config-edx": "^4.0.4",
|
"eslint-config-edx": "^4.0.4",
|
||||||
"fetch-mock": "^6.5.2",
|
"fetch-mock": "^6.5.2",
|
||||||
@@ -82,19 +75,19 @@
|
|||||||
"image-webpack-loader": "^4.2.0",
|
"image-webpack-loader": "^4.2.0",
|
||||||
"jest": "^22.4.4",
|
"jest": "^22.4.4",
|
||||||
"mini-css-extract-plugin": "^0.4.0",
|
"mini-css-extract-plugin": "^0.4.0",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.11.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"react-dev-utils": "^5.0.3",
|
"react-dev-utils": "^5.0.3",
|
||||||
"react-test-renderer": "^16.10.1",
|
"react-test-renderer": "^16.7.0",
|
||||||
"redux-mock-store": "^1.5.3",
|
"redux-mock-store": "^1.5.3",
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
"semantic-release": "^15.13.24",
|
"semantic-release": "^15.13.3",
|
||||||
"style-loader": "^0.20.3",
|
"style-loader": "^0.20.3",
|
||||||
"travis-deploy-once": "^5.0.11",
|
"travis-deploy-once": "^5.0.11",
|
||||||
"webpack": "^4.41.0",
|
"webpack": "^4.28.4",
|
||||||
"webpack-cli": "^3.3.9",
|
"webpack-cli": "^3.2.1",
|
||||||
"webpack-dev-server": "^3.8.2",
|
"webpack-dev-server": "^3.1.14",
|
||||||
"webpack-merge": "^4.2.2"
|
"webpack-merge": "^4.2.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
@import "~@edx/edx-bootstrap/sass/edx/theme";
|
||||||
@import "~@edx/paragon/scss/edx/theme.scss";
|
@import "~bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
$fa-font-path: "~font-awesome/fonts";
|
$fa-font-path: "~font-awesome/fonts";
|
||||||
@import "~font-awesome/scss/font-awesome";
|
@import "~font-awesome/scss/font-awesome";
|
||||||
|
|
||||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
@import "~@edx/paragon/src/SearchField/SearchField";
|
||||||
|
|
||||||
@import "~@edx/frontend-component-footer/src/lib/scss/site-footer";
|
@import "~@edx/frontend-component-footer/src/lib/scss/site-footer";
|
||||||
|
|
||||||
@import "./components/Gradebook/gradebook";
|
@import "./components/Gradebook/gradebook";
|
||||||
@import "./components/Drawer/Drawer";
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
$drawer-width: 350px;
|
|
||||||
|
|
||||||
.drawer-contents {
|
|
||||||
overflow-x: auto;
|
|
||||||
transition: margin 300ms cubic-bezier(0.4,0,0.2,1);
|
|
||||||
margin-left: 0;
|
|
||||||
.drawer.open + & {
|
|
||||||
margin-left: $drawer-width;
|
|
||||||
}
|
|
||||||
&.opened {
|
|
||||||
width: calc(100vw - #{$drawer-width});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-contents {
|
|
||||||
overflow-x: auto;
|
|
||||||
transition: margin 300ms cubic-bezier(0.4,0,0.2,1);
|
|
||||||
margin-left: 0;
|
|
||||||
.drawer.open + & {
|
|
||||||
margin-left: $drawer-width;
|
|
||||||
}
|
|
||||||
&.opened {
|
|
||||||
width: calc(100vw - #{$drawer-width});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-container .collapsible {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer {
|
|
||||||
height: 100%;
|
|
||||||
width: $drawer-width;
|
|
||||||
position: absolute;
|
|
||||||
transform: translateX(-$drawer-width);
|
|
||||||
flex-direction: column;
|
|
||||||
transition: transform 300ms cubic-bezier(0.4,0,0.2,1);
|
|
||||||
&.open {
|
|
||||||
transform: translateX(0%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Button } from '@edx/paragon';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
export default class Drawer extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
open: props.initiallyOpen,
|
|
||||||
transitioning: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
deferToNextRepaint(callback) {
|
|
||||||
window.requestAnimationFrame(() =>
|
|
||||||
window.setTimeout(callback, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
close = () => {
|
|
||||||
if (this.state.open) {
|
|
||||||
this.toggleOpen();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleOpen = () => {
|
|
||||||
this.setState({ transitioning: true });
|
|
||||||
// defer the transition to the next repaint so we can be sure that
|
|
||||||
// opening drawer is visible before it transitions
|
|
||||||
// (the start state of the opening animation doesn't work if the element starts hidden)
|
|
||||||
this.deferToNextRepaint(() => this.setState(prevState => ({ open: !prevState.open })));
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSlideDone = (e) => {
|
|
||||||
if (e.currentTarget === e.target) {
|
|
||||||
this.setState({ transitioning: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="d-flex drawer-container">
|
|
||||||
<aside
|
|
||||||
className={classNames(
|
|
||||||
'drawer',
|
|
||||||
{
|
|
||||||
open: this.state.open,
|
|
||||||
'd-none': !this.state.transitioning && !this.state.open,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
onTransitionEnd={this.handleSlideDone}
|
|
||||||
>
|
|
||||||
<div className="drawer-header">
|
|
||||||
<h2>{this.props.title}</h2>
|
|
||||||
<Button
|
|
||||||
className="p-1"
|
|
||||||
onClick={this.close}
|
|
||||||
aria-label="Close Filters"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTimes} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{this.props.children}
|
|
||||||
</aside>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'drawer-contents',
|
|
||||||
'position-relative',
|
|
||||||
!this.state.drawerTransitioning && this.state.drawerOpen && 'opened',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{this.props.mainContent(this.toggleOpen)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Drawer.propTypes = {
|
|
||||||
initiallyOpen: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
mainContent: PropTypes.func.isRequired,
|
|
||||||
title: PropTypes.node.isRequired,
|
|
||||||
};
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import initialFilters from '../../data/constants/filters';
|
|
||||||
|
|
||||||
function FilterBadge({ name, value, onClick }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<span className="badge badge-info">
|
|
||||||
<span>{`${name}: ${value}`}</span>
|
|
||||||
<button type="button" className="btn-info" aria-label="Close" onClick={onClick}>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RangeFilterBadge({
|
|
||||||
displayName,
|
|
||||||
filterName1,
|
|
||||||
filterValue1,
|
|
||||||
filterName2,
|
|
||||||
filterValue2,
|
|
||||||
handleBadgeClose,
|
|
||||||
}) {
|
|
||||||
return ((filterValue1 !== initialFilters[filterName1]) ||
|
|
||||||
(filterValue2 !== initialFilters[filterName2]))
|
|
||||||
&&
|
|
||||||
<FilterBadge
|
|
||||||
name={displayName}
|
|
||||||
value={`${filterValue1} - ${filterValue2}`}
|
|
||||||
onClick={handleBadgeClose}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function SingleValueFilterBadge({
|
|
||||||
displayName, filterName, filterValue, handleBadgeClose,
|
|
||||||
}) {
|
|
||||||
return (filterValue !== initialFilters[filterName]) &&
|
|
||||||
<FilterBadge
|
|
||||||
name={displayName}
|
|
||||||
value={filterValue}
|
|
||||||
onClick={handleBadgeClose}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterBadges({
|
|
||||||
assignment,
|
|
||||||
assignmentType,
|
|
||||||
track,
|
|
||||||
cohort,
|
|
||||||
assignmentGradeMin,
|
|
||||||
assignmentGradeMax,
|
|
||||||
courseGradeMin,
|
|
||||||
courseGradeMax,
|
|
||||||
handleFilterBadgeClose,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SingleValueFilterBadge
|
|
||||||
displayName="Assignment Type"
|
|
||||||
filterName="assignmentType"
|
|
||||||
filterValue={assignmentType}
|
|
||||||
handleBadgeClose={handleFilterBadgeClose(['assignmentType'])}
|
|
||||||
/>
|
|
||||||
<SingleValueFilterBadge
|
|
||||||
displayName="Assignment"
|
|
||||||
filterName="assignment"
|
|
||||||
filterValue={assignment}
|
|
||||||
handleBadgeClose={handleFilterBadgeClose(['assignment', 'assignmentGradeMax', 'assignmentGradeMin'])}
|
|
||||||
/>
|
|
||||||
<RangeFilterBadge
|
|
||||||
displayName="Assignment Grade"
|
|
||||||
filterName1="assignmentGradeMin"
|
|
||||||
filterValue1={assignmentGradeMin}
|
|
||||||
filterName2="assignmentGradeMax"
|
|
||||||
filterValue2={assignmentGradeMax}
|
|
||||||
handleBadgeClose={handleFilterBadgeClose(['assignmentGradeMin', 'assignmentGradeMax'])}
|
|
||||||
/>
|
|
||||||
<RangeFilterBadge
|
|
||||||
displayName="Course Grade"
|
|
||||||
filterName1="courseGradeMin"
|
|
||||||
filterValue1={courseGradeMin}
|
|
||||||
filterName2="courseGradeMax"
|
|
||||||
filterValue2={courseGradeMax}
|
|
||||||
handleBadgeClose={handleFilterBadgeClose(['courseGradeMin', 'courseGradeMax'])}
|
|
||||||
/>
|
|
||||||
<SingleValueFilterBadge
|
|
||||||
displayName="Track"
|
|
||||||
filterName="track"
|
|
||||||
filterValue={track}
|
|
||||||
handleBadgeClose={handleFilterBadgeClose(['track'])}
|
|
||||||
/>
|
|
||||||
<SingleValueFilterBadge
|
|
||||||
displayName="Cohort"
|
|
||||||
filterName="track"
|
|
||||||
filterValue={cohort}
|
|
||||||
handleBadgeClose={handleFilterBadgeClose(['cohort'])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => (
|
|
||||||
{
|
|
||||||
assignment: (state.filters.assignment || {}).label,
|
|
||||||
assignmentType: state.filters.assignmentType,
|
|
||||||
track: state.filters.track,
|
|
||||||
cohort: state.filters.cohort,
|
|
||||||
assignmentGradeMin: state.filters.assignmentGradeMin,
|
|
||||||
assignmentGradeMax: state.filters.assignmentGradeMax,
|
|
||||||
courseGradeMin: state.filters.courseGradeMin,
|
|
||||||
courseGradeMax: state.filters.courseGradeMax,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const ConnectedFilterBadges = connect(mapStateToProps)(FilterBadges);
|
|
||||||
export default ConnectedFilterBadges;
|
|
||||||
|
|
||||||
FilterBadge.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
FilterBadges.defaultProps = {
|
|
||||||
assignment: initialFilters.assignmentType,
|
|
||||||
assignmentType: initialFilters.assignmentType,
|
|
||||||
track: initialFilters.track,
|
|
||||||
cohort: initialFilters.cohort,
|
|
||||||
assignmentGradeMin: initialFilters.assignmentGradeMin,
|
|
||||||
assignmentGradeMax: initialFilters.assignmentGradeMax,
|
|
||||||
courseGradeMin: initialFilters.courseGradeMin,
|
|
||||||
courseGradeMax: initialFilters.courseGradeMax,
|
|
||||||
};
|
|
||||||
|
|
||||||
FilterBadges.propTypes = {
|
|
||||||
assignment: PropTypes.string,
|
|
||||||
assignmentType: PropTypes.string,
|
|
||||||
track: PropTypes.string,
|
|
||||||
cohort: PropTypes.string,
|
|
||||||
assignmentGradeMin: PropTypes.string,
|
|
||||||
assignmentGradeMax: PropTypes.string,
|
|
||||||
courseGradeMin: PropTypes.string,
|
|
||||||
courseGradeMax: PropTypes.string,
|
|
||||||
handleFilterBadgeClose: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
|
||||||
background-color: #999;
|
background-color: #999;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
z-index: 99999;
|
z-index: 99999;
|
||||||
@@ -17,16 +16,16 @@
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gradebook-content {
|
.gradebook-container{
|
||||||
// note that this width isn't well-abstracted from Drawer code.
|
width: 500px;
|
||||||
// if we need to change it we may need to dig into those styles as well
|
@media only screen and (min-width: 640px) {
|
||||||
width: 100vw;
|
width: 630px;
|
||||||
.search-help-text {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
}
|
||||||
h4 {
|
@media only screen and (min-width: 992px) {
|
||||||
font-weight: bold;
|
width: 900px;
|
||||||
margin-top: 2rem;
|
}
|
||||||
|
@media only screen and (min-width: 1200px) {
|
||||||
|
width: 1024px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,60 +38,34 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.grade-history-header{
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grade-history-assignment{
|
|
||||||
padding-right: 49px;
|
|
||||||
}
|
|
||||||
.grade-history-student{
|
|
||||||
padding-right: 75px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grade-history-original-grade{
|
|
||||||
padding-right: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grade-history-current-grade{
|
|
||||||
padding-right: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gbook {
|
.gbook {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
|
|
||||||
.grade-button {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.student-key {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
padding-left: 244px;
|
padding-left: 244px;
|
||||||
// prevents the table from shrinking to a width where "Final 01" breaks to two lines
|
|
||||||
min-width: 731px;
|
|
||||||
th {
|
|
||||||
vertical-align: top;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table thead tr {
|
|
||||||
height: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table tr th:first-child {
|
.table tr th:first-child {
|
||||||
|
position: absolute;
|
||||||
|
width: 160px;
|
||||||
|
height:50px;
|
||||||
|
display: block;
|
||||||
|
background-color: #fff;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.table tr th:first-child,
|
|
||||||
.table tr td:first-child {
|
.table tr td:first-child {
|
||||||
position: sticky;
|
position: absolute;
|
||||||
width: 160px;
|
width: 160px;
|
||||||
left: 0;
|
height:50px;
|
||||||
z-index: 1; // to float over the following children in the side-scrolling case
|
display: block;
|
||||||
background: white;
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.table tr td:nth-child(2) {
|
||||||
|
box-sizing: content-box;
|
||||||
|
padding-left: 170px;
|
||||||
|
}
|
||||||
|
.table tr th:nth-child(2) {
|
||||||
|
padding-left: 170px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table tbody th {
|
.table tbody th {
|
||||||
@@ -108,19 +81,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-percent-label {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-85 {
|
.mb-85 {
|
||||||
margin-bottom: 85px;
|
margin-bottom: 85px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-dialog {
|
|
||||||
max-width: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap-text-in-cell {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,145 +2,102 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Collapsible,
|
|
||||||
Icon,
|
|
||||||
InputSelect,
|
InputSelect,
|
||||||
InputText,
|
|
||||||
Modal,
|
Modal,
|
||||||
SearchField,
|
SearchField,
|
||||||
StatefulButton,
|
|
||||||
StatusAlert,
|
StatusAlert,
|
||||||
Table,
|
Table,
|
||||||
Tabs,
|
Icon,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faDownload, faSpinner, faFilter } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { configuration } from '../../config';
|
import { configuration } from '../../config';
|
||||||
import PageButtons from '../PageButtons';
|
import PageButtons from '../PageButtons';
|
||||||
import Drawer from '../Drawer';
|
|
||||||
import { formatDateForDisplay } from '../../data/actions/utils';
|
|
||||||
import initialFilters from '../../data/constants/filters';
|
|
||||||
import ConnectedFilterBadges from '../FilterBadges';
|
|
||||||
|
|
||||||
|
|
||||||
const DECIMAL_PRECISION = 2;
|
const DECIMAL_PRECISION = 2;
|
||||||
const GRADE_OVERRIDE_HISTORY_COLUMNS = [{ label: 'Date', key: 'date' }, { label: 'Grader', key: 'grader' },
|
|
||||||
{ label: 'Reason', key: 'reason' },
|
|
||||||
{ label: 'Adjusted grade', key: 'adjustedGrade' }];
|
|
||||||
|
|
||||||
export default class Gradebook extends React.Component {
|
export default class Gradebook extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
filterValue: '',
|
filterValue: '',
|
||||||
courseGradeMin: '0',
|
|
||||||
courseGradeMax: '100',
|
|
||||||
modalOpen: false,
|
modalOpen: false,
|
||||||
adjustedGradeValue: 0,
|
modalModel: [{}],
|
||||||
|
updateVal: 0,
|
||||||
updateModuleId: null,
|
updateModuleId: null,
|
||||||
updateUserId: null,
|
updateUserId: null,
|
||||||
reasonForChange: '',
|
socket: null,
|
||||||
assignmentGradeMin: '0',
|
websocketMsg: {
|
||||||
assignmentGradeMax: '100',
|
visible: false,
|
||||||
isMinCourseGradeFilterValid: true,
|
},
|
||||||
isMaxCourseGradeFilterValid: true,
|
|
||||||
};
|
};
|
||||||
this.fileFormRef = React.createRef();
|
|
||||||
this.fileInputRef = React.createRef();
|
|
||||||
this.myRef = React.createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const urlQuery = queryString.parse(this.props.location.search);
|
const urlQuery = queryString.parse(this.props.location.search);
|
||||||
this.props.initializeFilters(urlQuery);
|
this.props.getRoles(this.props.match.params.courseId, urlQuery);
|
||||||
this.props.getRoles(this.props.courseId);
|
const socket = new WebSocket('ws://localhost:8765/ws/gradebook/course-v1:edX+DemoX+Demo_Course/');
|
||||||
this.overrideReasonInput.focus();
|
socket.onmessage = this.socketMessageFunction;
|
||||||
|
|
||||||
const newStateFields = {};
|
|
||||||
['assignmentGradeMin', 'assignmentGradeMax', 'courseGradeMin', 'courseGradeMax'].forEach((attr) => {
|
|
||||||
if (urlQuery[attr]) {
|
|
||||||
newStateFields[attr] = urlQuery[attr];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState(newStateFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(e) {
|
socketMessageFunction = (event) => {
|
||||||
this.setState({ [e.target.name]: e.target.value });
|
var data = JSON.parse(event.data);
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
const userIndex = this.props.grades.findIndex((entry) => entry.user_id == data.user_id);
|
||||||
|
const username = this.props.grades[userIndex].username;
|
||||||
|
const subsectionIndex = this.props.grades[userIndex].section_breakdown.findIndex((entry) => entry.module_id = data.subsection_id);
|
||||||
|
const subsectionName = this.props.grades[userIndex].section_breakdown[subsectionIndex].label;
|
||||||
|
|
||||||
|
let subsectionGrade = this.props.grades[userIndex].section_breakdown[subsectionIndex];
|
||||||
|
subsectionGrade.score_earned = data.override.earned_graded_override;
|
||||||
|
subsectionGrade.score_possible = data.override.possible_graded_override;
|
||||||
|
|
||||||
|
const updatedMsg = {
|
||||||
|
visible: true,
|
||||||
|
username: username,
|
||||||
|
subsectionName: subsectionName,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({ websocketMsg: updatedMsg });
|
||||||
|
this.props.gradeUpdateSuccess(this.props.match.params.courseId, this.props.grades);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNewModalState = (userEntry, subsection) => {
|
setNewModalState = (userEntry, subsection) => {
|
||||||
this.props.fetchGradeOverrideHistory(
|
|
||||||
subsection.module_id,
|
|
||||||
userEntry.user_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
let adjustedGradePossible = '';
|
let adjustedGradePossible = '';
|
||||||
|
let currentGradePossible = '';
|
||||||
if (subsection.attempted) {
|
if (subsection.attempted) {
|
||||||
adjustedGradePossible = subsection.score_possible;
|
adjustedGradePossible = ` / ${subsection.score_possible}`;
|
||||||
|
currentGradePossible = `/${subsection.score_possible}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
modalAssignmentName: `${subsection.subsection_name}`,
|
modalModel: [{
|
||||||
|
username: userEntry.username,
|
||||||
|
currentGrade: `${subsection.score_earned}${currentGradePossible}`,
|
||||||
|
adjustedGrade: (
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
style={{ width: '25px' }}
|
||||||
|
type="text"
|
||||||
|
onChange={event => this.setState({ updateVal: event.target.value })}
|
||||||
|
/>{adjustedGradePossible}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
assignmentName: `${subsection.subsection_name}`,
|
||||||
|
}],
|
||||||
modalOpen: true,
|
modalOpen: true,
|
||||||
updateModuleId: subsection.module_id,
|
updateModuleId: subsection.module_id,
|
||||||
updateUserId: userEntry.user_id,
|
updateUserId: userEntry.user_id,
|
||||||
updateUserName: userEntry.username,
|
|
||||||
todaysDate: formatDateForDisplay(new Date()),
|
|
||||||
adjustedGradePossible,
|
|
||||||
reasonForChange: '',
|
|
||||||
adjustedGradeValue: '',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getLearnerInformation = entry => (
|
|
||||||
<div>
|
|
||||||
<div>{entry.username}</div>
|
|
||||||
{entry.external_user_key && <div className="student-key">{entry.external_user_key}</div>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
getActiveTabs = () => {
|
|
||||||
if (this.props.showBulkManagement) {
|
|
||||||
return ['Grades', 'Bulk Management'];
|
|
||||||
}
|
|
||||||
return ['Grades'];
|
|
||||||
};
|
|
||||||
|
|
||||||
getAssignmentFilterOptions = () => [
|
|
||||||
{ label: 'All', value: '' },
|
|
||||||
...this.props.assignmentFilterOptions.map((assignment) => {
|
|
||||||
const { label, subsectionLabel } = assignment;
|
|
||||||
return {
|
|
||||||
label: `${label}: ${subsectionLabel}`,
|
|
||||||
value: label,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
getCourseGradeFilterAlertDialog = () => {
|
|
||||||
let dialog = '';
|
|
||||||
|
|
||||||
if (!this.state.isMinCourseGradeFilterValid) {
|
|
||||||
dialog += 'Minimum course grade value must be between 0 and 100. ';
|
|
||||||
}
|
|
||||||
if (!this.state.isMaxCourseGradeFilterValid) {
|
|
||||||
dialog += 'Maximum course grade value must be between 0 and 100. ';
|
|
||||||
}
|
|
||||||
return dialog;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAdjustedGradeClick = () => {
|
handleAdjustedGradeClick = () => {
|
||||||
this.props.updateGrades(
|
this.props.updateGrades(
|
||||||
this.props.courseId, [
|
this.props.match.params.courseId, [
|
||||||
{
|
{
|
||||||
user_id: this.state.updateUserId,
|
user_id: this.state.updateUserId,
|
||||||
usage_id: this.state.updateModuleId,
|
usage_id: this.state.updateModuleId,
|
||||||
grade: {
|
grade: {
|
||||||
earned_graded_override: this.state.adjustedGradeValue,
|
earned_graded_override: this.state.updateVal,
|
||||||
comment: this.state.reasonForChange,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -149,46 +106,18 @@ export default class Gradebook extends React.Component {
|
|||||||
this.props.selectedTrack,
|
this.props.selectedTrack,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.closeAssignmentModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAssignmentModal = () => {
|
|
||||||
this.props.doneViewingAssignment();
|
|
||||||
this.setState({
|
this.setState({
|
||||||
adjustedGradePossible: '',
|
modalModel: [{}],
|
||||||
adjustedGradeValue: '',
|
|
||||||
modalOpen: false,
|
modalOpen: false,
|
||||||
reasonForChange: '',
|
|
||||||
updateModuleId: null,
|
updateModuleId: null,
|
||||||
updateUserId: null,
|
updateUserId: null,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
handleAssignmentFilterChange = (assignment) => {
|
|
||||||
const selectedFilterOption = this.props.assignmentFilterOptions.find(assig =>
|
|
||||||
assig.label === assignment);
|
|
||||||
const { type, id } = selectedFilterOption || {};
|
|
||||||
const typedValue = { label: assignment, type, id };
|
|
||||||
this.props.updateAssignmentFilter(typedValue);
|
|
||||||
this.updateQueryParams({ assignment: id });
|
|
||||||
this.props.updateGradesIfAssignmentGradeFiltersSet(
|
|
||||||
this.props.courseId,
|
|
||||||
this.props.selectedCohort,
|
|
||||||
this.props.selectedTrack,
|
|
||||||
this.props.selectedAssignmentType,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateQueryParams = (queryParams) => {
|
|
||||||
const parsed = queryString.parse(this.props.location.search);
|
|
||||||
Object.keys(queryParams).forEach((key) => {
|
|
||||||
if (queryParams[key]) {
|
|
||||||
parsed[key] = queryParams[key];
|
|
||||||
} else {
|
|
||||||
delete parsed[key];
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
this.props.history.push(`?${queryString.stringify(parsed)}`);
|
updateQueryParams = (queryKey, queryValue) => {
|
||||||
|
const parsed = queryString.parse(this.props.location.search);
|
||||||
|
parsed[queryKey] = queryValue;
|
||||||
|
return `?${queryString.stringify(parsed)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
mapAssignmentTypeEntries = (entries) => {
|
mapAssignmentTypeEntries = (entries) => {
|
||||||
@@ -196,7 +125,7 @@ export default class Gradebook extends React.Component {
|
|||||||
id: entry,
|
id: entry,
|
||||||
label: entry,
|
label: entry,
|
||||||
}));
|
}));
|
||||||
mapped.unshift({ id: 0, label: 'All', value: '' });
|
mapped.unshift({ id: 0, label: 'All' });
|
||||||
return mapped;
|
return mapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -218,56 +147,10 @@ export default class Gradebook extends React.Component {
|
|||||||
return mapped;
|
return mapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
formatHistoryRow = (row) => {
|
updateAssignmentTypes = (event) => {
|
||||||
const {
|
this.props.filterColumns(event, this.props.grades[0]);
|
||||||
summaryOfRowsProcessed: {
|
const updatedQueryStrings = this.updateQueryParams('assignmentType', event);
|
||||||
total,
|
this.props.history.push(updatedQueryStrings);
|
||||||
successfullyProcessed,
|
|
||||||
failed,
|
|
||||||
skipped,
|
|
||||||
},
|
|
||||||
unique_id: courseId,
|
|
||||||
originalFilename,
|
|
||||||
id,
|
|
||||||
user: username,
|
|
||||||
...rest
|
|
||||||
} = row;
|
|
||||||
const resultsText = [
|
|
||||||
`${total} Students: ${successfullyProcessed} processed`,
|
|
||||||
...(skipped > 0 ? [`${skipped} skipped`] : []),
|
|
||||||
...(failed > 0 ? [`${failed} failed`] : []),
|
|
||||||
].join(', ');
|
|
||||||
const resultsSummary = (
|
|
||||||
<a
|
|
||||||
href={`${configuration.LMS_BASE_URL}/api/bulk_grades/course/${courseId}/?error_id=${id}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faDownload} />
|
|
||||||
{resultsText}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
const filename = (
|
|
||||||
<span className="wrap-text-in-cell">
|
|
||||||
{originalFilename}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
const user = (
|
|
||||||
<span className="wrap-text-in-cell">
|
|
||||||
{username}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
resultsSummary,
|
|
||||||
filename,
|
|
||||||
user,
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
updateAssignmentTypes = (assignmentType) => {
|
|
||||||
this.props.filterAssignmentType(assignmentType);
|
|
||||||
this.updateQueryParams({ assignmentType });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTracks = (event) => {
|
updateTracks = (event) => {
|
||||||
@@ -277,12 +160,13 @@ export default class Gradebook extends React.Component {
|
|||||||
selectedTrackSlug = selectedTrackItem.slug;
|
selectedTrackSlug = selectedTrackItem.slug;
|
||||||
}
|
}
|
||||||
this.props.getUserGrades(
|
this.props.getUserGrades(
|
||||||
this.props.courseId,
|
this.props.match.params.courseId,
|
||||||
this.props.selectedCohort,
|
this.props.selectedCohort,
|
||||||
selectedTrackSlug,
|
selectedTrackSlug,
|
||||||
this.props.selectedAssignmentType,
|
this.props.selectedAssignmentType,
|
||||||
);
|
);
|
||||||
this.updateQueryParams({ track: selectedTrackSlug });
|
const updatedQueryStrings = this.updateQueryParams('track', selectedTrackSlug);
|
||||||
|
this.props.history.push(updatedQueryStrings);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateCohorts = (event) => {
|
updateCohorts = (event) => {
|
||||||
@@ -292,68 +176,14 @@ export default class Gradebook extends React.Component {
|
|||||||
selectedCohortId = selectedCohortItem.id;
|
selectedCohortId = selectedCohortItem.id;
|
||||||
}
|
}
|
||||||
this.props.getUserGrades(
|
this.props.getUserGrades(
|
||||||
this.props.courseId,
|
this.props.match.params.courseId,
|
||||||
selectedCohortId,
|
selectedCohortId,
|
||||||
this.props.selectedTrack,
|
this.props.selectedTrack,
|
||||||
this.props.selectedAssignmentType,
|
this.props.selectedAssignmentType,
|
||||||
);
|
);
|
||||||
this.updateQueryParams({ cohort: selectedCohortId });
|
this.updateQueryParams('cohort', selectedCohortId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// At present, we don't store label and value in google analytics. By setting the label
|
|
||||||
// property of the below events, I want to verify that we can set the label of google anlatyics
|
|
||||||
// The following properties of a google analytics event are:
|
|
||||||
// category (used), name(used), lavel(not used), value(not used)
|
|
||||||
handleClickExportGrades = () => {
|
|
||||||
this.props.downloadBulkGradesReport(this.props.courseId);
|
|
||||||
window.location = this.props.gradeExportUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClickDownloadInterventions = () => {
|
|
||||||
this.props.downloadInterventionReport(this.props.courseId);
|
|
||||||
window.location = this.props.interventionExportUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClickImportGrades = () => {
|
|
||||||
const fileInput = this.fileInputRef.current;
|
|
||||||
if (fileInput) {
|
|
||||||
fileInput.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFileInputChange = (event) => {
|
|
||||||
const fileInput = event.target;
|
|
||||||
const file = fileInput.files[0];
|
|
||||||
const form = this.fileFormRef.current;
|
|
||||||
if (file && form) {
|
|
||||||
const formData = new FormData(form);
|
|
||||||
this.props.submitFileUploadFormData(this.props.courseId, formData).then(() => {
|
|
||||||
fileInput.value = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSubmitAssignmentGrade = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const {
|
|
||||||
assignmentGradeMin,
|
|
||||||
assignmentGradeMax,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
this.props.updateAssignmentLimits(assignmentGradeMin, assignmentGradeMax);
|
|
||||||
this.props.getUserGrades(
|
|
||||||
this.props.courseId,
|
|
||||||
this.props.selectedCohort,
|
|
||||||
this.props.selectedTrack,
|
|
||||||
this.props.selectedAssignmentType,
|
|
||||||
);
|
|
||||||
this.updateQueryParams({ assignmentGradeMin, assignmentGradeMax });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMinAssigGradeChange = assignmentGradeMin => this.setState({ assignmentGradeMin });
|
|
||||||
|
|
||||||
handleMaxAssigGradeChange = assignmentGradeMax => this.setState({ assignmentGradeMax });
|
|
||||||
|
|
||||||
mapSelectedCohortEntry = (entry) => {
|
mapSelectedCohortEntry = (entry) => {
|
||||||
const selectedCohortEntry = this.props.cohorts.find(x => x.id === parseInt(entry, 10));
|
const selectedCohortEntry = this.props.cohorts.find(x => x.id === parseInt(entry, 10));
|
||||||
if (selectedCohortEntry) {
|
if (selectedCohortEntry) {
|
||||||
@@ -374,16 +204,7 @@ export default class Gradebook extends React.Component {
|
|||||||
|
|
||||||
formatter = {
|
formatter = {
|
||||||
percent: (entries, areGradesFrozen) => entries.map((entry) => {
|
percent: (entries, areGradesFrozen) => entries.map((entry) => {
|
||||||
const learnerInformation = this.getLearnerInformation(entry);
|
const results = { username: entry.username };
|
||||||
const results = {
|
|
||||||
Username: (
|
|
||||||
<div><span className="wrap-text-in-cell">{learnerInformation}</span></div>
|
|
||||||
),
|
|
||||||
Email: (
|
|
||||||
<span className="wrap-text-in-cell">{entry.email}</span>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const assignments = entry.section_breakdown
|
const assignments = entry.section_breakdown
|
||||||
.reduce((acc, subsection) => {
|
.reduce((acc, subsection) => {
|
||||||
if (areGradesFrozen) {
|
if (areGradesFrozen) {
|
||||||
@@ -391,7 +212,7 @@ export default class Gradebook extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
acc[subsection.label] = (
|
acc[subsection.label] = (
|
||||||
<button
|
<button
|
||||||
className="btn btn-header link-style grade-button"
|
className="btn btn-header link-style"
|
||||||
onClick={() => this.setNewModalState(entry, subsection)}
|
onClick={() => this.setNewModalState(entry, subsection)}
|
||||||
>
|
>
|
||||||
{this.roundGrade(subsection.percent * 100)}%
|
{this.roundGrade(subsection.percent * 100)}%
|
||||||
@@ -399,21 +220,12 @@ export default class Gradebook extends React.Component {
|
|||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
const totals = { Total: `${this.roundGrade(entry.percent * 100)}%` };
|
const totals = { total: `${this.roundGrade(entry.percent * 100)}%` };
|
||||||
return Object.assign(results, assignments, totals);
|
return Object.assign(results, assignments, totals);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
absolute: (entries, areGradesFrozen) => entries.map((entry) => {
|
absolute: (entries, areGradesFrozen) => entries.map((entry) => {
|
||||||
const learnerInformation = this.getLearnerInformation(entry);
|
const results = { username: entry.username };
|
||||||
const results = {
|
|
||||||
Username: (
|
|
||||||
<div><span className="wrap-text-in-cell">{learnerInformation}</span></div>
|
|
||||||
),
|
|
||||||
Email: (
|
|
||||||
<span className="wrap-text-in-cell">{entry.email}</span>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const assignments = entry.section_breakdown
|
const assignments = entry.section_breakdown
|
||||||
.reduce((acc, subsection) => {
|
.reduce((acc, subsection) => {
|
||||||
const scoreEarned = this.roundGrade(subsection.score_earned);
|
const scoreEarned = this.roundGrade(subsection.score_earned);
|
||||||
@@ -437,122 +249,27 @@ export default class Gradebook extends React.Component {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const totals = { Total: `${this.roundGrade(entry.percent * 100)}/100` };
|
const totals = { total: `${this.roundGrade(entry.percent * 100)}/100` };
|
||||||
return Object.assign(results, assignments, totals);
|
return Object.assign(results, assignments, totals);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
lmsInstructorDashboardUrl = courseId => `${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`;
|
lmsInstructorDashboardUrl = courseId => `${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`;
|
||||||
|
|
||||||
formatHeadings = () => {
|
|
||||||
let headings = [...this.props.headings];
|
|
||||||
|
|
||||||
if (headings.length > 0) {
|
|
||||||
const userInformationHeadingLabel = (
|
|
||||||
<div>
|
|
||||||
<div>Username</div>
|
|
||||||
<div className="font-weight-normal student-key">Student Key*</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
const emailHeadingLabel = 'Email*';
|
|
||||||
|
|
||||||
headings = headings.map(heading => ({ label: heading, key: heading, width: 'col' }));
|
|
||||||
|
|
||||||
// replace username heading label to include additional user data
|
|
||||||
headings[0].label = userInformationHeadingLabel;
|
|
||||||
headings[0].width = 'col-2';
|
|
||||||
headings[1].label = emailHeadingLabel;
|
|
||||||
headings[1].width = 'col-2';
|
|
||||||
}
|
|
||||||
|
|
||||||
return headings;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCourseGradeFilterChange = (type, value) => {
|
|
||||||
const filterValue = value;
|
|
||||||
|
|
||||||
if (type === 'min') {
|
|
||||||
this.setState({
|
|
||||||
courseGradeMin: filterValue,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
courseGradeMax: filterValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCourseGradeFilterApplyButtonClick = () => {
|
|
||||||
const { courseGradeMin, courseGradeMax } = this.state;
|
|
||||||
const isMinValid = this.isGradeFilterValueInRange(courseGradeMin);
|
|
||||||
const isMaxValid = this.isGradeFilterValueInRange(courseGradeMax);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isMinCourseGradeFilterValid: isMinValid,
|
|
||||||
isMaxCourseGradeFilterValid: isMaxValid,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isMinValid && isMaxValid) {
|
|
||||||
this.props.updateCourseGradeFilter(
|
|
||||||
courseGradeMin,
|
|
||||||
courseGradeMax,
|
|
||||||
this.props.courseId,
|
|
||||||
);
|
|
||||||
this.props.getUserGrades(
|
|
||||||
this.props.courseId,
|
|
||||||
this.props.selectedCohort,
|
|
||||||
this.props.selectedTrack,
|
|
||||||
this.props.selectedAssignmentType,
|
|
||||||
{
|
|
||||||
courseGradeMin,
|
|
||||||
courseGradeMax,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
this.updateQueryParams({ courseGradeMin, courseGradeMax });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isGradeFilterValueInRange = (value) => {
|
|
||||||
const valueAsInt = parseInt(value, 10);
|
|
||||||
return valueAsInt >= 0 && valueAsInt <= 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFilterBadgeClose = filterNames => () => {
|
|
||||||
this.props.resetFilters(filterNames);
|
|
||||||
const queryParams = {};
|
|
||||||
filterNames.forEach((filterName) => {
|
|
||||||
queryParams[filterName] = false;
|
|
||||||
});
|
|
||||||
this.updateQueryParams(queryParams);
|
|
||||||
const stateUpdate = {};
|
|
||||||
const rangeStateFilters = ['assignmentGradeMin', 'assignmentGradeMax', 'courseGradeMin', 'courseGradeMax'];
|
|
||||||
rangeStateFilters.forEach((filterName) => {
|
|
||||||
if (filterNames.includes(filterName)) {
|
|
||||||
stateUpdate[filterName] = initialFilters[filterName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.setState(stateUpdate);
|
|
||||||
this.props.getUserGrades(
|
|
||||||
this.props.courseId,
|
|
||||||
filterNames.includes('cohort') ? initialFilters.cohort : this.props.selectedCohort,
|
|
||||||
filterNames.includes('track') ? initialFilters.track : this.props.selectedTrack,
|
|
||||||
filterNames.includes('assignmentType') ? initialFilters.assignmentType : this.props.selectedAssignmentType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<div className="d-flex justify-content-center">
|
||||||
mainContent={toggleFilterDrawer => (
|
{ this.props.showSpinner && <div className="spinner-overlay"><Icon className={['fa', 'fa-spinner', 'fa-spin', 'fa-5x', 'color-black']} /></div>}
|
||||||
<div className="px-3 gradebook-content">
|
<div className="gradebook-container">
|
||||||
|
<div>
|
||||||
<a
|
<a
|
||||||
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
|
href={this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}
|
||||||
className="mb-3"
|
className="mb-3"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">{'<< '}</span> {'Back to Dashboard'}
|
<span aria-hidden="true">{'<< '}</span> {'Back to Dashboard'}
|
||||||
</a>
|
</a>
|
||||||
<h1>Gradebook</h1>
|
<h1>Gradebook</h1>
|
||||||
<h3> {this.props.courseId}</h3>
|
<h3> {this.props.match.params.courseId}</h3>
|
||||||
{ this.props.areGradesFrozen &&
|
{ this.props.areGradesFrozen &&
|
||||||
<div className="alert alert-warning" role="alert" >
|
<div className="alert alert-warning" role="alert" >
|
||||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||||
@@ -563,366 +280,60 @@ export default class Gradebook extends React.Component {
|
|||||||
You are not authorized to view the gradebook for this course.
|
You are not authorized to view the gradebook for this course.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<Tabs labels={this.getActiveTabs()}>
|
<hr />
|
||||||
<div>
|
|
||||||
<h4>Step 1: Filter the Grade Report</h4>
|
|
||||||
<div className="d-flex justify-content-between" >
|
<div className="d-flex justify-content-between" >
|
||||||
{this.props.showSpinner && <div className="spinner-overlay"><Icon className="fa fa-spinner fa-spin fa-5x color-black" /></div>}
|
|
||||||
<Button className="btn-primary align-self-start" onClick={toggleFilterDrawer}><FontAwesomeIcon icon={faFilter} /> Edit Filters</Button>
|
|
||||||
<div>
|
<div>
|
||||||
<SearchField
|
<div role="radiogroup" aria-labelledby="score-view-group-label">
|
||||||
onSubmit={value =>
|
<span id="score-view-group-label">Score View:</span>
|
||||||
this.props.searchForUser(
|
|
||||||
this.props.courseId,
|
|
||||||
value,
|
|
||||||
this.props.selectedCohort,
|
|
||||||
this.props.selectedTrack,
|
|
||||||
this.props.selectedAssignmentType,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
inputLabel="Search for a learner"
|
|
||||||
onChange={filterValue => this.setState({ filterValue })}
|
|
||||||
onClear={() =>
|
|
||||||
this.props.getUserGrades(
|
|
||||||
this.props.courseId,
|
|
||||||
this.props.selectedCohort,
|
|
||||||
this.props.selectedTrack,
|
|
||||||
this.props.selectedAssignmentType,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={this.state.filterValue}
|
|
||||||
/>
|
|
||||||
<small className="form-text text-muted search-help-text">Search by username, email, or student key</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ConnectedFilterBadges
|
|
||||||
handleFilterBadgeClose={this.handleFilterBadgeClose}
|
|
||||||
/>
|
|
||||||
<StatusAlert
|
|
||||||
alertType="success"
|
|
||||||
dialog="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
|
|
||||||
onClose={() => this.props.closeBanner()}
|
|
||||||
open={this.props.showSuccess}
|
|
||||||
/>
|
|
||||||
<StatusAlert
|
|
||||||
alertType="danger"
|
|
||||||
dialog={this.getCourseGradeFilterAlertDialog()}
|
|
||||||
dismissible={false}
|
|
||||||
open={
|
|
||||||
!this.state.isMinCourseGradeFilterValid ||
|
|
||||||
!this.state.isMaxCourseGradeFilterValid
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<h4>Step 2: View or Modify Individual Grades</h4>
|
|
||||||
{this.props.totalUsersCount ?
|
|
||||||
<div>
|
|
||||||
Showing
|
|
||||||
<span className="font-weight-bold"> {this.props.filteredUsersCount} </span>
|
|
||||||
of
|
|
||||||
<span className="font-weight-bold"> {this.props.totalUsersCount} </span>
|
|
||||||
total learners
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<InputSelect
|
|
||||||
label="Score View:"
|
|
||||||
name="ScoreView"
|
|
||||||
value="percent"
|
|
||||||
options={[{ label: 'Percent', value: 'percent' }, { label: 'Absolute', value: 'absolute' }]}
|
|
||||||
onChange={this.props.toggleFormat}
|
|
||||||
/>
|
|
||||||
{this.props.showBulkManagement && (
|
|
||||||
<div>
|
|
||||||
<StatefulButton
|
|
||||||
buttonType="outline-primary"
|
|
||||||
onClick={this.handleClickExportGrades}
|
|
||||||
state={this.props.showSpinner ? 'pending' : 'default'}
|
|
||||||
labels={{
|
|
||||||
default: 'Bulk Management',
|
|
||||||
pending: 'Bulk Management',
|
|
||||||
}}
|
|
||||||
icons={{
|
|
||||||
default: <FontAwesomeIcon className="mr-2" icon={faDownload} />,
|
|
||||||
pending: <FontAwesomeIcon className="fa-spin mr-2" icon={faSpinner} />,
|
|
||||||
}}
|
|
||||||
disabledStates={['pending']}
|
|
||||||
/>
|
|
||||||
<StatefulButton
|
|
||||||
buttonType="outline-primary"
|
|
||||||
onClick={this.handleClickDownloadInterventions}
|
|
||||||
state={this.props.showSpinner ? 'pending' : 'default'}
|
|
||||||
className="ml-2"
|
|
||||||
labels={{
|
|
||||||
default: 'Interventions*',
|
|
||||||
pending: 'Interventions*',
|
|
||||||
}}
|
|
||||||
icons={{
|
|
||||||
default: <FontAwesomeIcon className="mr-2" icon={faDownload} />,
|
|
||||||
pending: <FontAwesomeIcon className="fa-spin mr-2" icon={faSpinner} />,
|
|
||||||
}}
|
|
||||||
disabledStates={['pending']}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="gradebook-container">
|
|
||||||
<div className="gbook">
|
|
||||||
<Table
|
|
||||||
columns={this.formatHeadings()}
|
|
||||||
data={this.formatter[this.props.format](
|
|
||||||
this.props.grades,
|
|
||||||
this.props.areGradesFrozen,
|
|
||||||
)}
|
|
||||||
rowHeaderColumnKey="username"
|
|
||||||
hasFixedColumnWidths
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{PageButtons(this.props)}
|
|
||||||
<p>* available for learners in the Master's track only</p>
|
|
||||||
<Modal
|
|
||||||
open={this.state.modalOpen}
|
|
||||||
title="Edit Grades"
|
|
||||||
closeText="Cancel"
|
|
||||||
body={(
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div className="grade-history-header grade-history-assignment">Assignment: </div> <div>{this.state.modalAssignmentName}</div>
|
|
||||||
<div className="grade-history-header grade-history-student">Student: </div> <div>{this.state.updateUserName}</div>
|
|
||||||
<div className="grade-history-header grade-history-original-grade">Original Grade: </div> <div>{this.props.gradeOriginalEarnedGraded}</div>
|
|
||||||
<div className="grade-history-header grade-history-current-grade">Current Grade: </div> <div>{this.props.gradeOverrideCurrentEarnedGradedOverride}</div>
|
|
||||||
</div>
|
|
||||||
<StatusAlert
|
|
||||||
alertType="danger"
|
|
||||||
dialog="Error retrieving grade override history."
|
|
||||||
open={this.props.errorFetchingGradeOverrideHistory}
|
|
||||||
dismissible={false}
|
|
||||||
/>
|
|
||||||
{!this.props.errorFetchingGradeOverrideHistory && (
|
|
||||||
<Table
|
|
||||||
columns={GRADE_OVERRIDE_HISTORY_COLUMNS}
|
|
||||||
data={[...this.props.gradeOverrides, {
|
|
||||||
date: this.state.todaysDate,
|
|
||||||
reason: (<input
|
|
||||||
type="text"
|
|
||||||
name="reasonForChange"
|
|
||||||
value={this.state.reasonForChange}
|
|
||||||
onChange={value => this.onChange(value)}
|
|
||||||
ref={(input) => { this.overrideReasonInput = input; }}
|
|
||||||
/>),
|
|
||||||
adjustedGrade: (
|
|
||||||
<span>
|
<span>
|
||||||
|
<label className="mr-2" htmlFor="score-view-percent">
|
||||||
<input
|
<input
|
||||||
type="text"
|
id="score-view-percent"
|
||||||
name="adjustedGradeValue"
|
className="ml-2 mr-1"
|
||||||
value={this.state.adjustedGradeValue}
|
type="radio"
|
||||||
onChange={value => this.onChange(value)}
|
name="score-view"
|
||||||
/>
|
value="percent"
|
||||||
{(this.state.adjustedGradePossible
|
defaultChecked
|
||||||
|| this.props.gradeOriginalPossibleGraded)
|
onClick={() => this.props.toggleFormat('percent')}
|
||||||
&& ' / '}
|
|
||||||
{this.state.adjustedGradePossible
|
|
||||||
|| this.props.gradeOriginalPossibleGraded}
|
|
||||||
</span>),
|
|
||||||
}]}
|
|
||||||
/>)}
|
|
||||||
|
|
||||||
<div>Showing most recent actions (max 5). To see more, please contact
|
|
||||||
support.
|
|
||||||
</div>
|
|
||||||
<div>Note: Once you save, your changes will be visible to students.</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
buttons={[
|
|
||||||
<Button
|
|
||||||
buttonType="primary"
|
|
||||||
onClick={this.handleAdjustedGradeClick}
|
|
||||||
>
|
|
||||||
Save Grade
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
onClose={this.closeAssignmentModal}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{this.props.showBulkManagement && (
|
|
||||||
<div>
|
|
||||||
<h4>Use this feature by downloading a CSV for bulk management,
|
|
||||||
overriding grades locally, and coming back here to upload.
|
|
||||||
</h4>
|
|
||||||
<form ref={this.fileFormRef} action={this.props.gradeExportUrl} method="post">
|
|
||||||
<StatusAlert
|
|
||||||
alertType="danger"
|
|
||||||
dialog={this.props.bulkImportError}
|
|
||||||
open={this.props.bulkImportError}
|
|
||||||
dismissible={false}
|
|
||||||
/>
|
|
||||||
<StatusAlert
|
|
||||||
alertType="success"
|
|
||||||
dialog="CSV processing. File uploads may take several minutes to complete"
|
|
||||||
open={this.props.uploadSuccess}
|
|
||||||
dismissible={false}
|
|
||||||
/>
|
/>
|
||||||
|
Percent
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<label htmlFor="score-view-absolute">
|
||||||
<input
|
<input
|
||||||
className="d-none"
|
id="score-view-absolute"
|
||||||
type="file"
|
type="radio"
|
||||||
name="csv"
|
name="score-view"
|
||||||
label="Upload Grade CSV"
|
value="absolute"
|
||||||
onChange={this.handleFileInputChange}
|
className="mr-1"
|
||||||
ref={this.fileInputRef}
|
onClick={() => this.props.toggleFormat('absolute')}
|
||||||
/>
|
/>
|
||||||
</form>
|
Absolute
|
||||||
<Button
|
</label>
|
||||||
buttonType="primary"
|
</span>
|
||||||
onClick={this.handleClickImportGrades}
|
|
||||||
>
|
|
||||||
Import Grades
|
|
||||||
</Button>
|
|
||||||
<p>
|
|
||||||
Results appear in the table below.<br />
|
|
||||||
Grade processing may take a few seconds.
|
|
||||||
</p>
|
|
||||||
<Table
|
|
||||||
data={this.props.bulkManagementHistory.map(this.formatHistoryRow)}
|
|
||||||
hasFixedColumnWidths
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
key: 'filename',
|
|
||||||
label: 'Gradebook',
|
|
||||||
columnSortable: false,
|
|
||||||
width: 'col-5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'resultsSummary',
|
|
||||||
label: 'Download Summary',
|
|
||||||
columnSortable: false,
|
|
||||||
width: 'col',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'user',
|
|
||||||
label: 'Who',
|
|
||||||
columnSortable: false,
|
|
||||||
width: 'col-1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'timeUploaded',
|
|
||||||
label: 'When',
|
|
||||||
columnSortable: false,
|
|
||||||
width: 'col',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className="table-striped"
|
|
||||||
/>
|
|
||||||
</div>)}
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{ this.props.assignmentTypes.length > 0 &&
|
||||||
initiallyOpen={false}
|
|
||||||
title={
|
|
||||||
<React.Fragment>
|
|
||||||
<FontAwesomeIcon icon={faFilter} /> Filter By...
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Collapsible title="Assignments" isOpen className="filter-group mb-3">
|
|
||||||
<div>
|
|
||||||
<div className="student-filters">
|
<div className="student-filters">
|
||||||
<span className="label">
|
<span className="label">
|
||||||
Assignment Types:
|
Assignment Types:
|
||||||
</span>
|
</span>
|
||||||
<InputSelect
|
<InputSelect
|
||||||
name="assignment-types"
|
name="assignment-types"
|
||||||
aria-label="Assignment Types"
|
ariaLabel="Assignment Types"
|
||||||
value={this.props.selectedAssignmentType}
|
value={this.props.selectedAssignmentType}
|
||||||
options={this.mapAssignmentTypeEntries(this.props.assignmentTypes)}
|
options={this.mapAssignmentTypeEntries(this.props.assignmentTypes)}
|
||||||
onChange={this.updateAssignmentTypes}
|
onChange={this.updateAssignmentTypes}
|
||||||
disabled={this.props.assignmentFilterOptions.length === 0}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<div className="student-filters">
|
<div className="student-filters">
|
||||||
<span className="label">
|
<span className="label">
|
||||||
Assignment:
|
Student Groups:
|
||||||
</span>
|
</span>
|
||||||
<InputSelect
|
|
||||||
name="assignment"
|
|
||||||
aria-label="Assignment"
|
|
||||||
value={this.props.selectedAssignment}
|
|
||||||
options={this.getAssignmentFilterOptions()}
|
|
||||||
onChange={this.handleAssignmentFilterChange}
|
|
||||||
disabled={this.props.assignmentFilterOptions.length === 0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p>Grade Range (0% - 100%)</p>
|
|
||||||
<form className="d-flex justify-content-between align-items-center" onSubmit={this.handleSubmitAssignmentGrade}>
|
|
||||||
<InputText
|
|
||||||
label="Min Grade"
|
|
||||||
name="assignmentGradeMin"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={this.state.assignmentGradeMin}
|
|
||||||
disabled={!this.props.selectedAssignment}
|
|
||||||
onChange={this.handleMinAssigGradeChange}
|
|
||||||
/>
|
|
||||||
<span className="input-percent-label">%</span>
|
|
||||||
<InputText
|
|
||||||
label="Max Grade"
|
|
||||||
name="assignmentGradeMax"
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={this.state.assignmentGradeMax}
|
|
||||||
disabled={!this.props.selectedAssignment}
|
|
||||||
onChange={this.handleMaxAssigGradeChange}
|
|
||||||
/>
|
|
||||||
<span className="input-percent-label">%</span>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="btn-outline-secondary"
|
|
||||||
name="assignmentGradeMinMax"
|
|
||||||
disabled={!this.props.selectedAssignment}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Collapsible>
|
|
||||||
<Collapsible title="Overall Grade" isOpen className="filter-group mb-3">
|
|
||||||
<div className="d-flex justify-content-between align-items-center">
|
|
||||||
<InputText
|
|
||||||
value={this.state.courseGradeMin}
|
|
||||||
name="minimum-grade"
|
|
||||||
label="Min Grade"
|
|
||||||
onChange={value => this.handleCourseGradeFilterChange('min', value)}
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
/>
|
|
||||||
<span className="input-percent-label">%</span>
|
|
||||||
<InputText
|
|
||||||
value={this.state.courseGradeMax}
|
|
||||||
name="max-grade"
|
|
||||||
label="Max Grade"
|
|
||||||
onChange={value => this.handleCourseGradeFilterChange('max', value)}
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
/>
|
|
||||||
<span className="input-percent-label">%</span>
|
|
||||||
<Button
|
|
||||||
buttonType="outline-secondary"
|
|
||||||
onClick={this.handleCourseGradeFilterApplyButtonClick}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Collapsible>
|
|
||||||
<Collapsible title="Student Groups" isOpen className="filter-group mb-3">
|
|
||||||
<InputSelect
|
<InputSelect
|
||||||
name="Tracks"
|
name="Tracks"
|
||||||
aria-label="Tracks"
|
ariaLabel="Tracks"
|
||||||
disabled={this.props.tracks.length === 0}
|
disabled={this.props.tracks.length === 0}
|
||||||
value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
|
value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
|
||||||
options={this.mapTracksEntries(this.props.tracks)}
|
options={this.mapTracksEntries(this.props.tracks)}
|
||||||
@@ -930,14 +341,99 @@ export default class Gradebook extends React.Component {
|
|||||||
/>
|
/>
|
||||||
<InputSelect
|
<InputSelect
|
||||||
name="Cohorts"
|
name="Cohorts"
|
||||||
aria-label="Cohorts"
|
ariaLabel="Cohorts"
|
||||||
disabled={this.props.cohorts.length === 0}
|
disabled={this.props.cohorts.length === 0}
|
||||||
value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
|
value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
|
||||||
options={this.mapCohortsEntries(this.props.cohorts)}
|
options={this.mapCohortsEntries(this.props.cohorts)}
|
||||||
onChange={this.updateCohorts}
|
onChange={this.updateCohorts}
|
||||||
/>
|
/>
|
||||||
</Collapsible>
|
</div>
|
||||||
</Drawer>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginLeft: '10px', marginBottom: '10px' }}>
|
||||||
|
<a className="btn btn-outline-primary mb-85" href={`${this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}#view-data_download`}>Generate Grade Report</a>
|
||||||
|
</div>
|
||||||
|
<SearchField
|
||||||
|
onSubmit={value =>
|
||||||
|
this.props.searchForUser(
|
||||||
|
this.props.match.params.courseId,
|
||||||
|
value,
|
||||||
|
this.props.selectedCohort,
|
||||||
|
this.props.selectedTrack,
|
||||||
|
this.props.selectedAssignmentType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
inputLabel="Search Username:"
|
||||||
|
onChange={filterValue => this.setState({ filterValue })}
|
||||||
|
onClear={() =>
|
||||||
|
this.props.getUserGrades(
|
||||||
|
this.props.match.params.courseId,
|
||||||
|
this.props.selectedCohort,
|
||||||
|
this.props.selectedTrack,
|
||||||
|
this.props.selectedAssignmentType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={this.state.filterValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<StatusAlert
|
||||||
|
alertType="success"
|
||||||
|
dialog="The grade has been successfully edited."
|
||||||
|
onClose={() => this.props.updateBanner(false)}
|
||||||
|
open={this.props.showSuccess}
|
||||||
|
/>
|
||||||
|
<StatusAlert
|
||||||
|
alertType="success"
|
||||||
|
dialog={`Grade for user ${this.state.websocketMsg.username} in ${this.state.websocketMsg.subsectionName} was updated.`}
|
||||||
|
onClose={() => this.setState({ websocketMsg : false })}
|
||||||
|
open={this.state.websocketMsg.visible}
|
||||||
|
/>
|
||||||
|
{PageButtons(this.props)}
|
||||||
|
<div className="gbook">
|
||||||
|
<Table
|
||||||
|
columns={this.props.headings}
|
||||||
|
data={this.formatter[this.props.format](
|
||||||
|
this.props.grades,
|
||||||
|
this.props.areGradesFrozen,
|
||||||
|
)}
|
||||||
|
rowHeaderColumnKey="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{PageButtons(this.props)}
|
||||||
|
<Modal
|
||||||
|
open={this.state.modalOpen}
|
||||||
|
title="Edit Grades"
|
||||||
|
closeText="Cancel"
|
||||||
|
body={(
|
||||||
|
<div>
|
||||||
|
<h3>{this.state.modalModel[0].assignmentName}</h3>
|
||||||
|
<Table
|
||||||
|
columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
|
||||||
|
data={this.state.modalModel}
|
||||||
|
/>
|
||||||
|
<div>Note: Once you save, your changes will be visible to students.</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
buttons={[
|
||||||
|
<Button
|
||||||
|
label="Save Grade"
|
||||||
|
buttonType="primary"
|
||||||
|
onClick={this.handleAdjustedGradeClick}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
onClose={() => this.setState({
|
||||||
|
modalOpen: false,
|
||||||
|
modalModel: [{}],
|
||||||
|
updateVal: 0,
|
||||||
|
updateModuleId: null,
|
||||||
|
updateUserId: null,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -945,52 +441,33 @@ export default class Gradebook extends React.Component {
|
|||||||
Gradebook.defaultProps = {
|
Gradebook.defaultProps = {
|
||||||
areGradesFrozen: false,
|
areGradesFrozen: false,
|
||||||
assignmentTypes: [],
|
assignmentTypes: [],
|
||||||
assignmentFilterOptions: [],
|
|
||||||
canUserViewGradebook: false,
|
canUserViewGradebook: false,
|
||||||
cohorts: [],
|
cohorts: [],
|
||||||
grades: [],
|
grades: [],
|
||||||
gradeOverrides: [],
|
|
||||||
gradeOverrideCurrentEarnedGradedOverride: null,
|
|
||||||
gradeOriginalEarnedGraded: null,
|
|
||||||
gradeOriginalPossibleGraded: null,
|
|
||||||
location: {
|
location: {
|
||||||
search: '',
|
search: '',
|
||||||
},
|
},
|
||||||
|
match: {
|
||||||
|
params: {
|
||||||
courseId: '',
|
courseId: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
selectedCohort: null,
|
selectedCohort: null,
|
||||||
selectedTrack: null,
|
selectedTrack: null,
|
||||||
selectedAssignmentType: '',
|
selectedAssignmentType: 'All',
|
||||||
selectedAssignment: '',
|
|
||||||
showSpinner: false,
|
showSpinner: false,
|
||||||
tracks: [],
|
tracks: [],
|
||||||
bulkImportError: '',
|
|
||||||
uploadSuccess: false,
|
|
||||||
showBulkManagement: false,
|
|
||||||
bulkManagementHistory: [],
|
|
||||||
errorFetchingGradeOverrideHistory: false,
|
|
||||||
totalUsersCount: null,
|
|
||||||
filteredUsersCount: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Gradebook.propTypes = {
|
Gradebook.propTypes = {
|
||||||
areGradesFrozen: PropTypes.bool,
|
areGradesFrozen: PropTypes.bool,
|
||||||
assignmentTypes: PropTypes.arrayOf(PropTypes.string),
|
assignmentTypes: PropTypes.arrayOf(PropTypes.string),
|
||||||
assignmentFilterOptions: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
label: PropTypes.string,
|
|
||||||
subsectionLabel: PropTypes.string,
|
|
||||||
})),
|
|
||||||
canUserViewGradebook: PropTypes.bool,
|
canUserViewGradebook: PropTypes.bool,
|
||||||
cohorts: PropTypes.arrayOf(PropTypes.shape({
|
cohorts: PropTypes.arrayOf(PropTypes.string),
|
||||||
name: PropTypes.string,
|
filterColumns: PropTypes.func.isRequired,
|
||||||
id: PropTypes.number,
|
|
||||||
})),
|
|
||||||
filterAssignmentType: PropTypes.func.isRequired,
|
|
||||||
updateAssignmentFilter: PropTypes.func.isRequired,
|
|
||||||
updateAssignmentLimits: PropTypes.func.isRequired,
|
|
||||||
format: PropTypes.string.isRequired,
|
format: PropTypes.string.isRequired,
|
||||||
getRoles: PropTypes.func.isRequired,
|
getRoles: PropTypes.func.isRequired,
|
||||||
getUserGrades: PropTypes.func.isRequired,
|
getUserGrades: PropTypes.func.isRequired,
|
||||||
fetchGradeOverrideHistory: PropTypes.func.isRequired,
|
|
||||||
grades: PropTypes.arrayOf(PropTypes.shape({
|
grades: PropTypes.arrayOf(PropTypes.shape({
|
||||||
percent: PropTypes.number,
|
percent: PropTypes.number,
|
||||||
section_breakdown: PropTypes.arrayOf(PropTypes.shape({
|
section_breakdown: PropTypes.arrayOf(PropTypes.shape({
|
||||||
@@ -1006,61 +483,33 @@ Gradebook.propTypes = {
|
|||||||
user_id: PropTypes.number,
|
user_id: PropTypes.number,
|
||||||
user_name: PropTypes.string,
|
user_name: PropTypes.string,
|
||||||
})),
|
})),
|
||||||
gradeOverrides: PropTypes.arrayOf(PropTypes.shape({
|
headings: PropTypes.arrayOf(PropTypes.shape({
|
||||||
date: PropTypes.string,
|
label: PropTypes.string,
|
||||||
grader: PropTypes.string,
|
key: PropTypes.string,
|
||||||
reason: PropTypes.string,
|
})).isRequired,
|
||||||
adjustedGrade: PropTypes.number,
|
|
||||||
})),
|
|
||||||
gradeOverrideCurrentEarnedGradedOverride: PropTypes.number,
|
|
||||||
gradeOriginalEarnedGraded: PropTypes.number,
|
|
||||||
gradeOriginalPossibleGraded: PropTypes.number,
|
|
||||||
doneViewingAssignment: PropTypes.func.isRequired,
|
|
||||||
headings: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
history: PropTypes.shape({
|
history: PropTypes.shape({
|
||||||
push: PropTypes.func,
|
push: PropTypes.func,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
location: PropTypes.shape({
|
location: PropTypes.shape({
|
||||||
search: PropTypes.string,
|
search: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
|
match: PropTypes.shape({
|
||||||
|
params: PropTypes.shape({
|
||||||
courseId: PropTypes.string,
|
courseId: PropTypes.string,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
searchForUser: PropTypes.func.isRequired,
|
searchForUser: PropTypes.func.isRequired,
|
||||||
selectedAssignmentType: PropTypes.string,
|
selectedAssignmentType: PropTypes.string,
|
||||||
selectedAssignment: PropTypes.string,
|
selectedCohort: PropTypes.shape({
|
||||||
selectedCohort: PropTypes.string,
|
name: PropTypes.string,
|
||||||
|
}),
|
||||||
selectedTrack: PropTypes.string,
|
selectedTrack: PropTypes.string,
|
||||||
resetFilters: PropTypes.func.isRequired,
|
|
||||||
showSpinner: PropTypes.bool,
|
showSpinner: PropTypes.bool,
|
||||||
showSuccess: PropTypes.bool.isRequired,
|
showSuccess: PropTypes.bool.isRequired,
|
||||||
toggleFormat: PropTypes.func.isRequired,
|
toggleFormat: PropTypes.func.isRequired,
|
||||||
tracks: PropTypes.arrayOf(PropTypes.shape({
|
tracks: PropTypes.arrayOf(PropTypes.shape({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
})),
|
})),
|
||||||
closeBanner: PropTypes.func.isRequired,
|
updateBanner: PropTypes.func.isRequired,
|
||||||
updateGrades: PropTypes.func.isRequired,
|
updateGrades: PropTypes.func.isRequired,
|
||||||
gradeExportUrl: PropTypes.string.isRequired,
|
|
||||||
interventionExportUrl: PropTypes.string.isRequired,
|
|
||||||
submitFileUploadFormData: PropTypes.func.isRequired,
|
|
||||||
bulkImportError: PropTypes.string,
|
|
||||||
uploadSuccess: PropTypes.bool,
|
|
||||||
errorFetchingGradeOverrideHistory: PropTypes.bool,
|
|
||||||
showBulkManagement: PropTypes.bool,
|
|
||||||
bulkManagementHistory: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
originalFilename: PropTypes.string.isRequired,
|
|
||||||
user: PropTypes.string.isRequired,
|
|
||||||
timeUploaded: PropTypes.string.isRequired,
|
|
||||||
summaryOfRowsProcessed: PropTypes.shape({
|
|
||||||
total: PropTypes.number.isRequired,
|
|
||||||
successfullyProcessed: PropTypes.number.isRequired,
|
|
||||||
failed: PropTypes.number.isRequired,
|
|
||||||
skipped: PropTypes.number.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
})),
|
|
||||||
totalUsersCount: PropTypes.number,
|
|
||||||
filteredUsersCount: PropTypes.number,
|
|
||||||
initializeFilters: PropTypes.func.isRequired,
|
|
||||||
updateGradesIfAssignmentGradeFiltersSet: PropTypes.func.isRequired,
|
|
||||||
updateCourseGradeFilter: PropTypes.func.isRequired,
|
|
||||||
downloadBulkGradesReport: PropTypes.func.isRequired,
|
|
||||||
downloadInterventionReport: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ export default class Header extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<header className="d-flex justify-content-center align-items-center p-3 border-bottom-blue">
|
<header className="d-flex justify-content-center align-items-center p-3 border-bottom-blue">
|
||||||
<Hyperlink destination="https://www.edx.org">
|
<Hyperlink content={this.renderLogo()} destination="https://www.edx.org" />
|
||||||
{this.renderLogo()}
|
|
||||||
</Hyperlink>
|
|
||||||
<div />
|
<div />
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function PageButtons({
|
|||||||
style={{ paddingBottom: '20px' }}
|
style={{ paddingBottom: '20px' }}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
label="Previous Page"
|
||||||
style={{ margin: '20px' }}
|
style={{ margin: '20px' }}
|
||||||
buttonType="outline-primary"
|
buttonType="outline-primary"
|
||||||
disabled={!prevPage}
|
disabled={!prevPage}
|
||||||
@@ -24,10 +25,9 @@ export default function PageButtons({
|
|||||||
selectedTrack,
|
selectedTrack,
|
||||||
selectedAssignmentType,
|
selectedAssignmentType,
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
Previous Page
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
|
label="Next Page"
|
||||||
style={{ margin: '20px' }}
|
style={{ margin: '20px' }}
|
||||||
buttonType="outline-primary"
|
buttonType="outline-primary"
|
||||||
disabled={!nextPage}
|
disabled={!nextPage}
|
||||||
@@ -39,9 +39,7 @@ export default function PageButtons({
|
|||||||
selectedTrack,
|
selectedTrack,
|
||||||
selectedAssignmentType,
|
selectedAssignmentType,
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
Next Page
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,37 +2,19 @@ import { connect } from 'react-redux';
|
|||||||
|
|
||||||
import Gradebook from '../../components/Gradebook';
|
import Gradebook from '../../components/Gradebook';
|
||||||
import {
|
import {
|
||||||
closeBanner,
|
|
||||||
doneViewingAssignment,
|
|
||||||
fetchGradeOverrideHistory,
|
|
||||||
fetchGrades,
|
fetchGrades,
|
||||||
fetchMatchingUserGrades,
|
fetchMatchingUserGrades,
|
||||||
fetchPrevNextGrades,
|
fetchPrevNextGrades,
|
||||||
filterAssignmentType,
|
gradeUpdateSuccess,
|
||||||
submitFileUploadFormData,
|
|
||||||
toggleGradeFormat,
|
|
||||||
updateGrades,
|
updateGrades,
|
||||||
updateGradesIfAssignmentGradeFiltersSet,
|
toggleGradeFormat,
|
||||||
downloadBulkGradesReport,
|
filterColumns,
|
||||||
downloadInterventionReport,
|
updateBanner,
|
||||||
} from '../../data/actions/grades';
|
} from '../../data/actions/grades';
|
||||||
import { fetchCohorts } from '../../data/actions/cohorts';
|
import { fetchCohorts } from '../../data/actions/cohorts';
|
||||||
import { fetchTracks } from '../../data/actions/tracks';
|
import { fetchTracks } from '../../data/actions/tracks';
|
||||||
import { initializeFilters, resetFilters, updateAssignmentFilter, updateAssignmentLimits, updateCourseGradeFilter } from '../../data/actions/filters';
|
|
||||||
import stateHasMastersTrack from '../../data/selectors/tracks';
|
|
||||||
import {
|
|
||||||
getBulkManagementHistory,
|
|
||||||
getHeadings,
|
|
||||||
formatMinAssignmentGrade,
|
|
||||||
formatMaxAssignmentGrade,
|
|
||||||
formatMinCourseGrade,
|
|
||||||
formatMaxCourseGrade,
|
|
||||||
} from '../../data/selectors/grades';
|
|
||||||
import { selectableAssignmentLabels } from '../../data/selectors/filters';
|
|
||||||
import { getCohortNameById } from '../../data/selectors/cohorts';
|
|
||||||
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
||||||
import { getRoles } from '../../data/actions/roles';
|
import { getRoles } from '../../data/actions/roles';
|
||||||
import LmsApiService from '../../data/services/LmsApiService';
|
|
||||||
|
|
||||||
function shouldShowSpinner(state) {
|
function shouldShowSpinner(state) {
|
||||||
if (state.roles.canUserViewGradebook === true) {
|
if (state.roles.canUserViewGradebook === true) {
|
||||||
@@ -43,104 +25,66 @@ function shouldShowSpinner(state) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => (
|
const mapStateToProps = state => (
|
||||||
{
|
{
|
||||||
courseId: ownProps.match.params.courseId,
|
|
||||||
grades: state.grades.results,
|
grades: state.grades.results,
|
||||||
gradeOverrides: state.grades.gradeOverrideHistoryResults,
|
headings: state.grades.headings,
|
||||||
gradeOverrideCurrentEarnedAllOverride: state.grades.gradeOverrideCurrentEarnedAllOverride,
|
|
||||||
gradeOverrideCurrentPossibleAllOverride: state.grades.gradeOverrideCurrentPossibleAllOverride,
|
|
||||||
gradeOverrideCurrentEarnedGradedOverride: state.grades.gradeOverrideCurrentEarnedGradedOverride,
|
|
||||||
gradeOverrideCurrentPossibleGradedOverride:
|
|
||||||
state.grades.gradeOverrideCurrentPossibleGradedOverride,
|
|
||||||
gradeOriginalEarnedGraded: state.grades.gradeOriginalEarnedGraded,
|
|
||||||
gradeOriginalPossibleGraded: state.grades.gradeOriginalPossibleGraded,
|
|
||||||
headings: getHeadings(state),
|
|
||||||
tracks: state.tracks.results,
|
tracks: state.tracks.results,
|
||||||
cohorts: state.cohorts.results,
|
cohorts: state.cohorts.results,
|
||||||
selectedTrack: state.filters.track,
|
selectedTrack: state.grades.selectedTrack,
|
||||||
selectedCohort: state.filters.cohort,
|
selectedCohort: state.grades.selectedCohort,
|
||||||
selectedAssignmentType: state.filters.assignmentType,
|
selectedAssignmentType: state.grades.selectedAssignmentType,
|
||||||
selectedAssignment: (state.filters.assignment || {}).label,
|
|
||||||
format: state.grades.gradeFormat,
|
format: state.grades.gradeFormat,
|
||||||
showSuccess: state.grades.showSuccess,
|
showSuccess: state.grades.showSuccess,
|
||||||
errorFetchingGradeOverrideHistory: state.grades.errorFetchingOverrideHistory,
|
|
||||||
prevPage: state.grades.prevPage,
|
prevPage: state.grades.prevPage,
|
||||||
nextPage: state.grades.nextPage,
|
nextPage: state.grades.nextPage,
|
||||||
assignmentTypes: state.assignmentTypes.results,
|
assignmentTypes: state.assignmentTypes.results,
|
||||||
assignmentFilterOptions: selectableAssignmentLabels(state),
|
|
||||||
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
|
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
|
||||||
showSpinner: shouldShowSpinner(state),
|
showSpinner: shouldShowSpinner(state),
|
||||||
canUserViewGradebook: state.roles.canUserViewGradebook,
|
canUserViewGradebook: state.roles.canUserViewGradebook,
|
||||||
gradeExportUrl: LmsApiService.getGradeExportCsvUrl(ownProps.match.params.courseId, {
|
|
||||||
cohort: getCohortNameById(state, state.filters.cohort),
|
|
||||||
track: state.filters.track,
|
|
||||||
assignment: (state.filters.assignment || {}).id,
|
|
||||||
assignmentType: state.filters.assignmentType,
|
|
||||||
assignmentGradeMin: formatMinAssignmentGrade(
|
|
||||||
state.filters.assignmentGradeMin,
|
|
||||||
{ assignmentId: (state.filters.assignment || {}).id },
|
|
||||||
),
|
|
||||||
assignmentGradeMax: formatMaxAssignmentGrade(
|
|
||||||
state.filters.assignmentGradeMax,
|
|
||||||
{ assignmentId: (state.filters.assignment || {}).id },
|
|
||||||
),
|
|
||||||
courseGradeMin: formatMinCourseGrade(state.filters.courseGradeMin),
|
|
||||||
courseGradeMax: formatMaxCourseGrade(state.filters.courseGradeMax),
|
|
||||||
}),
|
|
||||||
interventionExportUrl:
|
|
||||||
LmsApiService.getInterventionExportCsvUrl(ownProps.match.params.courseId, {
|
|
||||||
cohort: getCohortNameById(state, state.filters.cohort),
|
|
||||||
assignment: (state.filters.assignment || {}).id,
|
|
||||||
assignmentType: state.filters.assignmentType,
|
|
||||||
assignmentGradeMin: formatMinAssignmentGrade(
|
|
||||||
state.filters.assignmentGradeMin,
|
|
||||||
{ assignmentId: (state.filters.assignment || {}).id },
|
|
||||||
),
|
|
||||||
assignmentGradeMax: formatMaxAssignmentGrade(
|
|
||||||
state.filters.assignmentGradeMax,
|
|
||||||
{ assignmentId: (state.filters.assignment || {}).id },
|
|
||||||
),
|
|
||||||
courseGradeMin: formatMinCourseGrade(state.filters.courseGradeMin),
|
|
||||||
courseGradeMax: formatMaxCourseGrade(state.filters.courseGradeMax),
|
|
||||||
}),
|
|
||||||
bulkImportError: state.grades.bulkManagement &&
|
|
||||||
state.grades.bulkManagement.errorMessages ?
|
|
||||||
`Errors while processing: ${state.grades.bulkManagement.errorMessages.join(', ')}` :
|
|
||||||
'',
|
|
||||||
uploadSuccess: !!(state.grades.bulkManagement &&
|
|
||||||
state.grades.bulkManagement.uploadSuccess),
|
|
||||||
showBulkManagement: stateHasMastersTrack(state) && state.config.bulkManagementAvailable,
|
|
||||||
bulkManagementHistory: getBulkManagementHistory(state),
|
|
||||||
totalUsersCount: state.grades.totalUsersCount,
|
|
||||||
filteredUsersCount: state.grades.filteredUsersCount,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = dispatch => (
|
||||||
doneViewingAssignment,
|
{
|
||||||
getUserGrades: fetchGrades,
|
getUserGrades: (courseId, cohort, track, assignmentType) => {
|
||||||
fetchGradeOverrideHistory,
|
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
||||||
searchForUser: fetchMatchingUserGrades,
|
},
|
||||||
getPrevNextGrades: fetchPrevNextGrades,
|
searchForUser: (courseId, searchText, cohort, track, assignmentType) => {
|
||||||
getCohorts: fetchCohorts,
|
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, assignmentType, false));
|
||||||
getTracks: fetchTracks,
|
},
|
||||||
getAssignmentTypes: fetchAssignmentTypes,
|
getPrevNextGrades: (endpoint, courseId, cohort, track, assignmentType) => {
|
||||||
updateGrades,
|
dispatch(fetchPrevNextGrades(endpoint, courseId, cohort, track, assignmentType));
|
||||||
toggleFormat: toggleGradeFormat,
|
},
|
||||||
filterAssignmentType,
|
getCohorts: (courseId) => {
|
||||||
closeBanner,
|
dispatch(fetchCohorts(courseId));
|
||||||
getRoles,
|
},
|
||||||
submitFileUploadFormData,
|
getTracks: (courseId) => {
|
||||||
initializeFilters,
|
dispatch(fetchTracks(courseId));
|
||||||
resetFilters,
|
},
|
||||||
updateAssignmentFilter,
|
getAssignmentTypes: (courseId) => {
|
||||||
updateAssignmentLimits,
|
dispatch(fetchAssignmentTypes(courseId));
|
||||||
updateGradesIfAssignmentGradeFiltersSet,
|
},
|
||||||
updateCourseGradeFilter,
|
updateGrades: (courseId, updateData, searchText, cohort, track) => {
|
||||||
downloadBulkGradesReport,
|
dispatch(updateGrades(courseId, updateData, searchText, cohort, track));
|
||||||
downloadInterventionReport,
|
},
|
||||||
};
|
toggleFormat: (formatType) => {
|
||||||
|
dispatch(toggleGradeFormat(formatType));
|
||||||
|
},
|
||||||
|
filterColumns: (filterType, exampleUser) => {
|
||||||
|
dispatch(filterColumns(filterType, exampleUser));
|
||||||
|
},
|
||||||
|
updateBanner: (showSuccess) => {
|
||||||
|
dispatch(updateBanner(showSuccess));
|
||||||
|
},
|
||||||
|
getRoles: (matchParams, urlQuery) => {
|
||||||
|
dispatch(getRoles(matchParams, urlQuery));
|
||||||
|
},
|
||||||
|
gradeUpdateSuccess: (courseId, data) => {
|
||||||
|
dispatch(gradeUpdateSuccess(courseId, data));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const GradebookPage = connect(
|
const GradebookPage = connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
|
|||||||
@@ -4,17 +4,12 @@ import {
|
|||||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||||
GOT_ARE_GRADES_FROZEN,
|
GOT_ARE_GRADES_FROZEN,
|
||||||
} from '../constants/actionTypes/assignmentTypes';
|
} from '../constants/actionTypes/assignmentTypes';
|
||||||
import GOT_BULK_MANAGEMENT_CONFIG from '../constants/actionTypes/config';
|
|
||||||
import LmsApiService from '../services/LmsApiService';
|
import LmsApiService from '../services/LmsApiService';
|
||||||
|
|
||||||
const startedFetchingAssignmentTypes = () => ({ type: STARTED_FETCHING_ASSIGNMENT_TYPES });
|
const startedFetchingAssignmentTypes = () => ({ type: STARTED_FETCHING_ASSIGNMENT_TYPES });
|
||||||
const errorFetchingAssignmentTypes = () => ({ type: ERROR_FETCHING_ASSIGNMENT_TYPES });
|
const errorFetchingAssignmentTypes = () => ({ type: ERROR_FETCHING_ASSIGNMENT_TYPES });
|
||||||
const gotAssignmentTypes = assignmentTypes => ({ type: GOT_ASSIGNMENT_TYPES, assignmentTypes });
|
const gotAssignmentTypes = assignmentTypes => ({ type: GOT_ASSIGNMENT_TYPES, assignmentTypes });
|
||||||
const gotGradesFrozen = areGradesFrozen => ({ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen });
|
const gotGradesFrozen = areGradesFrozen => ({ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen });
|
||||||
const gotBulkManagementConfig = bulkManagementEnabled => ({
|
|
||||||
type: GOT_BULK_MANAGEMENT_CONFIG,
|
|
||||||
data: bulkManagementEnabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchAssignmentTypes = courseId => (
|
const fetchAssignmentTypes = courseId => (
|
||||||
(dispatch) => {
|
(dispatch) => {
|
||||||
@@ -24,7 +19,6 @@ const fetchAssignmentTypes = courseId => (
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatch(gotAssignmentTypes(Object.keys(data.assignment_types)));
|
dispatch(gotAssignmentTypes(Object.keys(data.assignment_types)));
|
||||||
dispatch(gotGradesFrozen(data.grades_frozen));
|
dispatch(gotGradesFrozen(data.grades_frozen));
|
||||||
dispatch(gotBulkManagementConfig(data.can_see_bulk_management));
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
dispatch(errorFetchingAssignmentTypes());
|
dispatch(errorFetchingAssignmentTypes());
|
||||||
|
|||||||
@@ -11,12 +11,9 @@ import {
|
|||||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||||
GOT_ARE_GRADES_FROZEN,
|
GOT_ARE_GRADES_FROZEN,
|
||||||
} from '../constants/actionTypes/assignmentTypes';
|
} from '../constants/actionTypes/assignmentTypes';
|
||||||
import GOT_BULK_MANAGEMENT_CONFIG from '../constants/actionTypes/config';
|
|
||||||
|
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
const axiosMock = new MockAdapter(apiClient);
|
const axiosMock = new MockAdapter(apiClient);
|
||||||
apiClient.isAccessTokenExpired = jest.fn();
|
|
||||||
apiClient.isAccessTokenExpired.mockReturnValue(false);
|
|
||||||
|
|
||||||
describe('actions', () => {
|
describe('actions', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -43,14 +40,12 @@ describe('actions', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
grades_frozen: false,
|
grades_frozen: false,
|
||||||
can_see_bulk_management: true,
|
|
||||||
};
|
};
|
||||||
it('dispatches success action after fetching fetchAssignmentTypes', () => {
|
it('dispatches success action after fetching fetchAssignmentTypes', () => {
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||||
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
|
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
|
||||||
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: responseData.grades_frozen },
|
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: responseData.grades_frozen },
|
||||||
{ type: GOT_BULK_MANAGEMENT_CONFIG, data: true },
|
|
||||||
];
|
];
|
||||||
const store = mockStore();
|
const store = mockStore();
|
||||||
|
|
||||||
@@ -82,7 +77,6 @@ describe('actions', () => {
|
|||||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||||
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
|
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
|
||||||
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: true },
|
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: true },
|
||||||
{ type: GOT_BULK_MANAGEMENT_CONFIG, data: true },
|
|
||||||
];
|
];
|
||||||
const store = mockStore();
|
const store = mockStore();
|
||||||
responseData.grades_frozen = true;
|
responseData.grades_frozen = true;
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
|
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
const axiosMock = new MockAdapter(apiClient);
|
const axiosMock = new MockAdapter(apiClient);
|
||||||
apiClient.isAccessTokenExpired = jest.fn();
|
|
||||||
apiClient.isAccessTokenExpired.mockReturnValue(false);
|
|
||||||
|
|
||||||
describe('actions', () => {
|
describe('actions', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import initialFilters from '../constants/filters';
|
|
||||||
import { INITIALIZE_FILTERS, RESET_FILTERS, UPDATE_ASSIGNMENT_FILTER, UPDATE_ASSIGNMENT_LIMITS, UPDATE_COURSE_GRADE_LIMITS } from '../constants/actionTypes/filters';
|
|
||||||
|
|
||||||
const initializeFilters = ({
|
|
||||||
assignment = initialFilters.assignment,
|
|
||||||
assignmentType = initialFilters.assignmentType,
|
|
||||||
track = initialFilters.track,
|
|
||||||
cohort = initialFilters.cohort,
|
|
||||||
assignmentGradeMin = initialFilters.assignmentGradeMin,
|
|
||||||
assignmentGradeMax = initialFilters.assignmentGradeMax,
|
|
||||||
courseGradeMin = initialFilters.courseGradeMin,
|
|
||||||
courseGradeMax = initialFilters.assignmentGradeMax,
|
|
||||||
}) => ({
|
|
||||||
type: INITIALIZE_FILTERS,
|
|
||||||
data: {
|
|
||||||
assignment: { id: assignment },
|
|
||||||
assignmentType,
|
|
||||||
track,
|
|
||||||
cohort,
|
|
||||||
assignmentGradeMin,
|
|
||||||
assignmentGradeMax,
|
|
||||||
courseGradeMin,
|
|
||||||
courseGradeMax,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetFilters = filterNames => ({
|
|
||||||
type: RESET_FILTERS,
|
|
||||||
filterNames,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateAssignmentFilter = assignment => ({
|
|
||||||
type: UPDATE_ASSIGNMENT_FILTER,
|
|
||||||
data: assignment,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateAssignmentLimits = (minGrade, maxGrade) => ({
|
|
||||||
type: UPDATE_ASSIGNMENT_LIMITS,
|
|
||||||
data: { minGrade, maxGrade },
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateCourseGradeFilter = (courseGradeMin, courseGradeMax, courseId) => ({
|
|
||||||
type: UPDATE_COURSE_GRADE_LIMITS,
|
|
||||||
data: {
|
|
||||||
courseGradeMin,
|
|
||||||
courseGradeMax,
|
|
||||||
courseId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
|
||||||
initializeFilters, resetFilters, updateAssignmentFilter,
|
|
||||||
updateAssignmentLimits, updateCourseGradeFilter,
|
|
||||||
};
|
|
||||||
@@ -7,45 +7,19 @@ import {
|
|||||||
GRADE_UPDATE_SUCCESS,
|
GRADE_UPDATE_SUCCESS,
|
||||||
GRADE_UPDATE_FAILURE,
|
GRADE_UPDATE_FAILURE,
|
||||||
TOGGLE_GRADE_FORMAT,
|
TOGGLE_GRADE_FORMAT,
|
||||||
FILTER_BY_ASSIGNMENT_TYPE,
|
FILTER_COLUMNS,
|
||||||
OPEN_BANNER,
|
UPDATE_BANNER,
|
||||||
CLOSE_BANNER,
|
|
||||||
START_UPLOAD,
|
|
||||||
UPLOAD_COMPLETE,
|
|
||||||
UPLOAD_ERR,
|
|
||||||
GOT_BULK_HISTORY,
|
|
||||||
BULK_HISTORY_ERR,
|
|
||||||
GOT_GRADE_OVERRIDE_HISTORY,
|
|
||||||
DONE_VIEWING_ASSIGNMENT,
|
|
||||||
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
|
||||||
UPLOAD_OVERRIDE,
|
|
||||||
UPLOAD_OVERRIDE_ERROR,
|
|
||||||
BULK_GRADE_REPORT_DOWNLOADED,
|
|
||||||
INTERVENTION_REPORT_DOWNLOADED,
|
|
||||||
} from '../constants/actionTypes/grades';
|
} from '../constants/actionTypes/grades';
|
||||||
import LmsApiService from '../services/LmsApiService';
|
import LmsApiService from '../services/LmsApiService';
|
||||||
import { sortAlphaAsc, formatDateForDisplay } from './utils';
|
import { headingMapper, sortAlphaAsc } from './utils';
|
||||||
import { formatMaxAssignmentGrade, formatMinAssignmentGrade, formatMaxCourseGrade, formatMinCourseGrade } from '../selectors/grades';
|
|
||||||
import { getFilters } from '../selectors/filters';
|
|
||||||
import apiClient from '../apiClient';
|
import apiClient from '../apiClient';
|
||||||
|
|
||||||
const defaultAssignmentFilter = 'All';
|
const defaultAssignmentFilter = 'All';
|
||||||
|
|
||||||
const startedCsvUpload = () => ({ type: START_UPLOAD });
|
|
||||||
const finishedCsvUpload = () => ({ type: UPLOAD_COMPLETE });
|
|
||||||
const csvUploadError = data => ({ type: UPLOAD_ERR, data });
|
|
||||||
const gotBulkHistory = data => ({ type: GOT_BULK_HISTORY, data });
|
|
||||||
const bulkHistoryError = () => ({ type: BULK_HISTORY_ERR });
|
|
||||||
|
|
||||||
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
|
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
|
||||||
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
|
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
|
||||||
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
|
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
|
||||||
const errorFetchingGradeOverrideHistory = () => ({ type: ERROR_FETCHING_GRADE_OVERRIDE_HISTORY });
|
const gotGrades = (grades, cohort, track, assignmentType, headings, prev, next, courseId) => ({
|
||||||
|
|
||||||
const gotGrades = ({
|
|
||||||
grades, cohort, track, assignmentType, headings, prev,
|
|
||||||
next, courseId, totalUsersCount, filteredUsersCount,
|
|
||||||
}) => ({
|
|
||||||
type: GOT_GRADES,
|
type: GOT_GRADES,
|
||||||
grades,
|
grades,
|
||||||
cohort,
|
cohort,
|
||||||
@@ -55,26 +29,6 @@ const gotGrades = ({
|
|||||||
prev,
|
prev,
|
||||||
next,
|
next,
|
||||||
courseId,
|
courseId,
|
||||||
totalUsersCount,
|
|
||||||
filteredUsersCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
const gotGradeOverrideHistory = ({
|
|
||||||
overrideHistory, currentEarnedAllOverride, currentPossibleAllOverride,
|
|
||||||
currentEarnedGradedOverride, currentPossibleGradedOverride,
|
|
||||||
originalGradeEarnedAll, originalGradePossibleAll, originalGradeEarnedGraded,
|
|
||||||
originalGradePossibleGraded,
|
|
||||||
}) => ({
|
|
||||||
type: GOT_GRADE_OVERRIDE_HISTORY,
|
|
||||||
overrideHistory,
|
|
||||||
currentEarnedAllOverride,
|
|
||||||
currentPossibleAllOverride,
|
|
||||||
currentEarnedGradedOverride,
|
|
||||||
currentPossibleGradedOverride,
|
|
||||||
originalGradeEarnedAll,
|
|
||||||
originalGradePossibleAll,
|
|
||||||
originalGradeEarnedGraded,
|
|
||||||
originalGradePossibleGraded,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
|
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
|
||||||
@@ -88,91 +42,37 @@ const gradeUpdateFailure = (courseId, error) => ({
|
|||||||
courseId,
|
courseId,
|
||||||
payload: { error },
|
payload: { error },
|
||||||
});
|
});
|
||||||
const uploadOverrideSuccess = courseId => ({
|
|
||||||
type: UPLOAD_OVERRIDE,
|
|
||||||
courseId,
|
|
||||||
});
|
|
||||||
// This action for google analytics only. Doesn't change redux state.
|
|
||||||
const downloadBulkGradesReport = courseId => ({
|
|
||||||
type: BULK_GRADE_REPORT_DOWNLOADED,
|
|
||||||
courseId,
|
|
||||||
});
|
|
||||||
// This action for google analytics only. Doesn't change redux state.
|
|
||||||
const downloadInterventionReport = courseId => ({
|
|
||||||
type: INTERVENTION_REPORT_DOWNLOADED,
|
|
||||||
courseId,
|
|
||||||
});
|
|
||||||
const uploadOverrideFailure = (courseId, error) => ({
|
|
||||||
type: UPLOAD_OVERRIDE_ERROR,
|
|
||||||
courseId,
|
|
||||||
payload: { error },
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType });
|
const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType });
|
||||||
|
|
||||||
const filterAssignmentType = filterType => (
|
const filterColumns = (filterType, exampleUser) => (
|
||||||
dispatch => dispatch({
|
dispatch => dispatch({
|
||||||
type: FILTER_BY_ASSIGNMENT_TYPE,
|
type: FILTER_COLUMNS,
|
||||||
filterType,
|
headings: headingMapper(filterType)(exampleUser),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const openBanner = () => ({ type: OPEN_BANNER });
|
const updateBanner = showSuccess => ({ type: UPDATE_BANNER, showSuccess });
|
||||||
const closeBanner = () => ({ type: CLOSE_BANNER });
|
|
||||||
|
|
||||||
const fetchGrades = (
|
const fetchGrades = (courseId, cohort, track, assignmentType, showSuccess) => (
|
||||||
courseId,
|
(dispatch) => {
|
||||||
cohort,
|
|
||||||
track,
|
|
||||||
assignmentType,
|
|
||||||
options = {},
|
|
||||||
) => (
|
|
||||||
(dispatch, getState) => {
|
|
||||||
dispatch(startedFetchingGrades());
|
dispatch(startedFetchingGrades());
|
||||||
const {
|
return LmsApiService.fetchGradebookData(courseId, null, cohort, track)
|
||||||
assignment,
|
|
||||||
assignmentGradeMax: assignmentMax,
|
|
||||||
assignmentGradeMin: assignmentMin,
|
|
||||||
courseGradeMin,
|
|
||||||
courseGradeMax,
|
|
||||||
} = getFilters(getState());
|
|
||||||
const { id: assignmentId } = assignment || {};
|
|
||||||
const assignmentGradeMax = formatMaxAssignmentGrade(assignmentMax, { assignmentId });
|
|
||||||
const assignmentGradeMin = formatMinAssignmentGrade(assignmentMin, { assignmentId });
|
|
||||||
const courseGradeMinFormatted = formatMinCourseGrade(courseGradeMin);
|
|
||||||
const courseGradeMaxFormatted = formatMaxCourseGrade(courseGradeMax);
|
|
||||||
return LmsApiService.fetchGradebookData(
|
|
||||||
courseId,
|
|
||||||
options.searchText || null,
|
|
||||||
cohort,
|
|
||||||
track,
|
|
||||||
{
|
|
||||||
assignment: assignmentId,
|
|
||||||
assignmentGradeMax,
|
|
||||||
assignmentGradeMin,
|
|
||||||
courseGradeMin: courseGradeMinFormatted,
|
|
||||||
courseGradeMax: courseGradeMaxFormatted,
|
|
||||||
},
|
|
||||||
|
|
||||||
)
|
|
||||||
.then(response => response.data)
|
.then(response => response.data)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatch(gotGrades({
|
dispatch(gotGrades(
|
||||||
grades: data.results.sort(sortAlphaAsc),
|
data.results.sort(sortAlphaAsc),
|
||||||
cohort,
|
cohort,
|
||||||
track,
|
track,
|
||||||
assignmentType,
|
assignmentType,
|
||||||
prev: data.previous,
|
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
|
||||||
next: data.next,
|
data.previous,
|
||||||
|
data.next,
|
||||||
courseId,
|
courseId,
|
||||||
totalUsersCount: data.total_users_count,
|
));
|
||||||
filteredUsersCount: data.filtered_users_count,
|
|
||||||
}));
|
|
||||||
dispatch(finishedFetchingGrades());
|
dispatch(finishedFetchingGrades());
|
||||||
if (options.showSuccess) {
|
dispatch(updateBanner(!!showSuccess));
|
||||||
dispatch(openBanner());
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
dispatch(errorFetchingGrades());
|
dispatch(errorFetchingGrades());
|
||||||
@@ -180,40 +80,6 @@ const fetchGrades = (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatGradeOverrideForDisplay = historyArray => historyArray.map(item => ({
|
|
||||||
date: formatDateForDisplay(new Date(item.history_date)),
|
|
||||||
grader: item.history_user,
|
|
||||||
reason: item.override_reason,
|
|
||||||
adjustedGrade: item.earned_graded_override,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const doneViewingAssignment = () => dispatch => dispatch({
|
|
||||||
type: DONE_VIEWING_ASSIGNMENT,
|
|
||||||
});
|
|
||||||
const fetchGradeOverrideHistory = (subsectionId, userId) => (
|
|
||||||
dispatch =>
|
|
||||||
LmsApiService.fetchGradeOverrideHistory(subsectionId, userId)
|
|
||||||
.then(response => response.data)
|
|
||||||
.then((data) => {
|
|
||||||
dispatch(gotGradeOverrideHistory({
|
|
||||||
overrideHistory: formatGradeOverrideForDisplay(data.history),
|
|
||||||
currentEarnedAllOverride: data.override ? data.override.earned_all_override : null,
|
|
||||||
currentPossibleAllOverride: data.override ? data.override.possible_all_override : null,
|
|
||||||
currentEarnedGradedOverride: data.override ? data.override.earned_graded_override : null,
|
|
||||||
currentPossibleGradedOverride: data.override ?
|
|
||||||
data.override.possible_graded_override : null,
|
|
||||||
originalGradeEarnedAll: data.original_grade ? data.original_grade.earned_all : null,
|
|
||||||
originalGradePossibleAll: data.original_grade ? data.original_grade.possible_all : null,
|
|
||||||
originalGradeEarnedGraded: data.original_grade ? data.original_grade.earned_graded : null,
|
|
||||||
originalGradePossibleGraded: data.original_grade ?
|
|
||||||
data.original_grade.possible_graded : null,
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
dispatch(errorFetchingGradeOverrideHistory());
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchMatchingUserGrades = (
|
const fetchMatchingUserGrades = (
|
||||||
courseId,
|
courseId,
|
||||||
searchText,
|
searchText,
|
||||||
@@ -221,11 +87,30 @@ const fetchMatchingUserGrades = (
|
|||||||
track,
|
track,
|
||||||
assignmentType,
|
assignmentType,
|
||||||
showSuccess,
|
showSuccess,
|
||||||
options = {},
|
) => (
|
||||||
) => {
|
(dispatch) => {
|
||||||
const newOptions = { ...options, searchText, showSuccess };
|
dispatch(startedFetchingGrades());
|
||||||
return fetchGrades(courseId, cohort, track, assignmentType, newOptions);
|
return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track)
|
||||||
};
|
.then(response => response.data)
|
||||||
|
.then((data) => {
|
||||||
|
dispatch(gotGrades(
|
||||||
|
data.results.sort(sortAlphaAsc),
|
||||||
|
cohort,
|
||||||
|
track,
|
||||||
|
assignmentType,
|
||||||
|
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
|
||||||
|
data.previous,
|
||||||
|
data.next,
|
||||||
|
courseId,
|
||||||
|
));
|
||||||
|
dispatch(finishedFetchingGrades());
|
||||||
|
dispatch(updateBanner(showSuccess));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(errorFetchingGrades());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
|
const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
|
||||||
(dispatch) => {
|
(dispatch) => {
|
||||||
@@ -233,17 +118,16 @@ const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType)
|
|||||||
return apiClient.get(endpoint)
|
return apiClient.get(endpoint)
|
||||||
.then(response => response.data)
|
.then(response => response.data)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatch(gotGrades({
|
dispatch(gotGrades(
|
||||||
grades: data.results.sort(sortAlphaAsc),
|
data.results.sort(sortAlphaAsc),
|
||||||
cohort,
|
cohort,
|
||||||
track,
|
track,
|
||||||
assignmentType,
|
assignmentType,
|
||||||
prev: data.previous,
|
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
|
||||||
next: data.next,
|
data.previous,
|
||||||
|
data.next,
|
||||||
courseId,
|
courseId,
|
||||||
totalUsersCount: data.total_users_count,
|
));
|
||||||
filteredUsersCount: data.filtered_users_count,
|
|
||||||
}));
|
|
||||||
dispatch(finishedFetchingGrades());
|
dispatch(finishedFetchingGrades());
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -259,15 +143,14 @@ const updateGrades = (courseId, updateData, searchText, cohort, track) => (
|
|||||||
.then(response => response.data)
|
.then(response => response.data)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatch(gradeUpdateSuccess(courseId, data));
|
dispatch(gradeUpdateSuccess(courseId, data));
|
||||||
dispatch(fetchMatchingUserGrades(
|
// dispatch(fetchMatchingUserGrades(
|
||||||
courseId,
|
// courseId,
|
||||||
searchText,
|
// searchText,
|
||||||
cohort,
|
// cohort,
|
||||||
track,
|
// track,
|
||||||
defaultAssignmentFilter,
|
// defaultAssignmentFilter,
|
||||||
true,
|
// true,
|
||||||
{ searchText },
|
// ));
|
||||||
));
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
dispatch(gradeUpdateFailure(courseId, error));
|
dispatch(gradeUpdateFailure(courseId, error));
|
||||||
@@ -275,49 +158,6 @@ const updateGrades = (courseId, updateData, searchText, cohort, track) => (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const submitFileUploadFormData = (courseId, formData) => (
|
|
||||||
(dispatch) => {
|
|
||||||
dispatch(startedCsvUpload());
|
|
||||||
return LmsApiService.uploadGradeCsv(courseId, formData).then(() => {
|
|
||||||
dispatch(finishedCsvUpload());
|
|
||||||
dispatch(uploadOverrideSuccess(courseId));
|
|
||||||
}).catch((err) => {
|
|
||||||
dispatch(uploadOverrideFailure(courseId, err));
|
|
||||||
if (err.status === 200 && err.data.error_messages.length) {
|
|
||||||
const { error_messages: errorMessages, saved, total } = err.data;
|
|
||||||
return dispatch(csvUploadError({ errorMessages, saved, total }));
|
|
||||||
}
|
|
||||||
return dispatch(csvUploadError({ errorMessages: ['Unknown error.'] }));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchBulkUpgradeHistory = courseId => (
|
|
||||||
dispatch =>
|
|
||||||
// todo add loading effect
|
|
||||||
LmsApiService.fetchGradeBulkOperationHistory(courseId).then((response) => {
|
|
||||||
dispatch(gotBulkHistory(response));
|
|
||||||
}).catch(() => dispatch(bulkHistoryError()))
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateGradesIfAssignmentGradeFiltersSet = (
|
|
||||||
courseId,
|
|
||||||
cohort,
|
|
||||||
track,
|
|
||||||
assignmentType,
|
|
||||||
) => (dispatch, getState) => {
|
|
||||||
const { filters } = getState();
|
|
||||||
const hasAssignmentGradeFiltersSet = filters.assignmentGradeMax || filters.assignmentGradeMin;
|
|
||||||
if (hasAssignmentGradeFiltersSet) {
|
|
||||||
dispatch(fetchGrades(
|
|
||||||
courseId,
|
|
||||||
cohort,
|
|
||||||
track,
|
|
||||||
assignmentType,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
startedFetchingGrades,
|
startedFetchingGrades,
|
||||||
finishedFetchingGrades,
|
finishedFetchingGrades,
|
||||||
@@ -331,13 +171,6 @@ export {
|
|||||||
gradeUpdateFailure,
|
gradeUpdateFailure,
|
||||||
updateGrades,
|
updateGrades,
|
||||||
toggleGradeFormat,
|
toggleGradeFormat,
|
||||||
filterAssignmentType,
|
filterColumns,
|
||||||
closeBanner,
|
updateBanner,
|
||||||
submitFileUploadFormData,
|
|
||||||
fetchBulkUpgradeHistory,
|
|
||||||
doneViewingAssignment,
|
|
||||||
fetchGradeOverrideHistory,
|
|
||||||
updateGradesIfAssignmentGradeFiltersSet,
|
|
||||||
downloadBulkGradesReport,
|
|
||||||
downloadInterventionReport,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,14 +10,13 @@ import {
|
|||||||
FINISHED_FETCHING_GRADES,
|
FINISHED_FETCHING_GRADES,
|
||||||
ERROR_FETCHING_GRADES,
|
ERROR_FETCHING_GRADES,
|
||||||
GOT_GRADES,
|
GOT_GRADES,
|
||||||
|
UPDATE_BANNER,
|
||||||
} from '../constants/actionTypes/grades';
|
} from '../constants/actionTypes/grades';
|
||||||
import { sortAlphaAsc } from './utils';
|
import { sortAlphaAsc } from './utils';
|
||||||
|
|
||||||
|
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
const axiosMock = new MockAdapter(apiClient);
|
const axiosMock = new MockAdapter(apiClient);
|
||||||
apiClient.isAccessTokenExpired = jest.fn();
|
|
||||||
apiClient.isAccessTokenExpired.mockReturnValue(false);
|
|
||||||
|
|
||||||
describe('actions', () => {
|
describe('actions', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -97,11 +96,22 @@ describe('actions', () => {
|
|||||||
cohort: expectedCohort,
|
cohort: expectedCohort,
|
||||||
track: expectedTrack,
|
track: expectedTrack,
|
||||||
assignmentType: expectedAssignmentType,
|
assignmentType: expectedAssignmentType,
|
||||||
|
headings: [
|
||||||
|
{
|
||||||
|
key: 'username',
|
||||||
|
label: 'Username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'total',
|
||||||
|
label: 'Total',
|
||||||
|
},
|
||||||
|
],
|
||||||
prev: responseData.previous,
|
prev: responseData.previous,
|
||||||
next: responseData.next,
|
next: responseData.next,
|
||||||
courseId,
|
courseId,
|
||||||
},
|
},
|
||||||
{ type: FINISHED_FETCHING_GRADES },
|
{ type: FINISHED_FETCHING_GRADES },
|
||||||
|
{ type: UPDATE_BANNER, showSuccess: false },
|
||||||
];
|
];
|
||||||
const store = mockStore();
|
const store = mockStore();
|
||||||
|
|
||||||
@@ -154,11 +164,13 @@ describe('actions', () => {
|
|||||||
cohort: expectedCohort,
|
cohort: expectedCohort,
|
||||||
track: expectedTrack,
|
track: expectedTrack,
|
||||||
assignmentType: expectedAssignmentType,
|
assignmentType: expectedAssignmentType,
|
||||||
|
headings: [],
|
||||||
prev: responseData.previous,
|
prev: responseData.previous,
|
||||||
next: responseData.next,
|
next: responseData.next,
|
||||||
courseId,
|
courseId,
|
||||||
},
|
},
|
||||||
{ type: FINISHED_FETCHING_GRADES },
|
{ type: FINISHED_FETCHING_GRADES },
|
||||||
|
{ type: UPDATE_BANNER, showSuccess: false },
|
||||||
];
|
];
|
||||||
const store = mockStore();
|
const store = mockStore();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { fetchGrades } from './grades';
|
|||||||
import { fetchTracks } from './tracks';
|
import { fetchTracks } from './tracks';
|
||||||
import { fetchCohorts } from './cohorts';
|
import { fetchCohorts } from './cohorts';
|
||||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||||
import { getFilters } from '../selectors/filters';
|
|
||||||
import LmsApiService from '../services/LmsApiService';
|
import LmsApiService from '../services/LmsApiService';
|
||||||
|
|
||||||
const allowedRoles = ['staff', 'instructor', 'support'];
|
const allowedRoles = ['staff', 'instructor', 'support'];
|
||||||
@@ -18,17 +17,16 @@ const gotRoles = (canUserViewGradebook, courseId) => ({
|
|||||||
});
|
});
|
||||||
const errorFetchingRoles = () => ({ type: ERROR_FETCHING_ROLES });
|
const errorFetchingRoles = () => ({ type: ERROR_FETCHING_ROLES });
|
||||||
|
|
||||||
const getRoles = courseId => (
|
const getRoles = (courseId, urlQuery) => (
|
||||||
(dispatch, getState) => LmsApiService.fetchUserRoles(courseId)
|
dispatch => LmsApiService.fetchUserRoles(courseId)
|
||||||
.then(response => response.data)
|
.then(response => response.data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const canUserViewGradebook = response.is_staff
|
const canUserViewGradebook = response.is_staff
|
||||||
|| (response.roles.some(role => (role.course_id === courseId)
|
|| (response.roles.some(role => (role.course_id === courseId)
|
||||||
&& allowedRoles.includes(role.role)));
|
&& allowedRoles.includes(role.role)));
|
||||||
dispatch(gotRoles(canUserViewGradebook, courseId));
|
dispatch(gotRoles(canUserViewGradebook, courseId));
|
||||||
const { cohort, track, assignmentType } = getFilters(getState());
|
|
||||||
if (canUserViewGradebook) {
|
if (canUserViewGradebook) {
|
||||||
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track, urlQuery.assignmentType));
|
||||||
dispatch(fetchTracks(courseId));
|
dispatch(fetchTracks(courseId));
|
||||||
dispatch(fetchCohorts(courseId));
|
dispatch(fetchCohorts(courseId));
|
||||||
dispatch(fetchAssignmentTypes(courseId));
|
dispatch(fetchAssignmentTypes(courseId));
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import { STARTED_FETCHING_ASSIGNMENT_TYPES } from '../constants/actionTypes/assi
|
|||||||
|
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
const axiosMock = new MockAdapter(apiClient);
|
const axiosMock = new MockAdapter(apiClient);
|
||||||
apiClient.isAccessTokenExpired = jest.fn();
|
|
||||||
apiClient.isAccessTokenExpired.mockReturnValue(false);
|
|
||||||
|
|
||||||
const course1Id = 'course-v1:edX+DemoX+Demo_Course';
|
const course1Id = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
const course2Id = 'course-v1:edX+DemoX+Demo_Course_2';
|
const course2Id = 'course-v1:edX+DemoX+Demo_Course_2';
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import {
|
|||||||
GOT_TRACKS,
|
GOT_TRACKS,
|
||||||
ERROR_FETCHING_TRACKS,
|
ERROR_FETCHING_TRACKS,
|
||||||
} from '../constants/actionTypes/tracks';
|
} from '../constants/actionTypes/tracks';
|
||||||
import { hasMastersTrack } from '../selectors/tracks';
|
|
||||||
import { fetchBulkUpgradeHistory } from './grades';
|
|
||||||
import LmsApiService from '../services/LmsApiService';
|
import LmsApiService from '../services/LmsApiService';
|
||||||
|
|
||||||
const startedFetchingTracks = () => ({ type: STARTED_FETCHING_TRACKS });
|
const startedFetchingTracks = () => ({ type: STARTED_FETCHING_TRACKS });
|
||||||
@@ -18,9 +16,6 @@ const fetchTracks = courseId => (
|
|||||||
.then(response => response.data)
|
.then(response => response.data)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
dispatch(gotTracks(data.course_modes));
|
dispatch(gotTracks(data.course_modes));
|
||||||
if (hasMastersTrack(data.course_modes)) {
|
|
||||||
dispatch(fetchBulkUpgradeHistory(courseId));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
dispatch(errorFetchingTracks());
|
dispatch(errorFetchingTracks());
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
|
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
const axiosMock = new MockAdapter(apiClient);
|
const axiosMock = new MockAdapter(apiClient);
|
||||||
apiClient.isAccessTokenExpired = jest.fn();
|
|
||||||
apiClient.isAccessTokenExpired.mockReturnValue(false);
|
|
||||||
|
|
||||||
describe('actions', () => {
|
describe('actions', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -1,19 +1,3 @@
|
|||||||
const formatDateForDisplay = (inputDate) => {
|
|
||||||
const options = {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
timeZone: 'UTC',
|
|
||||||
};
|
|
||||||
const timeOptions = {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
timeZone: 'UTC',
|
|
||||||
timeZoneName: 'short',
|
|
||||||
};
|
|
||||||
return `${inputDate.toLocaleDateString('en-US', options)} at ${inputDate.toLocaleTimeString('en-US', timeOptions)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||||
const a = gradeRowA.username.toUpperCase();
|
const a = gradeRowA.username.toUpperCase();
|
||||||
const b = gradeRowB.username.toUpperCase();
|
const b = gradeRowB.username.toUpperCase();
|
||||||
@@ -26,5 +10,56 @@ const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { sortAlphaAsc, formatDateForDisplay };
|
const headingMapper = (filterKey) => {
|
||||||
|
function all(entry) {
|
||||||
|
if (entry) {
|
||||||
|
const results = [{
|
||||||
|
label: 'Username',
|
||||||
|
key: 'username',
|
||||||
|
}];
|
||||||
|
|
||||||
|
const assignmentHeadings = entry.section_breakdown
|
||||||
|
.filter(section => section.label)
|
||||||
|
.map(s => ({
|
||||||
|
label: s.label,
|
||||||
|
key: s.label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totals = [{
|
||||||
|
label: 'Total',
|
||||||
|
key: 'total',
|
||||||
|
}];
|
||||||
|
|
||||||
|
return results.concat(assignmentHeadings).concat(totals);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function some(entry) {
|
||||||
|
if (!entry) return [];
|
||||||
|
|
||||||
|
const results = [{
|
||||||
|
label: 'Username',
|
||||||
|
key: 'username',
|
||||||
|
}];
|
||||||
|
|
||||||
|
const assignmentHeadings = entry.section_breakdown
|
||||||
|
.filter(section => section.label && section.category === filterKey)
|
||||||
|
.map(s => ({
|
||||||
|
label: s.label,
|
||||||
|
key: s.label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totals = [{
|
||||||
|
label: 'Total',
|
||||||
|
key: 'total',
|
||||||
|
}];
|
||||||
|
|
||||||
|
return results.concat(assignmentHeadings).concat(totals);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterKey === 'All' ? all : some;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { headingMapper, sortAlphaAsc };
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
const GOT_BULK_MANAGEMENT_CONFIG = 'GOT_BULK_MANAGEMENT_CONFIG';
|
|
||||||
|
|
||||||
export default GOT_BULK_MANAGEMENT_CONFIG;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
const INITIALIZE_FILTERS = 'INITIALIZE_FILTERS';
|
|
||||||
const RESET_FILTERS = 'RESET_FILTERS';
|
|
||||||
const UPDATE_ASSIGNMENT_FILTER = 'UPDATE_ASSIGNMENT_FILTER';
|
|
||||||
const UPDATE_ASSIGNMENT_LIMITS = 'UPDATE_ASSIGNMENT_LIMITS';
|
|
||||||
const UPDATE_COURSE_GRADE_LIMITS = 'UPDATE_COURSE_GRADE_LIMITS';
|
|
||||||
export {
|
|
||||||
INITIALIZE_FILTERS, RESET_FILTERS, UPDATE_ASSIGNMENT_FILTER,
|
|
||||||
UPDATE_ASSIGNMENT_LIMITS, UPDATE_COURSE_GRADE_LIMITS,
|
|
||||||
};
|
|
||||||
@@ -2,32 +2,14 @@ const STARTED_FETCHING_GRADES = 'STARTED_FETCHING_GRADES';
|
|||||||
const FINISHED_FETCHING_GRADES = 'FINISHED_FETCHING_GRADES';
|
const FINISHED_FETCHING_GRADES = 'FINISHED_FETCHING_GRADES';
|
||||||
const ERROR_FETCHING_GRADES = 'ERROR_FETCHING_GRADES';
|
const ERROR_FETCHING_GRADES = 'ERROR_FETCHING_GRADES';
|
||||||
const GOT_GRADES = 'GOT_GRADES';
|
const GOT_GRADES = 'GOT_GRADES';
|
||||||
const DONE_VIEWING_ASSIGNMENT = 'DONE_VIEWING_ASSIGNMENT';
|
|
||||||
const GOT_GRADE_OVERRIDE_HISTORY = 'GOT_GRADE_OVERRIDE_HISTORY';
|
|
||||||
const ERROR_FETCHING_GRADE_OVERRIDE_HISTORY = 'ERROR_FETCHING_GRADE_OVERRIDE_HISTORY';
|
|
||||||
|
|
||||||
const FILTER_SELECTED = 'FILTER_SELECTED';
|
|
||||||
const GRADE_OVERRIDE = 'GRADE_OVERRIDE';
|
|
||||||
const REPORT_DOWNLOADED = 'REPORT_DOWNLOADED';
|
|
||||||
const UPLOAD_OVERRIDE = 'UPLOAD_OVERRIDE';
|
|
||||||
const UPLOAD_OVERRIDE_ERROR = 'UPLOAD_OVERRIDE_ERROR';
|
|
||||||
|
|
||||||
const GRADE_UPDATE_REQUEST = 'GRADE_UPDATE_REQUEST';
|
const GRADE_UPDATE_REQUEST = 'GRADE_UPDATE_REQUEST';
|
||||||
const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
|
const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
|
||||||
const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
|
const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
|
||||||
|
|
||||||
const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT';
|
const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT';
|
||||||
const FILTER_BY_ASSIGNMENT_TYPE = 'FILTER_BY_ASSIGNMENT_TYPE';
|
const FILTER_COLUMNS = 'FILTER_COLUMNS';
|
||||||
const CLOSE_BANNER = 'CLOSE_BANNER';
|
const UPDATE_BANNER = 'UPDATE_BANNER';
|
||||||
const OPEN_BANNER = 'OPEN_BANNER';
|
|
||||||
|
|
||||||
const START_UPLOAD = 'START_UPLOAD';
|
|
||||||
const UPLOAD_COMPLETE = 'UPLOAD_COMPLETE';
|
|
||||||
const UPLOAD_ERR = 'UPLOAD_ERR';
|
|
||||||
const GOT_BULK_HISTORY = 'GOT_BULK_HISTORY';
|
|
||||||
const BULK_HISTORY_ERR = 'BULK_HISTORY_ERR';
|
|
||||||
const BULK_GRADE_REPORT_DOWNLOADED = 'BULK_GRADE_REPORT_DOWNLOADED';
|
|
||||||
const INTERVENTION_REPORT_DOWNLOADED = 'INTERVENTION_REPORT_DOWNLOADED';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
STARTED_FETCHING_GRADES,
|
STARTED_FETCHING_GRADES,
|
||||||
@@ -38,22 +20,6 @@ export {
|
|||||||
GRADE_UPDATE_SUCCESS,
|
GRADE_UPDATE_SUCCESS,
|
||||||
GRADE_UPDATE_FAILURE,
|
GRADE_UPDATE_FAILURE,
|
||||||
TOGGLE_GRADE_FORMAT,
|
TOGGLE_GRADE_FORMAT,
|
||||||
FILTER_BY_ASSIGNMENT_TYPE,
|
FILTER_COLUMNS,
|
||||||
OPEN_BANNER,
|
UPDATE_BANNER,
|
||||||
CLOSE_BANNER,
|
|
||||||
START_UPLOAD,
|
|
||||||
UPLOAD_COMPLETE,
|
|
||||||
UPLOAD_ERR,
|
|
||||||
GOT_BULK_HISTORY,
|
|
||||||
BULK_HISTORY_ERR,
|
|
||||||
DONE_VIEWING_ASSIGNMENT,
|
|
||||||
GOT_GRADE_OVERRIDE_HISTORY,
|
|
||||||
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
|
||||||
FILTER_SELECTED,
|
|
||||||
GRADE_OVERRIDE,
|
|
||||||
REPORT_DOWNLOADED,
|
|
||||||
UPLOAD_OVERRIDE,
|
|
||||||
UPLOAD_OVERRIDE_ERROR,
|
|
||||||
BULK_GRADE_REPORT_DOWNLOADED,
|
|
||||||
INTERVENTION_REPORT_DOWNLOADED,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
const initialFilters = {
|
|
||||||
assignment: '',
|
|
||||||
assignmentType: '',
|
|
||||||
track: '',
|
|
||||||
cohort: '',
|
|
||||||
assignmentGradeMin: '0',
|
|
||||||
assignmentGradeMax: '100',
|
|
||||||
courseGradeMin: '0',
|
|
||||||
courseGradeMax: '100',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default initialFilters;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import GOT_BULK_MANAGEMENT_CONFIG from '../constants/actionTypes/config';
|
|
||||||
|
|
||||||
const reducer = (state = {}, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case GOT_BULK_MANAGEMENT_CONFIG:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
bulkManagementAvailable: action.data,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reducer;
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { GOT_GRADES, FILTER_BY_ASSIGNMENT_TYPE } from '../constants/actionTypes/grades';
|
|
||||||
|
|
||||||
import { INITIALIZE_FILTERS, UPDATE_ASSIGNMENT_FILTER, UPDATE_ASSIGNMENT_LIMITS, UPDATE_COURSE_GRADE_LIMITS, RESET_FILTERS } from '../constants/actionTypes/filters';
|
|
||||||
|
|
||||||
import initialFilters from '../constants/filters';
|
|
||||||
|
|
||||||
import { getAssignmentsFromResultsSubstate, chooseRelevantAssignmentData } from '../selectors/filters';
|
|
||||||
|
|
||||||
const initialState = {};
|
|
||||||
|
|
||||||
const reducer = (state = initialState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case FILTER_BY_ASSIGNMENT_TYPE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
assignmentType: action.filterType,
|
|
||||||
assignment: (
|
|
||||||
action.filterType !== '' &&
|
|
||||||
(state.assignment || {}).type !== action.filterType)
|
|
||||||
? '' : state.assignment,
|
|
||||||
};
|
|
||||||
case INITIALIZE_FILTERS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...action.data,
|
|
||||||
};
|
|
||||||
case GOT_GRADES: {
|
|
||||||
const { assignment } = state;
|
|
||||||
const { id, type } = assignment || {};
|
|
||||||
if (!type) {
|
|
||||||
const relevantAssignment = getAssignmentsFromResultsSubstate(action.grades)
|
|
||||||
.map(chooseRelevantAssignmentData)
|
|
||||||
.find(assig => assig.id === id);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
track: action.track,
|
|
||||||
cohort: action.cohort,
|
|
||||||
assignment: relevantAssignment,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
track: action.track,
|
|
||||||
cohort: action.cohort,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case RESET_FILTERS: {
|
|
||||||
const result = { ...state };
|
|
||||||
action.filterNames.forEach((filterName) => {
|
|
||||||
result[filterName] = initialFilters[filterName];
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
case UPDATE_ASSIGNMENT_FILTER:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
assignment: action.data,
|
|
||||||
};
|
|
||||||
case UPDATE_ASSIGNMENT_LIMITS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
assignmentGradeMin: action.data.minGrade,
|
|
||||||
assignmentGradeMax: action.data.maxGrade,
|
|
||||||
};
|
|
||||||
case UPDATE_COURSE_GRADE_LIMITS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
courseGradeMin: action.data.courseGradeMin,
|
|
||||||
courseGradeMax: action.data.courseGradeMax,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reducer;
|
|
||||||
@@ -3,29 +3,12 @@ import {
|
|||||||
ERROR_FETCHING_GRADES,
|
ERROR_FETCHING_GRADES,
|
||||||
GOT_GRADES,
|
GOT_GRADES,
|
||||||
TOGGLE_GRADE_FORMAT,
|
TOGGLE_GRADE_FORMAT,
|
||||||
FILTER_BY_ASSIGNMENT_TYPE,
|
FILTER_COLUMNS,
|
||||||
OPEN_BANNER,
|
UPDATE_BANNER,
|
||||||
CLOSE_BANNER,
|
|
||||||
START_UPLOAD,
|
|
||||||
UPLOAD_COMPLETE,
|
|
||||||
UPLOAD_ERR,
|
|
||||||
GOT_BULK_HISTORY,
|
|
||||||
DONE_VIEWING_ASSIGNMENT,
|
|
||||||
GOT_GRADE_OVERRIDE_HISTORY,
|
|
||||||
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
|
||||||
} from '../constants/actionTypes/grades';
|
} from '../constants/actionTypes/grades';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
results: [],
|
results: [],
|
||||||
gradeOverrideHistoryResults: [],
|
|
||||||
gradeOverrideCurrentEarnedAllOverride: null,
|
|
||||||
gradeOverrideCurrentPossibleAllOverride: null,
|
|
||||||
gradeOverrideCurrentEarnedGradedOverride: null,
|
|
||||||
gradeOverrideCurrentPossibleGradedOverride: null,
|
|
||||||
gradeOriginalEarnedAll: null,
|
|
||||||
gradeOriginalPossibleAll: null,
|
|
||||||
gradeOriginalEarnedGraded: null,
|
|
||||||
gradeOriginalPossibleGraded: null,
|
|
||||||
headings: [],
|
headings: [],
|
||||||
startedFetching: false,
|
startedFetching: false,
|
||||||
finishedFetching: false,
|
finishedFetching: false,
|
||||||
@@ -35,9 +18,6 @@ const initialState = {
|
|||||||
prevPage: null,
|
prevPage: null,
|
||||||
nextPage: null,
|
nextPage: null,
|
||||||
showSpinner: true,
|
showSpinner: true,
|
||||||
bulkManagement: {},
|
|
||||||
totalUsersCount: 0,
|
|
||||||
filteredUsersCount: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const grades = (state = initialState, action) => {
|
const grades = (state = initialState, action) => {
|
||||||
@@ -49,50 +29,14 @@ const grades = (state = initialState, action) => {
|
|||||||
headings: action.headings,
|
headings: action.headings,
|
||||||
finishedFetching: true,
|
finishedFetching: true,
|
||||||
errorFetching: false,
|
errorFetching: false,
|
||||||
|
selectedTrack: action.track,
|
||||||
|
selectedCohort: action.cohort,
|
||||||
|
selectedAssignmentType: action.assignmentType,
|
||||||
prevPage: action.prev,
|
prevPage: action.prev,
|
||||||
nextPage: action.next,
|
nextPage: action.next,
|
||||||
showSpinner: false,
|
showSpinner: false,
|
||||||
courseId: action.courseId,
|
courseId: action.courseId,
|
||||||
totalUsersCount: action.totalUsersCount,
|
|
||||||
filteredUsersCount: action.filteredUsersCount,
|
|
||||||
};
|
};
|
||||||
case DONE_VIEWING_ASSIGNMENT: {
|
|
||||||
const {
|
|
||||||
gradeOverrideHistoryResults,
|
|
||||||
gradeOverrideCurrentEarnedAllOverride,
|
|
||||||
gradeOverrideCurrentPossibleAllOverride,
|
|
||||||
gradeOverrideCurrentEarnedGradedOverride,
|
|
||||||
gradeOverrideCurrentPossibleGradedOverride,
|
|
||||||
gradeOriginalEarnedAll,
|
|
||||||
gradeOriginalPossibleAll,
|
|
||||||
gradeOriginalEarnedGraded,
|
|
||||||
gradeOriginalPossibleGraded,
|
|
||||||
...rest
|
|
||||||
} = state;
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
case GOT_GRADE_OVERRIDE_HISTORY:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
gradeOverrideHistoryResults: action.overrideHistory,
|
|
||||||
gradeOverrideCurrentEarnedAllOverride: action.currentEarnedAllOverride,
|
|
||||||
gradeOverrideCurrentPossibleAllOverride: action.currentPossibleAllOverride,
|
|
||||||
gradeOverrideCurrentEarnedGradedOverride: action.currentEarnedGradedOverride,
|
|
||||||
gradeOverrideCurrentPossibleGradedOverride: action.currentPossibleGradedOverride,
|
|
||||||
gradeOriginalEarnedAll: action.originalGradeEarnedAll,
|
|
||||||
gradeOriginalPossibleAll: action.originalGradePossibleAll,
|
|
||||||
gradeOriginalEarnedGraded: action.originalGradeEarnedGraded,
|
|
||||||
gradeOriginalPossibleGraded: action.originalGradePossibleGraded,
|
|
||||||
errorFetchingOverrideHistory: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
case ERROR_FETCHING_GRADE_OVERRIDE_HISTORY:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
finishedFetchingOverrideHistory: true,
|
|
||||||
errorFetchingOverrideHistory: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
case STARTED_FETCHING_GRADES:
|
case STARTED_FETCHING_GRADES:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -111,57 +55,19 @@ const grades = (state = initialState, action) => {
|
|||||||
...state,
|
...state,
|
||||||
gradeFormat: action.formatType,
|
gradeFormat: action.formatType,
|
||||||
};
|
};
|
||||||
case FILTER_BY_ASSIGNMENT_TYPE:
|
case FILTER_COLUMNS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
selectedAssignmentType: action.filterType,
|
|
||||||
headings: action.headings,
|
headings: action.headings,
|
||||||
};
|
};
|
||||||
case OPEN_BANNER:
|
case UPDATE_BANNER:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
showSuccess: true,
|
showSuccess: action.showSuccess,
|
||||||
};
|
|
||||||
case CLOSE_BANNER:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
showSuccess: false,
|
|
||||||
};
|
|
||||||
case START_UPLOAD: {
|
|
||||||
const { errorMessages, uploadSuccess, ...rest } = state.bulkManagement;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
showSpinner: true,
|
|
||||||
bulkManagement: rest,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case UPLOAD_COMPLETE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
showSpinner: false,
|
|
||||||
bulkManagement: { uploadSuccess: true, ...state.bulkManagement },
|
|
||||||
};
|
|
||||||
case UPLOAD_ERR:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
showSpinner: false,
|
|
||||||
bulkManagement: {
|
|
||||||
...state.bulkManagement,
|
|
||||||
...action.data,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case GOT_BULK_HISTORY:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
bulkManagement: {
|
|
||||||
...state.bulkManagement,
|
|
||||||
history: action.data,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { initialState as initialGradesState };
|
|
||||||
export default grades;
|
export default grades;
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
import grades, { initialGradesState as initialState } from './grades';
|
import grades from './grades';
|
||||||
import {
|
import {
|
||||||
STARTED_FETCHING_GRADES,
|
STARTED_FETCHING_GRADES,
|
||||||
ERROR_FETCHING_GRADES,
|
ERROR_FETCHING_GRADES,
|
||||||
GOT_GRADES,
|
GOT_GRADES,
|
||||||
TOGGLE_GRADE_FORMAT,
|
TOGGLE_GRADE_FORMAT,
|
||||||
FILTER_BY_ASSIGNMENT_TYPE,
|
FILTER_COLUMNS,
|
||||||
OPEN_BANNER,
|
UPDATE_BANNER,
|
||||||
} from '../constants/actionTypes/grades';
|
} from '../constants/actionTypes/grades';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
results: [],
|
||||||
|
headings: [],
|
||||||
|
startedFetching: false,
|
||||||
|
finishedFetching: false,
|
||||||
|
errorFetching: false,
|
||||||
|
gradeFormat: 'percent',
|
||||||
|
showSuccess: false,
|
||||||
|
prevPage: null,
|
||||||
|
nextPage: null,
|
||||||
|
showSpinner: true,
|
||||||
|
};
|
||||||
|
|
||||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||||
const headingsData = [
|
const headingsData = [
|
||||||
{ name: 'exam' },
|
{ name: 'exam' },
|
||||||
@@ -94,12 +107,12 @@ describe('grades reducer', () => {
|
|||||||
headings: headingsData,
|
headings: headingsData,
|
||||||
errorFetching: false,
|
errorFetching: false,
|
||||||
finishedFetching: true,
|
finishedFetching: true,
|
||||||
|
selectedTrack: expectedTrack,
|
||||||
|
selectedCohort: expectedCohortId,
|
||||||
prevPage: expectedPrev,
|
prevPage: expectedPrev,
|
||||||
nextPage: expectedNext,
|
nextPage: expectedNext,
|
||||||
showSpinner: false,
|
showSpinner: false,
|
||||||
courseId,
|
courseId,
|
||||||
totalUsersCount: 4,
|
|
||||||
filteredUsersCount: 2,
|
|
||||||
};
|
};
|
||||||
expect(grades(undefined, {
|
expect(grades(undefined, {
|
||||||
type: GOT_GRADES,
|
type: GOT_GRADES,
|
||||||
@@ -111,8 +124,6 @@ describe('grades reducer', () => {
|
|||||||
cohort: expectedCohortId,
|
cohort: expectedCohortId,
|
||||||
showSpinner: true,
|
showSpinner: true,
|
||||||
courseId,
|
courseId,
|
||||||
totalUsersCount: 4,
|
|
||||||
filteredUsersCount: 2,
|
|
||||||
})).toEqual(expected);
|
})).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,7 +146,7 @@ describe('grades reducer', () => {
|
|||||||
headings: expectedHeadings,
|
headings: expectedHeadings,
|
||||||
};
|
};
|
||||||
expect(grades(undefined, {
|
expect(grades(undefined, {
|
||||||
type: FILTER_BY_ASSIGNMENT_TYPE,
|
type: FILTER_COLUMNS,
|
||||||
headings: expectedHeadings,
|
headings: expectedHeadings,
|
||||||
})).toEqual(expected);
|
})).toEqual(expected);
|
||||||
});
|
});
|
||||||
@@ -147,7 +158,8 @@ describe('grades reducer', () => {
|
|||||||
showSuccess: expectedShowSuccess,
|
showSuccess: expectedShowSuccess,
|
||||||
};
|
};
|
||||||
expect(grades(undefined, {
|
expect(grades(undefined, {
|
||||||
type: OPEN_BANNER,
|
type: UPDATE_BANNER,
|
||||||
|
showSuccess: expectedShowSuccess,
|
||||||
})).toEqual(expected);
|
})).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import grades from './grades';
|
|||||||
import tracks from './tracks';
|
import tracks from './tracks';
|
||||||
import assignmentTypes from './assignmentTypes';
|
import assignmentTypes from './assignmentTypes';
|
||||||
import roles from './roles';
|
import roles from './roles';
|
||||||
import filters from './filters';
|
|
||||||
import config from './config';
|
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
grades,
|
grades,
|
||||||
@@ -14,8 +12,6 @@ const rootReducer = combineReducers({
|
|||||||
tracks,
|
tracks,
|
||||||
assignmentTypes,
|
assignmentTypes,
|
||||||
roles,
|
roles,
|
||||||
filters,
|
|
||||||
config,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
const getCohorts = state => state.cohorts.results || [];
|
|
||||||
|
|
||||||
const getCohortById = (state, selectedCohortId) => {
|
|
||||||
const cohort = getCohorts(state).find(coh => coh.id === selectedCohortId);
|
|
||||||
return cohort;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCohortNameById = (state, selectedCohortId) =>
|
|
||||||
(getCohortById(state, selectedCohortId) || {}).name;
|
|
||||||
|
|
||||||
export { getCohortById, getCohortNameById, getCohorts };
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
const getFilters = state => state.filters || {};
|
|
||||||
|
|
||||||
const getAssignmentsFromResultsSubstate = results =>
|
|
||||||
(results[0] || {}).section_breakdown || [];
|
|
||||||
|
|
||||||
const selectableAssignments = (state) => {
|
|
||||||
const selectedAssignmentType = getFilters(state).assignmentType;
|
|
||||||
const needToFilter = selectedAssignmentType && selectedAssignmentType !== 'All';
|
|
||||||
const allAssignments = getAssignmentsFromResultsSubstate(state.grades.results);
|
|
||||||
if (needToFilter) {
|
|
||||||
return allAssignments.filter(assignment => assignment.category === selectedAssignmentType);
|
|
||||||
}
|
|
||||||
return allAssignments;
|
|
||||||
};
|
|
||||||
|
|
||||||
const chooseRelevantAssignmentData = assignment => ({
|
|
||||||
label: assignment.label,
|
|
||||||
subsectionLabel: assignment.subsection_name,
|
|
||||||
type: assignment.category,
|
|
||||||
id: assignment.module_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectableAssignmentLabels = state =>
|
|
||||||
selectableAssignments(state).map(chooseRelevantAssignmentData);
|
|
||||||
|
|
||||||
const typeOfSelectedAssignment = (state) => {
|
|
||||||
const selectedAssignmentLabel = getFilters(state).assignment;
|
|
||||||
const sectionBreakdown = (state.grades.results[0] || {}).section_breakdown || [];
|
|
||||||
const selectedAssignment = sectionBreakdown.find(section =>
|
|
||||||
section.label === selectedAssignmentLabel);
|
|
||||||
return selectedAssignment && selectedAssignment.category;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
selectableAssignmentLabels,
|
|
||||||
selectableAssignments,
|
|
||||||
getFilters,
|
|
||||||
typeOfSelectedAssignment,
|
|
||||||
chooseRelevantAssignmentData,
|
|
||||||
getAssignmentsFromResultsSubstate,
|
|
||||||
};
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import { formatDateForDisplay } from '../actions/utils';
|
|
||||||
import { getFilters } from './filters';
|
|
||||||
|
|
||||||
const getRowsProcessed = (data) => {
|
|
||||||
const {
|
|
||||||
processed_rows: processed,
|
|
||||||
saved_rows: saved,
|
|
||||||
total_rows: total,
|
|
||||||
} = data;
|
|
||||||
return {
|
|
||||||
total,
|
|
||||||
successfullyProcessed: saved,
|
|
||||||
failed: processed - saved,
|
|
||||||
skipped: total - processed,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const transformHistoryEntry = (historyRow) => {
|
|
||||||
const {
|
|
||||||
modified,
|
|
||||||
original_filename: originalFilename,
|
|
||||||
data,
|
|
||||||
...rest
|
|
||||||
} = historyRow;
|
|
||||||
|
|
||||||
const timeUploaded = formatDateForDisplay(new Date(modified));
|
|
||||||
const summaryOfRowsProcessed = getRowsProcessed(data);
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeUploaded,
|
|
||||||
originalFilename,
|
|
||||||
summaryOfRowsProcessed,
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const getBulkManagementHistoryFromState = state =>
|
|
||||||
state.grades.bulkManagement.history || [];
|
|
||||||
const getBulkManagementHistory = state =>
|
|
||||||
getBulkManagementHistoryFromState(state).map(transformHistoryEntry);
|
|
||||||
|
|
||||||
const headingMapper = (category, label = 'All') => {
|
|
||||||
const filters = {
|
|
||||||
all: section => section.label,
|
|
||||||
byCategory: section => section.label && section.category === category,
|
|
||||||
byLabel: section => section.label && section.label === label,
|
|
||||||
};
|
|
||||||
|
|
||||||
let filter;
|
|
||||||
if (label === 'All') {
|
|
||||||
filter = category === 'All' ? 'all' : 'byCategory';
|
|
||||||
} else {
|
|
||||||
filter = 'byLabel';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (entry) => {
|
|
||||||
if (entry) {
|
|
||||||
const results = ['Username', 'Email'];
|
|
||||||
|
|
||||||
const assignmentHeadings = entry
|
|
||||||
.filter(filters[filter])
|
|
||||||
.map(s => s.label);
|
|
||||||
|
|
||||||
const totals = ['Total'];
|
|
||||||
|
|
||||||
return results.concat(assignmentHeadings).concat(totals);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getExampleSectionBreakdown = state => (state.grades.results[0] || {}).section_breakdown || [];
|
|
||||||
|
|
||||||
const getHeadings = (state) => {
|
|
||||||
const filters = getFilters(state) || {};
|
|
||||||
const {
|
|
||||||
assignmentType: selectedAssignmentType,
|
|
||||||
assignment: selectedAssignment,
|
|
||||||
} = filters;
|
|
||||||
const assignments = getExampleSectionBreakdown(state);
|
|
||||||
const type = selectedAssignmentType || 'All';
|
|
||||||
const assignment = (selectedAssignment || {}).label || 'All';
|
|
||||||
return headingMapper(type, assignment)(assignments);
|
|
||||||
};
|
|
||||||
|
|
||||||
const composeFilters = (...predicates) => (percentGrade, options = {}) =>
|
|
||||||
predicates.reduce((accum, predicate) => {
|
|
||||||
if (predicate(percentGrade, options)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return accum;
|
|
||||||
}, percentGrade);
|
|
||||||
|
|
||||||
const percentGradeIsMax = percentGrade => (
|
|
||||||
percentGrade === '100'
|
|
||||||
);
|
|
||||||
|
|
||||||
const percentGradeIsMin = percentGrade => (
|
|
||||||
percentGrade === '0'
|
|
||||||
);
|
|
||||||
|
|
||||||
const assignmentIdIsDefined = (percentGrade, { assignmentId }) => (
|
|
||||||
!assignmentId
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatMaxCourseGrade = composeFilters(percentGradeIsMax);
|
|
||||||
const formatMinCourseGrade = composeFilters(percentGradeIsMin);
|
|
||||||
const formatMaxAssignmentGrade = composeFilters(
|
|
||||||
percentGradeIsMax,
|
|
||||||
assignmentIdIsDefined,
|
|
||||||
);
|
|
||||||
const formatMinAssignmentGrade = composeFilters(
|
|
||||||
percentGradeIsMin,
|
|
||||||
assignmentIdIsDefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
export {
|
|
||||||
getBulkManagementHistory,
|
|
||||||
getHeadings,
|
|
||||||
formatMinAssignmentGrade,
|
|
||||||
formatMaxAssignmentGrade,
|
|
||||||
formatMaxCourseGrade,
|
|
||||||
formatMinCourseGrade,
|
|
||||||
};
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { getBulkManagementHistory } from './grades';
|
|
||||||
|
|
||||||
const genericHistoryRow = {
|
|
||||||
id: 5,
|
|
||||||
class_name: 'bulk_grades.api.GradeCSVProcessor',
|
|
||||||
unique_id: 'course-v1:google+goog101+2018_spring',
|
|
||||||
operation: 'commit',
|
|
||||||
user: 'edx',
|
|
||||||
modified: '2019-07-16T20:25:46.700802Z',
|
|
||||||
original_filename: '',
|
|
||||||
data: {
|
|
||||||
total_rows: 5,
|
|
||||||
processed_rows: 3,
|
|
||||||
saved_rows: 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('getBulkManagementHistory', () => {
|
|
||||||
it('handles history being as-yet unloaded', () => {
|
|
||||||
const result = getBulkManagementHistory({ grades: { bulkManagement: {} } });
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('formats dates for us', () => {
|
|
||||||
const result = getBulkManagementHistory({
|
|
||||||
grades: {
|
|
||||||
bulkManagement: {
|
|
||||||
history: [
|
|
||||||
genericHistoryRow,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [{ timeUploaded }] = result;
|
|
||||||
expect(timeUploaded).not.toMatch(/Z$/);
|
|
||||||
expect(timeUploaded).toContain(' at ');
|
|
||||||
});
|
|
||||||
|
|
||||||
const exerciseGetRowsProcessed = (input, expectation) => {
|
|
||||||
const result = getBulkManagementHistory({
|
|
||||||
grades: {
|
|
||||||
bulkManagement: {
|
|
||||||
history: [
|
|
||||||
{ ...genericHistoryRow, data: input },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [{ summaryOfRowsProcessed }] = result;
|
|
||||||
expect(summaryOfRowsProcessed).toEqual(expect.objectContaining(expectation));
|
|
||||||
};
|
|
||||||
|
|
||||||
it('calculates skippage', () => {
|
|
||||||
exerciseGetRowsProcessed({
|
|
||||||
total_rows: 100,
|
|
||||||
processed_rows: 10,
|
|
||||||
saved_rows: 10,
|
|
||||||
}, {
|
|
||||||
skipped: 90,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calculates failures', () => {
|
|
||||||
exerciseGetRowsProcessed({
|
|
||||||
total_rows: 10,
|
|
||||||
processed_rows: 100,
|
|
||||||
saved_rows: 10,
|
|
||||||
}, {
|
|
||||||
failed: 90,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const compose = (...fns) => {
|
|
||||||
const [firstFunc, ...rest] = fns.reverse();
|
|
||||||
return (...args) =>
|
|
||||||
rest.reduce((accum, fn) => fn(accum), firstFunc(...args));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTracks = state => state.tracks.results || [];
|
|
||||||
const trackIsMasters = track => track.slug === 'masters';
|
|
||||||
const hasMastersTrack = tracks => tracks.some(trackIsMasters);
|
|
||||||
const stateHasMastersTrack = compose(hasMastersTrack, getTracks);
|
|
||||||
|
|
||||||
export { hasMastersTrack, trackIsMasters };
|
|
||||||
export default stateHasMastersTrack;
|
|
||||||
@@ -5,43 +5,19 @@ class LmsApiService {
|
|||||||
static baseUrl = configuration.LMS_BASE_URL;
|
static baseUrl = configuration.LMS_BASE_URL;
|
||||||
static pageSize = 25
|
static pageSize = 25
|
||||||
|
|
||||||
static fetchGradebookData(courseId, searchText, cohort, track, options = {}) {
|
static fetchGradebookData(courseId, searchText, cohort, track) {
|
||||||
const queryParams = {};
|
let gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/`;
|
||||||
queryParams.page_size = LmsApiService.pageSize;
|
|
||||||
|
gradebookUrl += `?page_size=${LmsApiService.pageSize}&`;
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
queryParams.user_contains = searchText;
|
gradebookUrl += `username_contains=${searchText}&`;
|
||||||
}
|
}
|
||||||
if (cohort) {
|
if (cohort) {
|
||||||
queryParams.cohort_id = cohort;
|
gradebookUrl += `cohort_id=${cohort}&`;
|
||||||
}
|
}
|
||||||
if (track) {
|
if (track) {
|
||||||
queryParams.enrollment_mode = track;
|
gradebookUrl += `enrollment_mode=${track}`;
|
||||||
}
|
}
|
||||||
if (options.assignmentGradeMax || options.assignmentGradeMin) {
|
|
||||||
if (!options.assignment) {
|
|
||||||
throw new Error('Gradebook LMS API requires assignment to be set to filter by min/max assig. grade');
|
|
||||||
}
|
|
||||||
queryParams.assignment = options.assignment;
|
|
||||||
if (options.assignmentGradeMin) {
|
|
||||||
queryParams.assignment_grade_min = options.assignmentGradeMin;
|
|
||||||
}
|
|
||||||
if (options.assignmentGradeMax) {
|
|
||||||
queryParams.assignment_grade_max = options.assignmentGradeMax;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (options.courseGradeMin) {
|
|
||||||
queryParams.course_grade_min = options.courseGradeMin;
|
|
||||||
}
|
|
||||||
if (options.courseGradeMax) {
|
|
||||||
queryParams.course_grade_max = options.courseGradeMax;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryParamString = Object.keys(queryParams)
|
|
||||||
.map(attr => `${attr}=${encodeURIComponent(queryParams[attr])}`)
|
|
||||||
.join('&');
|
|
||||||
|
|
||||||
const gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/?${queryParamString}`;
|
|
||||||
|
|
||||||
return apiClient.get(gradebookUrl);
|
return apiClient.get(gradebookUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +39,7 @@ class LmsApiService {
|
|||||||
"earned_all_override": 11,
|
"earned_all_override": 11,
|
||||||
"possible_all_override": 11,
|
"possible_all_override": 11,
|
||||||
"earned_graded_override": 11,
|
"earned_graded_override": 11,
|
||||||
"possible_graded_override": 11,
|
"possible_graded_override": 11
|
||||||
"comment": "reason for override"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -92,48 +67,6 @@ class LmsApiService {
|
|||||||
const rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(courseId)}`;
|
const rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(courseId)}`;
|
||||||
return apiClient.get(rolesUrl);
|
return apiClient.get(rolesUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getGradeExportCsvUrl(courseId, options = {}) {
|
|
||||||
const queryParams = ['track', 'cohort', 'assignment', 'assignmentType', 'assignmentGradeMax',
|
|
||||||
'assignmentGradeMin', 'courseGradeMin', 'courseGradeMax']
|
|
||||||
.filter(opt => options[opt] &&
|
|
||||||
options[opt] !== 'All')
|
|
||||||
.map(opt => `${opt}=${encodeURIComponent(options[opt])}`)
|
|
||||||
.join('&');
|
|
||||||
return `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}/?${queryParams}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getInterventionExportCsvUrl(courseId, options = {}) {
|
|
||||||
const queryParams = ['track', 'cohort', 'assignment', 'assignmentType', 'assignmentGradeMax',
|
|
||||||
'assignmentGradeMin', 'courseGradeMin', 'courseGradeMax']
|
|
||||||
.filter(opt => options[opt] &&
|
|
||||||
options[opt] !== 'All')
|
|
||||||
.map(opt => `${opt}=${encodeURIComponent(options[opt])}`)
|
|
||||||
.join('&');
|
|
||||||
return `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}/intervention?${queryParams}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getGradeImportCsvUrl = LmsApiService.getGradeExportCsvUrl;
|
|
||||||
|
|
||||||
static uploadGradeCsv(courseId, formData) {
|
|
||||||
const fileUploadUrl = LmsApiService.getGradeImportCsvUrl(courseId);
|
|
||||||
return apiClient.post(fileUploadUrl, formData).then((result) => {
|
|
||||||
if (result.status === 200 && !result.data.error_messages.length) {
|
|
||||||
return result.data;
|
|
||||||
}
|
|
||||||
return Promise.reject(result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static fetchGradeBulkOperationHistory(courseId) {
|
|
||||||
const url = `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}/history/`;
|
|
||||||
return apiClient.get(url).then(response => response.data).catch(() => Promise.reject(Error('unhandled response error')));
|
|
||||||
}
|
|
||||||
|
|
||||||
static fetchGradeOverrideHistory(subsectionId, userId) {
|
|
||||||
const historyUrl = `${LmsApiService.baseUrl}/api/grades/v1/subsection/${subsectionId}/?user_id=${userId}&history_record_limit=5`;
|
|
||||||
return apiClient.get(historyUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LmsApiService;
|
export default LmsApiService;
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import { createLogger } from 'redux-logger';
|
|||||||
import { createMiddleware } from 'redux-beacon';
|
import { createMiddleware } from 'redux-beacon';
|
||||||
import Segment, { trackEvent, trackPageView } from '@redux-beacon/segment';
|
import Segment, { trackEvent, trackPageView } from '@redux-beacon/segment';
|
||||||
import { GOT_ROLES } from './constants/actionTypes/roles';
|
import { GOT_ROLES } from './constants/actionTypes/roles';
|
||||||
import {
|
import { GOT_GRADES, GRADE_UPDATE_SUCCESS, GRADE_UPDATE_FAILURE } from './constants/actionTypes/grades';
|
||||||
GOT_GRADES, GRADE_UPDATE_SUCCESS, GRADE_UPDATE_FAILURE, UPLOAD_OVERRIDE,
|
|
||||||
UPLOAD_OVERRIDE_ERROR, BULK_GRADE_REPORT_DOWNLOADED, INTERVENTION_REPORT_DOWNLOADED,
|
|
||||||
} from './constants/actionTypes/grades';
|
|
||||||
import { UPDATE_COURSE_GRADE_LIMITS } from './constants/actionTypes/filters';
|
|
||||||
|
|
||||||
import reducers from './reducers';
|
import reducers from './reducers';
|
||||||
|
|
||||||
@@ -22,10 +18,10 @@ const eventsMap = {
|
|||||||
page: action.courseId,
|
page: action.courseId,
|
||||||
})),
|
})),
|
||||||
[GOT_GRADES]: trackEvent(action => ({
|
[GOT_GRADES]: trackEvent(action => ({
|
||||||
name: 'edx.gradebook.grades.displayed',
|
name: 'Grades displayed or paginated',
|
||||||
properties: {
|
properties: {
|
||||||
category: trackingCategory,
|
category: trackingCategory,
|
||||||
label: action.courseId,
|
courseId: action.courseId,
|
||||||
track: action.track,
|
track: action.track,
|
||||||
cohort: action.cohort,
|
cohort: action.cohort,
|
||||||
assignmentType: action.assignmentType,
|
assignmentType: action.assignmentType,
|
||||||
@@ -34,58 +30,21 @@ const eventsMap = {
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
[GRADE_UPDATE_SUCCESS]: trackEvent(action => ({
|
[GRADE_UPDATE_SUCCESS]: trackEvent(action => ({
|
||||||
name: 'edx.gradebook.grades.grade_override.succeeded',
|
name: 'Grades Updated',
|
||||||
properties: {
|
properties: {
|
||||||
category: trackingCategory,
|
category: trackingCategory,
|
||||||
label: action.courseId,
|
courseId: action.courseId,
|
||||||
updatedGrades: action.payload.responseData,
|
updatedGrades: action.payload.responseData,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
[GRADE_UPDATE_FAILURE]: trackEvent(action => ({
|
[GRADE_UPDATE_FAILURE]: trackEvent(action => ({
|
||||||
name: 'edx.gradebook.grades.grade_override.failed',
|
name: 'Grades Fail to Update',
|
||||||
properties: {
|
properties: {
|
||||||
category: trackingCategory,
|
category: trackingCategory,
|
||||||
label: action.courseId,
|
courseId: action.courseId,
|
||||||
error: action.payload.error,
|
error: action.payload.error,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
[UPLOAD_OVERRIDE]: trackEvent(action => ({
|
|
||||||
name: 'edx.gradebook.grades.upload.grades_overrides.succeeded',
|
|
||||||
properties: {
|
|
||||||
category: trackingCategory,
|
|
||||||
label: action.courseId,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
[UPLOAD_OVERRIDE_ERROR]: trackEvent(action => ({
|
|
||||||
name: 'edx.gradebook.grades.upload.grades_overrides.failed',
|
|
||||||
properties: {
|
|
||||||
category: trackingCategory,
|
|
||||||
label: action.courseId,
|
|
||||||
error: action.payload.error,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
[UPDATE_COURSE_GRADE_LIMITS]: trackEvent(action => ({
|
|
||||||
name: 'edx.gradebook.grades.filter_applied',
|
|
||||||
label: action.courseId,
|
|
||||||
properties: {
|
|
||||||
category: trackingCategory,
|
|
||||||
label: action.courseId,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
[BULK_GRADE_REPORT_DOWNLOADED]: trackEvent(action => ({
|
|
||||||
name: 'edx.gradebook.reports.grade_export.downloaded',
|
|
||||||
properties: {
|
|
||||||
category: trackingCategory,
|
|
||||||
label: action.courseId,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
[INTERVENTION_REPORT_DOWNLOADED]: trackEvent(action => ({
|
|
||||||
name: 'edx.gradebook.reports.intervention.downloaded',
|
|
||||||
properties: {
|
|
||||||
category: trackingCategory,
|
|
||||||
label: action.courseId,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const segmentMiddleware = createMiddleware(eventsMap, Segment());
|
const segmentMiddleware = createMiddleware(eventsMap, Segment());
|
||||||
@@ -96,5 +55,4 @@ const store = createStore(
|
|||||||
composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware, segmentMiddleware)),
|
composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware, segmentMiddleware)),
|
||||||
);
|
);
|
||||||
|
|
||||||
export { trackingCategory };
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|||||||
@@ -4,15 +4,6 @@ import ReactDOM from 'react-dom';
|
|||||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import SiteFooter from '@edx/frontend-component-footer';
|
import SiteFooter from '@edx/frontend-component-footer';
|
||||||
import { IntlProvider } from 'react-intl';
|
|
||||||
|
|
||||||
import {
|
|
||||||
faFacebookSquare,
|
|
||||||
faTwitterSquare,
|
|
||||||
faLinkedin,
|
|
||||||
faRedditSquare,
|
|
||||||
} from '@fortawesome/free-brands-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
|
|
||||||
import apiClient from './data/apiClient';
|
import apiClient from './data/apiClient';
|
||||||
import GradebookPage from './containers/GradebookPage';
|
import GradebookPage from './containers/GradebookPage';
|
||||||
@@ -21,42 +12,14 @@ import store from './data/store';
|
|||||||
import FooterLogo from '../assets/edx-footer.png';
|
import FooterLogo from '../assets/edx-footer.png';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
const socialLinks = [
|
|
||||||
{
|
|
||||||
title: 'Facebook',
|
|
||||||
url: process.env.FACEBOOK_URL,
|
|
||||||
icon: <FontAwesomeIcon icon={faFacebookSquare} className="social-icon" size="2x" />,
|
|
||||||
screenReaderText: 'Like edX on Facebook',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Twitter',
|
|
||||||
url: process.env.TWITTER_URL,
|
|
||||||
icon: <FontAwesomeIcon icon={faTwitterSquare} className="social-icon" size="2x" />,
|
|
||||||
screenReaderText: 'Follow edX on Twitter',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'LinkedIn',
|
|
||||||
url: process.env.LINKED_IN_URL,
|
|
||||||
icon: <FontAwesomeIcon icon={faLinkedin} className="social-icon" size="2x" />,
|
|
||||||
screenReaderText: 'Follow edX on LinkedIn',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Reddit',
|
|
||||||
url: process.env.REDDIT_URL,
|
|
||||||
icon: <FontAwesomeIcon icon={faRedditSquare} className="social-icon" size="2x" />,
|
|
||||||
screenReaderText: 'Subscribe to the edX subreddit',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<IntlProvider>
|
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router>
|
<Router>
|
||||||
<div>
|
<div>
|
||||||
<Header />
|
<Header />
|
||||||
<main>
|
<main>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/gradebook/:courseId" component={GradebookPage} />
|
<Route exact path="/:courseId" component={GradebookPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
<SiteFooter
|
<SiteFooter
|
||||||
@@ -68,27 +31,20 @@ const App = () => (
|
|||||||
openSourceUrl={process.env.OPEN_SOURCE_URL}
|
openSourceUrl={process.env.OPEN_SOURCE_URL}
|
||||||
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
|
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
|
||||||
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
|
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
|
||||||
|
facebookUrl={process.env.FACEBOOK_URL}
|
||||||
|
twitterUrl={process.env.TWITTER_URL}
|
||||||
|
youTubeUrl={process.env.YOU_TUBE_URL}
|
||||||
|
linkedInUrl={process.env.LINKED_IN_URL}
|
||||||
|
googlePlusUrl={process.env.GOOGLE_PLUS_URL}
|
||||||
|
redditUrl={process.env.REDDIT_URL}
|
||||||
appleAppStoreUrl={process.env.APPLE_APP_STORE_URL}
|
appleAppStoreUrl={process.env.APPLE_APP_STORE_URL}
|
||||||
googlePlayUrl={process.env.GOOGLE_PLAY_URL}
|
googlePlayUrl={process.env.GOOGLE_PLAY_URL}
|
||||||
socialLinks={socialLinks}
|
|
||||||
enterpriseMarketingLink={{
|
|
||||||
url: process.env.ENTERPRISE_MARKETING_URL,
|
|
||||||
queryParams: {
|
|
||||||
utm_source: process.env.ENTERPRISE_MARKETING_UTM_SOURCE,
|
|
||||||
utm_campaign: process.env.ENTERPRISE_MARKETING_UTM_CAMPAIGN,
|
|
||||||
utm_medium: process.env.ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
</IntlProvider>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
apiClient.ensurePublicOrAuthenticationAndCookies(
|
if (apiClient.ensurePublicOrAuthencationAndCookies(window.location.pathname)) {
|
||||||
window.location.pathname,
|
|
||||||
() => {
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'));
|
ReactDOM.render(<App />, document.getElementById('root'));
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user