Compare commits
88 Commits
v1.0.1
...
douglashal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d647f381 | ||
|
|
bea36fb387 | ||
|
|
1408b0ae7e | ||
|
|
7b817a4234 | ||
|
|
a762c47d77 | ||
|
|
aecb93c252 | ||
|
|
5a489b1bd5 | ||
|
|
5c642a1be5 | ||
|
|
9a0e0e0ece | ||
|
|
7486a342e2 | ||
|
|
fd807c54f8 | ||
|
|
9b894b502f | ||
|
|
afd9692f6b | ||
|
|
8f77dea222 | ||
|
|
3bf3acaaec | ||
|
|
f1ab3d0330 | ||
|
|
8d89bc16b1 | ||
|
|
d93476c198 | ||
|
|
b46d47286b | ||
|
|
4aecfce14a | ||
|
|
14f7ad01b8 | ||
|
|
104cb30ef5 | ||
|
|
e9bc4cebe4 | ||
|
|
559180592c | ||
|
|
0f1f0ae89d | ||
|
|
a812ee3816 | ||
|
|
2e725e0441 | ||
|
|
a1c2ccc539 | ||
|
|
a70ddd79f6 | ||
|
|
dd82054bbc | ||
|
|
6a4bc67841 | ||
|
|
adfefac85d | ||
|
|
c92144c436 | ||
|
|
ca0156ea4c | ||
|
|
61c4bc11bd | ||
|
|
db25a18f9d | ||
|
|
0d7fa18acd | ||
|
|
012bb3a1f3 | ||
|
|
de233e0285 | ||
|
|
ae7544cd53 | ||
|
|
14df81b312 | ||
|
|
4706cfcd94 | ||
|
|
1f5a2469b2 | ||
|
|
e31c670938 | ||
|
|
db9f683297 | ||
|
|
7a43cdcaea | ||
|
|
d5637a4550 | ||
|
|
0b9fa36fb7 | ||
|
|
7bd0c49c14 | ||
|
|
44f91bb453 | ||
|
|
d8f229838f | ||
|
|
7b5a095898 | ||
|
|
ff7937c2d7 | ||
|
|
d057497105 | ||
|
|
e1402b0d4f | ||
|
|
3ea337e3f8 | ||
|
|
9c2c16e378 | ||
|
|
7e9ef204a7 | ||
|
|
f779e7fd35 | ||
|
|
a5a62922b5 | ||
|
|
2c6aa96f8e | ||
|
|
30e866128f | ||
|
|
ec81eb47d9 | ||
|
|
cd2a5ae903 | ||
|
|
7a02330e9e | ||
|
|
a929194a29 | ||
|
|
febf4d99c6 | ||
|
|
83ed8ab875 | ||
|
|
6563f54590 | ||
|
|
e1fe31dc94 | ||
|
|
8754263584 | ||
|
|
c660bd8d15 | ||
|
|
df32123f34 | ||
|
|
e81db01be2 | ||
|
|
05c3468d93 | ||
|
|
1687a6ca1a | ||
|
|
1a88343be9 | ||
|
|
6f752f3a18 | ||
|
|
7bbc9a84dc | ||
|
|
85cf3e35e4 | ||
|
|
85fa6bca72 | ||
|
|
231685e78d | ||
|
|
a4dc135129 | ||
|
|
2c890e53f8 | ||
|
|
33556fd749 | ||
|
|
8a62e8b710 | ||
|
|
45272dd8b7 | ||
|
|
f94a0bd7f7 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,9 +1,14 @@
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
.idea
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
|
||||
dist/
|
||||
|
||||
.python-version
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
### Emacs ###
|
||||
*~
|
||||
|
||||
3
Makefile
3
Makefile
@@ -30,3 +30,6 @@ restart-detached:
|
||||
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
git diff --exit-code package-lock.json
|
||||
|
||||
test:
|
||||
docker exec -it edx.gradebook jest
|
||||
|
||||
77
README.md
77
README.md
@@ -23,12 +23,32 @@ npm i --save @edx/gradebook
|
||||
|
||||
After cloning the repository, run `make up-detached` in the `gradebook` directory - this will build and start the `gradebook` web application in a docker container.
|
||||
|
||||
The web application runs on port **1991**, so when you go to `http://localhost:1991` you should see the UI.
|
||||
The web application runs on port **1991**, so when you go to `http://localhost:1991/course-v1:edX+DemoX+Demo_Course` you should see the UI (assuming you have such a Demo Course in your devstack). Note that you always have to provide a course id to actually see a gradebook.
|
||||
|
||||
If you don't, you can see the log messages for the docker container by executing `make logs` in the `gradebook` directory.
|
||||
|
||||
Note that `make up-detached` executes the `npm run start` script which will hot-reload JavaScript and Sass files changes, so you should (:crossed_fingers:) not need to do anything (other than wait) when making changes.
|
||||
|
||||
## Configuring for local use in edx-platform
|
||||
|
||||
Assuming you've got the UI running at `http://localhost:1991`, you can configure the LMS in edx-platform
|
||||
to point to your local gradebook from the instructor dashboard by putting this settings in `lms/env/private.py`:
|
||||
```
|
||||
WRITABLE_GRADEBOOK_URL = 'http://localhost:1991'
|
||||
```
|
||||
|
||||
There are also several edx-platform waffle and feature flags you'll have to enable from the Django admin:
|
||||
|
||||
1. Grades > Persistent grades enabled flag. Add this flag if it doesn't exist,
|
||||
check the ``enabled`` and ``enabled for all courses`` boxes.
|
||||
|
||||
2. Waffle > Switches. Add the ``grades.assume_zero_grade_if_absent`` switch and make it active.
|
||||
|
||||
3. Waffle_utils > Waffle flag course overrides. You want to activate this flag for any course
|
||||
in which you'd like to enable the gradebook. Add a course override flag using a course id and the flag name
|
||||
``grades.writable_gradebook``. Make sure to check the ``enabled`` box. Alternatively, you could add this as a
|
||||
regular waffle flag to enable the gradebook for all courses.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
* `config`
|
||||
@@ -46,61 +66,6 @@ Note that `make up-detached` executes the `npm run start` script which will hot-
|
||||
* `constants`
|
||||
* `reducers`
|
||||
* Directory for `Redux` reducers
|
||||
* [`.babelrc`](#babelrc)
|
||||
* [`.dockerignore`](#dockerignore)
|
||||
* [`.eslintignore`](#eslintignore)
|
||||
* [`.eslintrc.js`](#eslintrcjs)
|
||||
* `.gitignore`
|
||||
* [`npmignore`](#npmignore)
|
||||
* [`.travis.yml`](#travisyml)
|
||||
* `docker-compose.yml`
|
||||
* `Dockerfile`
|
||||
* `LICENSE`
|
||||
* `Makefile`
|
||||
* `package-lock.json`
|
||||
* [`package.json`](#packagejson)
|
||||
|
||||
### `.babelrc`
|
||||
|
||||
We use [`Babel`](https://babeljs.io/) to transpile `ES2015+` JavaScript to `ES5` JavaScript. `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/).
|
||||
|
||||
The `.babelrc` file is used to specify a particular configuration - for example, we use the [`babel-preset-react`](https://babeljs.io/docs/plugins/preset-react/), which, among other things, allows `babel` to parse `JSX`.
|
||||
|
||||
### `.dockerignore`
|
||||
|
||||
The important thing to remember is to add the `node_modules` directory to `.dockerignore` - for more information [see the Docker documentation](https://docs.docker.com/engine/reference/builder/#dockerignore-file).
|
||||
|
||||
### `.eslintignore`
|
||||
|
||||
We use [`eslint`](https://eslint.org/) for our `JavaScript` linting needs. The `.eslintignore` file is used to [specify files or directories to, well, ignore](https://eslint.org/docs/user-guide/configuring#ignoring-files-and-directories).
|
||||
|
||||
While `eslint` automatically ignores `node_modules`, we like to add it to the `.eslintignore` just for the added explicitness. In addition, you probably want to add the directory for your compiled files (in our case, `./dist`) and your coverage directory (in our case, `./coverage`).
|
||||
|
||||
### `.eslintrc`
|
||||
|
||||
This is where the actual `eslint` configuration is specified. All `edX` JavaScript projects should extend either the [`eslint-config-edx`](https://github.com/edx/eslint-config-edx/blob/master/packages/eslint-config-edx/README.md) or [`eslint-config-edx-es5`](https://github.com/edx/eslint-config-edx/blob/master/packages/eslint-config-edx-es5/README.md) configurations (for `ES2015+` and `ES5` JavaScript, respectively). Both configurations can be found in [the `eslint-config-edx` repository](https://github.com/edx/eslint-config-edx).
|
||||
|
||||
### `.npmignore`
|
||||
|
||||
We are not currently publishing this package to [`npm`](https://www.npmjs.com/). If we did, we would want to exclude certain files from getting uploaded to `npm` (like our coverage files, for example). For more information, see [the `npm` documentation](https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package).
|
||||
|
||||
### `.travis.yml`
|
||||
|
||||
We use [`Travis CI`](https://travis-ci.org/) to build (and deploy) our application. The `.travis.yml` file specifies the configuration for `Travis` builds. For more information, see [the `Travis` documentation](https://docs.travis-ci.com/user/customizing-the-build/).
|
||||
|
||||
### `package.json`
|
||||
|
||||
Arguably, one of the **most important files in an `npm`-based application**, the `package.json` file specifies everything from the `name` of the application, were it to be published to `npm`, to it's `dependencies`.
|
||||
|
||||
For more information, see [the `npm` documentation](https://docs.npmjs.com/files/package.json).
|
||||
|
||||
## Helpful Applications
|
||||
|
||||
### [`Greenkeeper`](https://greenkeeper.io/)
|
||||
|
||||
[`Greenkeeper`](https://greenkeeper.io/) is basically a `GitHub` application that handles `npm` dependencies. It will automatically open PRs with `package.json` updates when new versions of your `npm` dependencies get published. There are ways to also automatically keep the `package-lock.json` in-line, in the same PR, using [`greenkeeper-lockfile`].
|
||||
|
||||
For more information, see [the `Greenkeeper` documentation](https://greenkeeper.io/docs.html#what-greenkeeper-does).
|
||||
|
||||
## Authentication with backend API services
|
||||
|
||||
|
||||
BIN
assets/edx-sm.png
Normal file
BIN
assets/edx-sm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/logo-footer.png
Normal file
BIN
assets/logo-footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -64,6 +64,28 @@ module.exports = Merge.smart(commonConfig, {
|
||||
test: /\.(woff2?|ttf|svg|eot)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
loader: 'file-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|ico)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: [
|
||||
'file-loader',
|
||||
{
|
||||
loader: 'image-webpack-loader',
|
||||
options: {
|
||||
optimizationlevel: 7,
|
||||
mozjpeg: {
|
||||
progressive: true,
|
||||
},
|
||||
gifsicle: {
|
||||
interlaced: false,
|
||||
},
|
||||
pngquant: {
|
||||
quality: '65-90',
|
||||
speed: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Specify additional processing or side-effects done on the Webpack output bundles as a whole.
|
||||
@@ -73,6 +95,22 @@ module.exports = Merge.smart(commonConfig, {
|
||||
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
|
||||
template: path.resolve(__dirname, '../public/index.html'),
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'development',
|
||||
BASE_URL: 'localhost:1991',
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
LOGIN_URL: 'http://localhost:18000/login',
|
||||
LOGOUT_URL: 'http://localhost:18000/login',
|
||||
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
|
||||
DATA_API_BASE_URL: 'http://localhost:8000',
|
||||
// LMS_CLIENT_ID should match the lms DOT client application id your LMS container
|
||||
LMS_CLIENT_ID: 'login-service-client-id',
|
||||
SEGMENT_KEY: null,
|
||||
FEATURE_FLAGS: {},
|
||||
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
|
||||
CSRF_COOKIE_NAME: 'csrftoken',
|
||||
}),
|
||||
// when the --hot option is not passed in as part of the command
|
||||
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
|
||||
// https://webpack.js.org/configuration/dev-server/#devserver-hot
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
const Merge = require('webpack-merge');
|
||||
const commonConfig = require('./webpack.common.config.js');
|
||||
const path = require('path');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
module.exports = Merge.smart(commonConfig, {
|
||||
mode: 'production',
|
||||
@@ -37,29 +38,27 @@ module.exports = Merge.smart(commonConfig, {
|
||||
// increases build time.
|
||||
{
|
||||
test: /(.scss|.css)$/,
|
||||
use: ExtractTextPlugin.extract({
|
||||
// creates style nodes from JS strings, only used if extracting fails
|
||||
fallback: 'style-loader',
|
||||
use: [
|
||||
{
|
||||
loader: 'css-loader', // translates CSS into CommonJS
|
||||
options: {
|
||||
sourceMap: true,
|
||||
minimize: true,
|
||||
},
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: 'css-loader', // translates CSS into CommonJS
|
||||
options: {
|
||||
sourceMap: true,
|
||||
minimize: true,
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader', // compiles Sass to CSS
|
||||
options: {
|
||||
sourceMap: true,
|
||||
includePaths: [
|
||||
path.join(__dirname, '../node_modules'),
|
||||
path.join(__dirname, '../src'),
|
||||
],
|
||||
},
|
||||
},
|
||||
'postcss-loader',
|
||||
{
|
||||
loader: 'sass-loader', // compiles Sass to CSS
|
||||
options: {
|
||||
sourceMap: true,
|
||||
includePaths: [
|
||||
path.join(__dirname, '../node_modules'),
|
||||
path.join(__dirname, '../src'),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Webpack, by default, uses the url-loader for images and fonts that are required/included by
|
||||
// files it processes, which just base64 encodes them and inlines them in the javascript
|
||||
@@ -69,6 +68,28 @@ module.exports = Merge.smart(commonConfig, {
|
||||
test: /\.(woff2?|ttf|svg|eot)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
loader: 'file-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|ico)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: [
|
||||
'file-loader',
|
||||
{
|
||||
loader: 'image-webpack-loader',
|
||||
options: {
|
||||
optimizationlevel: 7,
|
||||
mozjpeg: {
|
||||
progressive: true,
|
||||
},
|
||||
gifsicle: {
|
||||
interlaced: false,
|
||||
},
|
||||
pngquant: {
|
||||
quality: '65-90',
|
||||
speed: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// New in Webpack 4. Replaces CommonChunksPlugin. Extract common modules among all chunks to one
|
||||
@@ -82,14 +103,29 @@ module.exports = Merge.smart(commonConfig, {
|
||||
// Specify additional processing or side-effects done on the Webpack output bundles as a whole.
|
||||
plugins: [
|
||||
// Writes the extracted CSS from each entry to a file in the output directory.
|
||||
new ExtractTextPlugin({
|
||||
filename: '[name].min.css',
|
||||
allChunks: true,
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].[chunkhash].css',
|
||||
}),
|
||||
// Generates an HTML file in the output directory.
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
|
||||
template: path.resolve(__dirname, '../public/index.html'),
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production',
|
||||
BASE_URL: null,
|
||||
LMS_BASE_URL: null,
|
||||
LOGIN_URL: null,
|
||||
LOGOUT_URL: null,
|
||||
CSRF_TOKEN_API_PATH: null,
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: null,
|
||||
DATA_API_BASE_URL: null,
|
||||
SEGMENT_KEY: null,
|
||||
FEATURE_FLAGS: {},
|
||||
ACCESS_TOKEN_COOKIE_NAME: null,
|
||||
CSRF_COOKIE_NAME: 'csrftoken',
|
||||
NEW_RELIC_APP_ID: null,
|
||||
NEW_RELIC_LICENSE_KEY: null,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
14013
package-lock.json
generated
14013
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -25,20 +25,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/edx-bootstrap": "^0.4.3",
|
||||
"@edx/frontend-auth": "^1.0.3",
|
||||
"@edx/paragon": "^3.5.2",
|
||||
"@edx/frontend-auth": "^1.3.0",
|
||||
"@edx/paragon": "^3.7.2",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"classnames": "^2.2.5",
|
||||
"email-prop-type": "^1.1.5",
|
||||
"font-awesome": "^4.7.0",
|
||||
"history": "^4.7.2",
|
||||
"prop-types": "^15.5.10",
|
||||
"query-string": "^5.1.1",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"reactstrap": "^6.5.0",
|
||||
"redux": "^3.7.2",
|
||||
"redux-devtools-extension": "^2.13.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
@@ -46,6 +48,8 @@
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^9.4.2",
|
||||
"axios-mock-adapter": "^1.15.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"babel-jest": "^22.4.0",
|
||||
@@ -60,15 +64,17 @@
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"es-check": "^2.0.2",
|
||||
"eslint-config-edx": "^4.0.3",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"fetch-mock": "^6.3.0",
|
||||
"file-loader": "^1.1.9",
|
||||
"html-webpack-harddisk-plugin": "^0.2.0",
|
||||
"html-webpack-plugin": "^3.0.3",
|
||||
"husky": "^0.14.3",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"image-webpack-loader": "^4.2.0",
|
||||
"jest": "^22.4.0",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"react-dev-utils": "^5.0.0",
|
||||
"react-test-renderer": "^16.2.0",
|
||||
"redux-mock-store": "^1.5.1",
|
||||
@@ -76,8 +82,8 @@
|
||||
"semantic-release": "^15.10.7",
|
||||
"style-loader": "^0.20.2",
|
||||
"travis-deploy-once": "^5.0.9",
|
||||
"webpack": "^4.1.0",
|
||||
"webpack-cli": "^2.0.10",
|
||||
"webpack": "^4.25.1",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.0",
|
||||
"webpack-merge": "^4.1.1"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head></head>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
@@ -7,3 +7,4 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
@import "~@edx/paragon/src/SearchField/SearchField";
|
||||
|
||||
@import "./components/Gradebook/gradebook";
|
||||
@import "./components/Gradebook/footer";
|
||||
|
||||
122
src/components/Gradebook/footer.jsx
Normal file
122
src/components/Gradebook/footer.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import { Hyperlink, Icon } from '@edx/paragon';
|
||||
|
||||
import EdXLogo from '../../../assets/edx-sm.png';
|
||||
|
||||
export default function Footer() {
|
||||
function renderLogo() {
|
||||
return (
|
||||
<img src={EdXLogo} alt="edX logo" height="30" width="60" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<footer
|
||||
role="contentinfo"
|
||||
aria-label="Page Footer"
|
||||
className="footer d-flex justify-content-center border-top py-3 px-4"
|
||||
>
|
||||
<div className="max-width-1180 d-grid">
|
||||
<div className="area-1">
|
||||
<Hyperlink destination="https://www.edx.org/" content={renderLogo()} />
|
||||
</div>
|
||||
<div className="area-2">
|
||||
<h2>edx</h2>
|
||||
<ul className="list-unstyled p-0 m-0">
|
||||
<li><a href="https://www.edx.org/about-us">About</a></li>
|
||||
<li><a href="https://www.edx.org/enterprise">edX for Business</a></li>
|
||||
<li><a href="https://www.edx.org/affiliate-program">Affiliates</a></li>
|
||||
<li><a href="http://open.edx.org">Open edX</a></li>
|
||||
<li><a href="https://www.edx.org/careers">Careers</a></li>
|
||||
<li><a href="https://www.edx.org/news-announcements">News</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="area-3">
|
||||
<h2>Legal</h2>
|
||||
<ul className="list-unstyled p-0 m-0">
|
||||
<li><a href="https://www.edx.org/edx-terms-service">Terms of Service & Honor Code</a></li>
|
||||
<li><a href="https://www.edx.org/edx-privacy-policy">Privacy Policy</a></li>
|
||||
<li><a href="https://www.edx.org/accessibility">Accessibility Policy</a></li>
|
||||
<li><a href="https://www.edx.org/trademarks">Trademark Policy</a></li>
|
||||
<li><a href="https://www.edx.org/sitemap">Sitemap</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="area-4">
|
||||
<h2>Connect</h2>
|
||||
<ul className="list-unstyled p-0 m-0">
|
||||
<li><a href="https://www.edx.org/blog">Blog</a></li>
|
||||
<li><a href="https://courses.edx.org/support/contact_us">Contact Us</a></li>
|
||||
<li><a href="https://support.edx.org">Help Center</a></li>
|
||||
<li><a href="https://www.edx.org/media-kit">Media Kit</a></li>
|
||||
<li><a href="https://www.edx.org/donate">Donate</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="area-5">
|
||||
<ul
|
||||
className="d-flex flex-row justify-content-between list-unstyled max-width-222 p-0 mb-4"
|
||||
>
|
||||
{/* TODO: Use Paragon HyperLink with Icon. */}
|
||||
{/* Would need to add rel to paragon if we still need it. */}
|
||||
<li>
|
||||
<a href="http://www.facebook.com/EdxOnline" title="Facebook" rel="noopener noreferrer" target="_blank">
|
||||
<Icon className={['fa', 'fa-facebook-square', 'fa-2x']} screenReaderText="Like edX on Facebook" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/edXOnline" title="Twitter" rel="noopener noreferrer" target="_blank">
|
||||
<Icon className={['fa', 'fa-twitter-square', 'fa-2x']} screenReaderText="Follow edX on Twitter" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.youtube.com/user/edxonline" title="Youtube" rel="noopener noreferrer" target="_blank">
|
||||
<Icon className={['fa', 'fa-youtube-square', 'fa-2x']} screenReaderText="Subscribe to the edX YouTube channel" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.linkedin.com/company/edx" title="LinkedIn" rel="noopener noreferrer" target="_blank">
|
||||
<Icon className={['fa', 'fa-linkedin-square', 'fa-2x']} screenReaderText="Follow edX on LinkedIn" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://plus.google.com/+edXOnline" title="Google+" rel="noopener noreferrer" target="_blank">
|
||||
<Icon className={['fa', 'fa-google-plus-square', 'fa-2x']} screenReaderText="Follow edX on Google+" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.reddit.com/r/edx" title="Reddit" rel="noopener noreferrer" target="_blank">
|
||||
<Icon className={['fa', 'fa-reddit-square', 'fa-2x']} screenReaderText="Subscribe to the edX subreddit" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="d-flex flex-row justify-content-between list-unstyled max-width-264 p-0 mb-5">
|
||||
<li>
|
||||
<a href="https://itunes.apple.com/us/app/edx/id945480667?mt=8" rel="noopener noreferrer" target="_blank">
|
||||
<img
|
||||
className="max-height-39"
|
||||
alt="Download the edX mobile app from the Apple App Store"
|
||||
src="https://prod-edxapp.edx-cdn.org/static/images/app/app_store_badge_135x40.d0558d910630.svg"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://play.google.com/store/apps/details?id=org.edx.mobile" rel="noopener noreferrer" target="_blank">
|
||||
<img
|
||||
className="max-height-39"
|
||||
alt="Download the edX mobile app from Google Play"
|
||||
src="https://prod-edxapp.edx-cdn.org/static/images/app/google_play_badge_45.6ea466e328da.png"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
© 2012–{(new Date().getFullYear())} edX Inc.
|
||||
<br />
|
||||
EdX, Open edX, and MicroMasters are registered trademarks of edX Inc.
|
||||
| 粤ICP备17044299号-2
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
165
src/components/Gradebook/footer.scss
Normal file
165
src/components/Gradebook/footer.scss
Normal file
@@ -0,0 +1,165 @@
|
||||
.max-width-222 {
|
||||
max-width: 222px;
|
||||
}
|
||||
|
||||
.max-width-264 {
|
||||
max-width: 264px;
|
||||
}
|
||||
|
||||
.max-width-1180 {
|
||||
max-width: 1180px;
|
||||
}
|
||||
|
||||
.max-height-39 {
|
||||
max-height: 39px;
|
||||
}
|
||||
|
||||
.d-grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
$gray-footer: #fcfcfc;
|
||||
$border-1: 1px solid $gray-200;
|
||||
|
||||
.footer {
|
||||
background-color: $gray-footer;
|
||||
|
||||
.area-1 {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
border-bottom: $border-1;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.area-2 {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
border-bottom: $border-1;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.area-3 {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
border-bottom: $border-1;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.area-4 {
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
border-bottom: $border-1;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.area-5 {
|
||||
grid-column: 1;
|
||||
grid-row: 5;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 717px) {
|
||||
.area-1 {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 1;
|
||||
border-bottom: none;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.area-2 {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.area-3 {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.area-4 {
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.area-5 {
|
||||
grid-column: 2;
|
||||
grid-row: 2 / span 3;
|
||||
border-left: $border-1;
|
||||
padding-left: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 870px) {
|
||||
.area-1 {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / span 3;
|
||||
border-right: $border-1;
|
||||
padding-right: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.area-2 {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
border-bottom: none;
|
||||
border-right: $border-1;
|
||||
padding-right: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.area-3 {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
border-bottom: none;
|
||||
border-right: $border-1;
|
||||
padding-right: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.area-4 {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.area-5 {
|
||||
grid-column: 2 / span 3;
|
||||
grid-row: 2;
|
||||
border: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1188px) {
|
||||
.area-1 {
|
||||
grid-column: 1 / span 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.area-2 {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.area-3 {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.area-4 {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
border-right: $border-1;
|
||||
padding-right: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.area-5 {
|
||||
grid-column: 5 / span 1;
|
||||
grid-row: 1;
|
||||
max-width: 372px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,43 @@
|
||||
.spinner-overlay {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
background-color: #999;
|
||||
opacity: 0.5;
|
||||
z-index: 99999;
|
||||
display:flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 200px;
|
||||
}
|
||||
|
||||
.color-black {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.gradebook-container{
|
||||
width: 500px;
|
||||
@media only screen and (min-width: 640px) {
|
||||
width: 630px;
|
||||
}
|
||||
@media only screen and (min-width: 992px) {
|
||||
width: 900px;
|
||||
}
|
||||
@media only screen and (min-width: 1200px) {
|
||||
width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
.student-filters{
|
||||
display: flex;
|
||||
.label{
|
||||
padding-top: 30px;
|
||||
}
|
||||
.form-group{
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.gbook {
|
||||
overflow-x: scroll;
|
||||
|
||||
@@ -20,9 +60,6 @@
|
||||
display: block;
|
||||
background-color: #fff;
|
||||
}
|
||||
.table tr td:not(:first-child) {
|
||||
min-width: 250px;
|
||||
}
|
||||
.table tr td:nth-child(2) {
|
||||
box-sizing: content-box;
|
||||
padding-left: 170px;
|
||||
|
||||
@@ -1,322 +1,398 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import emailPropType from 'email-prop-type';
|
||||
import { SearchField, Table, Modal } from '@edx/paragon';
|
||||
import {
|
||||
Button,
|
||||
InputSelect,
|
||||
Modal,
|
||||
SearchField,
|
||||
StatusAlert,
|
||||
Table,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
import queryString from 'query-string';
|
||||
import { configuration } from '../../config';
|
||||
|
||||
const DECIMAL_PRECISION = 2;
|
||||
|
||||
export default class Gradebook extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
grades: this.mapUserEnteriesPercent(this.props.results).sort(this.sortAlphaDesc),
|
||||
headings: this.mapHeadings(this.props.results[0]),
|
||||
filterValue: '',
|
||||
modalContent: (<h1>Hello, World!</h1>),
|
||||
modalOpen: false,
|
||||
modalModel: [{}],
|
||||
updateVal: 0,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
};
|
||||
}
|
||||
|
||||
sortAlphaDesc = (gradeRowA, gradeRowB) => {
|
||||
const a = gradeRowA.username.toUpperCase();
|
||||
const b = gradeRowB.username.toUpperCase();
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||
const a = gradeRowA.username.toUpperCase();
|
||||
const b = gradeRowB.username.toUpperCase();
|
||||
if (a < b) {
|
||||
return 1;
|
||||
}
|
||||
if (a > b) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
sortNumerically = (colKey, direction) => {
|
||||
function sortNumAsc(gradeRowA, gradeRowB) {
|
||||
if (gradeRowA[colKey] < gradeRowB[colKey]) {
|
||||
return -1;
|
||||
}
|
||||
if (gradeRowA[colKey] > gradeRowB[colKey]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortNumDesc(gradeRowA, gradeRowB) {
|
||||
if (gradeRowA[colKey] < gradeRowB[colKey]) {
|
||||
return 1;
|
||||
}
|
||||
if (gradeRowA[colKey] > gradeRowB[colKey]) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
this.setState({grades: [...this.state.grades].sort(direction === 'desc' ? sortNumDesc : sortNumAsc)});
|
||||
componentDidMount() {
|
||||
const urlQuery = queryString.parse(this.props.location.search);
|
||||
this.props.getUserGrades(
|
||||
this.props.match.params.courseId,
|
||||
urlQuery.cohort,
|
||||
urlQuery.track,
|
||||
);
|
||||
this.props.getTracks(this.props.match.params.courseId);
|
||||
this.props.getCohorts(this.props.match.params.courseId);
|
||||
this.props.getAssignmentTypes(this.props.match.params.courseId);
|
||||
}
|
||||
|
||||
mapHeadings = entry => {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: direction => {
|
||||
this.setState({
|
||||
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc)
|
||||
})
|
||||
},
|
||||
}];
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label)
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: direction => {this.sortNumerically(s.label, direction)},
|
||||
}));
|
||||
const totals = [{
|
||||
label: 'Total',
|
||||
key: 'total',
|
||||
columnSortable: true,
|
||||
onSort: direction => {this.sortNumerically('total', direction)},
|
||||
}];
|
||||
setNewModalState = (userEntry, subsection) => {
|
||||
let adjustedGradePossible = '';
|
||||
let currentGradePossible = '';
|
||||
if (subsection.attempted) {
|
||||
adjustedGradePossible = ` / ${subsection.score_possible}`;
|
||||
currentGradePossible = `/${subsection.score_possible}`;
|
||||
}
|
||||
this.setState({
|
||||
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,
|
||||
updateModuleId: subsection.module_id,
|
||||
updateUserId: userEntry.user_id,
|
||||
});
|
||||
}
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
handleAdjustedGradeClick = () => {
|
||||
this.props.updateGrades(
|
||||
this.props.match.params.courseId, [
|
||||
{
|
||||
user_id: this.state.updateUserId,
|
||||
usage_id: this.state.updateModuleId,
|
||||
grade: {
|
||||
earned_graded_override: this.state.updateVal,
|
||||
},
|
||||
},
|
||||
],
|
||||
this.state.filterValue,
|
||||
this.props.selectedCohort,
|
||||
this.props.selectedTrack,
|
||||
);
|
||||
|
||||
this.setState({
|
||||
modalModel: [{}],
|
||||
modalOpen: false,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
});
|
||||
}
|
||||
|
||||
updateQueryParams = (queryKey, queryValue) => {
|
||||
const parsed = queryString.parse(this.props.location.search);
|
||||
parsed[queryKey] = queryValue;
|
||||
return `?${queryString.stringify(parsed)}`;
|
||||
};
|
||||
|
||||
mapHeadingsHw = entry => {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: direction => {
|
||||
this.setState({
|
||||
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc)
|
||||
})
|
||||
},
|
||||
}];
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label && section.category == 'Homework' )
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: direction => {this.sortNumerically(s.label, direction)},
|
||||
}));
|
||||
|
||||
return results.concat(assignmentHeadings);
|
||||
mapAssignmentTypeEntries = (entries) => {
|
||||
const mapped = entries.map(entry => ({
|
||||
id: entry,
|
||||
label: entry,
|
||||
}));
|
||||
mapped.unshift({ id: 0, label: 'All' });
|
||||
return mapped;
|
||||
};
|
||||
|
||||
mapHeadingsExam = entry => {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: direction => {
|
||||
this.setState({
|
||||
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc)
|
||||
})
|
||||
},
|
||||
}];
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label && section.category == 'Exam' )
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: direction => {this.sortNumerically(s.label, direction)},
|
||||
}));
|
||||
|
||||
return results.concat(assignmentHeadings);
|
||||
mapCohortsEntries = (entries) => {
|
||||
const mapped = entries.map(entry => ({
|
||||
id: entry.id,
|
||||
label: entry.name,
|
||||
}));
|
||||
mapped.unshift({ id: 0, label: 'Cohort-All' });
|
||||
return mapped;
|
||||
};
|
||||
|
||||
mapUserEnteriesPercent = (entries) => entries.map(entry => {
|
||||
const results = {username: entry.username};
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc,s) => {
|
||||
acc[s.label]= (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={()=> this.setState({
|
||||
modalModel: [{
|
||||
username: entry.username,
|
||||
autoGrade: `${s.score_earned}/${s.score_possible}`,
|
||||
adjustedGrade: (<span><input style={{width: '25px'}} type='text' value={this.updateVal}></input> / {s.score_possible}</span>),
|
||||
assignmentName: `${s.subsection_name}`,
|
||||
}],
|
||||
modalOpen: true,
|
||||
})}
|
||||
>
|
||||
{s.percent}
|
||||
</button>);
|
||||
return acc;
|
||||
}, {});
|
||||
const totals = {total: entry.percent * 100}
|
||||
return Object.assign(results, assignments, totals);
|
||||
});
|
||||
mapTracksEntries = (entries) => {
|
||||
const mapped = entries.map(entry => ({
|
||||
id: entry.slug,
|
||||
label: entry.name,
|
||||
}));
|
||||
mapped.unshift({ label: 'Track-All' });
|
||||
return mapped;
|
||||
};
|
||||
|
||||
mapUserEnteriesAbsolute = (entries) => entries.map(entry => {
|
||||
const results = {username: entry.username};
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc,s) => {
|
||||
acc[s.label]= (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={()=> this.setState({
|
||||
modalModel: [{
|
||||
username: entry.username,
|
||||
autoGrade: `${s.score_earned}/${s.score_possible}`,
|
||||
adjustedGrade: (<span><input style={{width: '25px'}} type='text' value={this.updateVal}></input> / {s.score_possible}</span>),
|
||||
assignmentName: `${s.subsection_name}`,
|
||||
}],
|
||||
modalOpen: true,
|
||||
})}
|
||||
>
|
||||
{s.score_earned}/{s.score_possible}
|
||||
</button>);
|
||||
//TODO: This is a really hacky thing I'm doing just to get sorting to work. Should be able to clean this up drastically when I introduce the reducers
|
||||
acc[`${s.label}Percent`] = s.percent
|
||||
updateAssignmentTypes = (event) => {
|
||||
this.props.filterColumns(event, this.props.grades[0]);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
const totals = {total: entry.percent * 100}
|
||||
return Object.assign(results, assignments, totals);
|
||||
});
|
||||
updateTracks = (event) => {
|
||||
const selectedTrackItem = this.props.tracks.find(x => x.name === event);
|
||||
let selectedTrackSlug = null;
|
||||
if (selectedTrackItem) {
|
||||
selectedTrackSlug = selectedTrackItem.slug;
|
||||
}
|
||||
this.props.getUserGrades(
|
||||
this.props.match.params.courseId,
|
||||
this.props.selectedCohort,
|
||||
selectedTrackSlug,
|
||||
);
|
||||
const updatedQueryStrings = this.updateQueryParams('track', selectedTrackSlug);
|
||||
this.props.history.push(updatedQueryStrings);
|
||||
};
|
||||
|
||||
updateCohorts = (event) => {
|
||||
const selectedCohortItem = this.props.cohorts.find(x => x.name === event);
|
||||
let selectedCohortId = null;
|
||||
if (selectedCohortItem) {
|
||||
selectedCohortId = selectedCohortItem.id;
|
||||
}
|
||||
this.props.getUserGrades(
|
||||
this.props.match.params.courseId,
|
||||
selectedCohortId,
|
||||
this.props.selectedTrack,
|
||||
);
|
||||
const updatedQueryStrings = this.updateQueryParams('cohort', selectedCohortId);
|
||||
this.props.history.push(updatedQueryStrings);
|
||||
};
|
||||
|
||||
mapSelectedAssignmentTypeEntry = (entry) => {
|
||||
const selectedAssignmentTypeEntry = this.props.assignmentTypes
|
||||
.find(x => x.id === parseInt(entry, 10));
|
||||
if (selectedAssignmentTypeEntry) {
|
||||
return selectedAssignmentTypeEntry.name;
|
||||
}
|
||||
return 'All';
|
||||
};
|
||||
|
||||
mapSelectedCohortEntry = (entry) => {
|
||||
const selectedCohortEntry = this.props.cohorts.find(x => x.id === parseInt(entry, 10));
|
||||
if (selectedCohortEntry) {
|
||||
return selectedCohortEntry.name;
|
||||
}
|
||||
return 'Cohorts';
|
||||
};
|
||||
|
||||
mapSelectedTrackEntry = (entry) => {
|
||||
const selectedTrackEntry = this.props.tracks.find(x => x.slug === entry);
|
||||
if (selectedTrackEntry) {
|
||||
return selectedTrackEntry.name;
|
||||
}
|
||||
return 'Tracks';
|
||||
};
|
||||
|
||||
roundGrade = percent => parseFloat(percent.toFixed(DECIMAL_PRECISION));
|
||||
|
||||
formatter = {
|
||||
percent: (entries, areGradesFrozen) => entries.map((entry) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, subsection) => {
|
||||
if (areGradesFrozen) {
|
||||
acc[subsection.label] = `${this.roundGrade(subsection.percent * 100)} %`;
|
||||
} else {
|
||||
acc[subsection.label] = (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={() => this.setNewModalState(entry, subsection)}
|
||||
>
|
||||
{this.roundGrade(subsection.percent * 100)}%
|
||||
</button>);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const totals = { total: `${this.roundGrade(entry.percent * 100)}%` };
|
||||
return Object.assign(results, assignments, totals);
|
||||
}),
|
||||
|
||||
absolute: (entries, areGradesFrozen) => entries.map((entry) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, subsection) => {
|
||||
const scoreEarned = this.roundGrade(subsection.score_earned);
|
||||
const scorePossible = this.roundGrade(subsection.score_possible);
|
||||
let label = `${scoreEarned}`;
|
||||
if (subsection.attempted) {
|
||||
label = `${scoreEarned}/${scorePossible}`;
|
||||
}
|
||||
if (areGradesFrozen) {
|
||||
acc[subsection.label] = label;
|
||||
} else {
|
||||
acc[subsection.label] = (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={() => this.setNewModalState(entry, subsection)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const totals = { total: `${this.roundGrade(entry.percent * 100)}/100` };
|
||||
return Object.assign(results, assignments, totals);
|
||||
}),
|
||||
};
|
||||
|
||||
lmsInstructorDashboardUrl = courseId => `${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="d-flex justify-content-center">
|
||||
<div className="card" style={{width: '50rem'}}>
|
||||
<div className="card-body">
|
||||
{ this.props.showSpinner && <div className="spinner-overlay"><Icon className={['fa', 'fa-spinner', 'fa-spin', 'fa-5x', 'color-black']} /></div>}
|
||||
<div className="gradebook-container">
|
||||
<div>
|
||||
<a
|
||||
href={this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}
|
||||
className="mb-3"
|
||||
>
|
||||
{'<< Back to Dashboard'}
|
||||
</a>
|
||||
<h1>Gradebook</h1>
|
||||
<hr/>
|
||||
<h3> {this.props.match.params.courseId}</h3>
|
||||
{ this.props.areGradesFrozen &&
|
||||
<div className="alert alert-warning" role="alert" >
|
||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||
</div>
|
||||
}
|
||||
<hr />
|
||||
<div className="d-flex justify-content-between" >
|
||||
<div>
|
||||
<div>
|
||||
Score View:
|
||||
Score View:
|
||||
<span>
|
||||
<input
|
||||
id='score-view-percent'
|
||||
className='ml-2'
|
||||
type='radio'
|
||||
name='score-view'
|
||||
value='percent'
|
||||
onClick={()=> {this.setState({
|
||||
grades: this.mapUserEnteriesPercent(this.props.results).sort(this.sortAlphaDesc),
|
||||
})}}
|
||||
<input
|
||||
id="score-view-percent"
|
||||
className="ml-2 mr-1"
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="percent"
|
||||
onClick={() => this.props.toggleFormat('percent')}
|
||||
/>
|
||||
<label className='ml-2 mr-2' htmlFor='score-view-percent'>Percent</label>
|
||||
<label className="mr-2" htmlFor="score-view-percent">Percent</label>
|
||||
</span>
|
||||
<span>
|
||||
<input
|
||||
id='score-view-absolute'
|
||||
type='radio'
|
||||
name='score-view'
|
||||
value='absolute'
|
||||
onClick={()=> {this.setState({
|
||||
grades: this.mapUserEnteriesAbsolute(this.props.results).sort(this.sortAlphaDesc),
|
||||
})}}
|
||||
<input
|
||||
id="score-view-absolute"
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="absolute"
|
||||
className="mr-1"
|
||||
onClick={() => this.props.toggleFormat('absolute')}
|
||||
/>
|
||||
<label className='ml-2 mr-2' htmlFor='score-view-absolute'>Absolute</label>
|
||||
<label htmlFor="score-view-absolute">Absolute</label>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Category:
|
||||
<span>
|
||||
<input
|
||||
id='category-all'
|
||||
className='ml-2'
|
||||
type='radio'
|
||||
name='category'
|
||||
value='all'
|
||||
onClick={()=> {this.setState({
|
||||
headings: this.mapHeadings(this.props.results[0]),
|
||||
})}}
|
||||
/>
|
||||
<label className='ml-2 mr-2' htmlFor='category-all'>All</label>
|
||||
</span>
|
||||
<span>
|
||||
<input
|
||||
id='category-homework'
|
||||
className='ml-2'
|
||||
type='radio'
|
||||
name='category'
|
||||
value='homework'
|
||||
onClick={()=> {this.setState({
|
||||
headings: this.mapHeadingsHw(this.props.results[0]),
|
||||
})}}
|
||||
/>
|
||||
<label className='ml-2 mr-2' htmlFor='category-homework'>Homework</label>
|
||||
</span>
|
||||
<span>
|
||||
<input
|
||||
id='category-exam'
|
||||
type='radio'
|
||||
name='category'
|
||||
value='exam'
|
||||
onClick={()=> {this.setState({
|
||||
headings: this.mapHeadingsExam(this.props.results[0]),
|
||||
})}}
|
||||
/>
|
||||
<label className='ml-2 mr-2' htmlFor='Exam'>Exam</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{marginLeft: "10px" ,marginBottom: "10px"}}>
|
||||
<a href="https://www.google./com">Download Grade Report</a>
|
||||
</div>
|
||||
<SearchField
|
||||
onSubmit={() => {this.setState({
|
||||
grades: this.mapUserEnteriesPercent(this.props.results).filter(entry => entry.username == '' || entry.username.includes(this.state.filterValue))
|
||||
})}}
|
||||
onChange={filterValue => this.setState({filterValue})}
|
||||
onClear={() => {this.setState({grades: this.mapUserEnteriesPercent(this.props.results).sort(this.sortAlphaDesc)})}}
|
||||
value={this.state.filterValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={this.state.headings}
|
||||
data={this.state.grades}
|
||||
tableSortable={true}
|
||||
defaultSortDirection='desc'
|
||||
defaultSortedColumn='username'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={this.state.modalOpen}
|
||||
title="Edit Grades"
|
||||
body={(
|
||||
<div>
|
||||
<h3>{this.state.modalModel[0].assignmentName}</h3>
|
||||
<Table
|
||||
columns={[{label: 'Username', key: 'username',},{label: 'Auto grade',key: 'autoGrade',},{label: 'Adjusted grade',key: 'adjustedGrade',}]}
|
||||
data={this.state.modalModel}
|
||||
tableSortable={true}
|
||||
defaultSortDirection='desc'
|
||||
defaultSortedColumn='username'
|
||||
{ this.props.assignmnetTypes.length > 0 &&
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Assignment Types:
|
||||
</span>
|
||||
<InputSelect
|
||||
name="assignment-types"
|
||||
value={this.mapSelectedTrackEntry(this.props.selectedAssignmentType)}
|
||||
options={this.mapAssignmentTypeEntries(this.props.assignmnetTypes)}
|
||||
onChange={this.updateAssignmentTypes}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onClose={()=> this.setState({modalOpen: false,})}
|
||||
}
|
||||
{(this.props.tracks.length > 0 || this.props.cohorts.length > 0) &&
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Student Groups:
|
||||
</span>
|
||||
{this.props.tracks.length > 0 &&
|
||||
<InputSelect
|
||||
name="Tracks"
|
||||
value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
|
||||
options={this.mapTracksEntries(this.props.tracks)}
|
||||
onChange={this.updateTracks}
|
||||
/>
|
||||
}
|
||||
{this.props.cohorts.length > 0 &&
|
||||
<InputSelect
|
||||
name="Cohorts"
|
||||
value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
|
||||
options={this.mapCohortsEntries(this.props.cohorts)}
|
||||
onChange={this.updateCohorts}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginLeft: '10px', marginBottom: '10px' }}>
|
||||
<a 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)}
|
||||
onChange={filterValue => this.setState({ filterValue })}
|
||||
onClear={() => this.props.getUserGrades(this.props.match.params.courseId, this.props.selectedCohort, this.props.selectedTrack)}
|
||||
value={this.state.filterValue}
|
||||
/>
|
||||
<div className="d-flex justify-content-end" style={{ marginTop: '20px' }}>
|
||||
<Button
|
||||
label="Previous"
|
||||
buttonType="primary"
|
||||
style={{ visibility: (!this.props.prevPage ? 'hidden' : 'visible') }}
|
||||
onClick={() => this.props.getPrevNextGrades(this.props.prevPage, this.props.selectedCohort, this.props.selectedTrack)}
|
||||
/>
|
||||
<div style={{ width: '10px' }} />
|
||||
<Button
|
||||
label="Next"
|
||||
buttonType="primary"
|
||||
style={{ visibility: (!this.props.nextPage ? 'hidden' : 'visible') }}
|
||||
onClick={() => this.props.getPrevNextGrades(this.props.nextPage, this.props.selectedCohort, this.props.selectedTrack)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<StatusAlert
|
||||
alertType="success"
|
||||
dialog="The grade has been successfully edited."
|
||||
onClose={() => this.props.updateBanner(false)}
|
||||
open={this.props.showSuccess}
|
||||
/>
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={this.props.headings}
|
||||
data={this.formatter[this.props.format](this.props.grades, this.props.areGradesFrozen)}
|
||||
tableSortable
|
||||
defaultSortDirection="asc"
|
||||
defaultSortedColumn="username"
|
||||
/>
|
||||
</div>
|
||||
<Modal
|
||||
open={this.state.modalOpen}
|
||||
title="Edit Grades"
|
||||
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>
|
||||
)}
|
||||
buttons={[
|
||||
<Button
|
||||
label="Edit Grade"
|
||||
buttonType="primary"
|
||||
onClick={this.handleAdjustedGradeClick}
|
||||
/>,
|
||||
]}
|
||||
onClose={() => this.setState({
|
||||
modalOpen: false,
|
||||
modalModel: [{}],
|
||||
updateVal: 0,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -325,1019 +401,3 @@ export default class Gradebook extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
Gradebook.defaultProps = {
|
||||
"results": [
|
||||
{
|
||||
"course_id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"email": "honor@example.com",
|
||||
"user_id": 6,
|
||||
"username": "honor",
|
||||
"full_name": "",
|
||||
"passed": false,
|
||||
"percent": 0,
|
||||
"letter_grade": null,
|
||||
"progress_page_url": "/courses/course-v1:edX+DemoX+Demo_Course/progress/6/",
|
||||
"section_breakdown": [
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Introduction",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b",
|
||||
"subsection_name": "Demo Course Overview"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 1: Getting Started",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/3.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 3,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations",
|
||||
"subsection_name": "Lesson 1 - Getting Started"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Homework",
|
||||
"chapter_name": "Example Week 1: Getting Started",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/11.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": "Ex 01",
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 11,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations",
|
||||
"subsection_name": "Homework - Question Styles"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Lesson 2 - Let's Get Interactive!"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Homework",
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/5.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": "Ex 02",
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 5,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Homework - Labs and Demos"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/19.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 19,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Homework - Essays"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "Lesson 3 - Be Social"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc027bcb4fe9afb744d2e8415855",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "Homework - Find Your Study Buddy"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@6ab9c442501d472c8ed200e367b4edfa",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "More Ways to Connect"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Exam",
|
||||
"chapter_name": "About Exams and Certificates",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/6.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 6,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@1414ffd5143b4b508f739b563ab468b7",
|
||||
"subsection_name": "edX Exams"
|
||||
}
|
||||
],
|
||||
"aggregates": {
|
||||
"Exam": {
|
||||
"score_possible": 6,
|
||||
"score_earned": 0
|
||||
},
|
||||
"Homework": {
|
||||
"score_possible": 16,
|
||||
"score_earned": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"course_id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"email": "audit@example.com",
|
||||
"user_id": 7,
|
||||
"username": "audit",
|
||||
"full_name": "",
|
||||
"passed": false,
|
||||
"percent": 0.17,
|
||||
"letter_grade": null,
|
||||
"progress_page_url": "/courses/course-v1:edX+DemoX+Demo_Course/progress/7/",
|
||||
"section_breakdown": [
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Introduction",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b",
|
||||
"subsection_name": "Demo Course Overview"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 1: Getting Started",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/3.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 3,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations",
|
||||
"subsection_name": "Lesson 1 - Getting Started"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Homework",
|
||||
"chapter_name": "Example Week 1: Getting Started",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.45",
|
||||
"is_graded": true,
|
||||
"grade_description": "(5.00/11.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": "Ex 01",
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
|
||||
"percent": 0.45,
|
||||
"score_earned": 5,
|
||||
"score_possible": 11,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations",
|
||||
"subsection_name": "Homework - Question Styles"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Lesson 2 - Let's Get Interactive!"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Homework",
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/5.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": "Ex 02",
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 5,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Homework - Labs and Demos"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/19.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 19,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Homework - Essays"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "Lesson 3 - Be Social"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc027bcb4fe9afb744d2e8415855",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "Homework - Find Your Study Buddy"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@6ab9c442501d472c8ed200e367b4edfa",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "More Ways to Connect"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Exam",
|
||||
"chapter_name": "About Exams and Certificates",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/6.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 6,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@1414ffd5143b4b508f739b563ab468b7",
|
||||
"subsection_name": "edX Exams"
|
||||
}
|
||||
],
|
||||
"aggregates": {
|
||||
"Exam": {
|
||||
"score_possible": 6,
|
||||
"score_earned": 0
|
||||
},
|
||||
"Homework": {
|
||||
"score_possible": 16,
|
||||
"score_earned": 5
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"course_id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"email": "verified@example.com",
|
||||
"user_id": 8,
|
||||
"username": "verified",
|
||||
"full_name": "",
|
||||
"passed": false,
|
||||
"percent": 0,
|
||||
"letter_grade": null,
|
||||
"progress_page_url": "/courses/course-v1:edX+DemoX+Demo_Course/progress/8/",
|
||||
"section_breakdown": [
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Introduction",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b",
|
||||
"subsection_name": "Demo Course Overview"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 1: Getting Started",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/3.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 3,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations",
|
||||
"subsection_name": "Lesson 1 - Getting Started"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Homework",
|
||||
"chapter_name": "Example Week 1: Getting Started",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/11.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": "Ex 01",
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 11,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations",
|
||||
"subsection_name": "Homework - Question Styles"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Lesson 2 - Let's Get Interactive!"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Homework",
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/5.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": "Ex 02",
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 5,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Homework - Labs and Demos"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/19.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 19,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Homework - Essays"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "Lesson 3 - Be Social"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc027bcb4fe9afb744d2e8415855",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "Homework - Find Your Study Buddy"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@6ab9c442501d472c8ed200e367b4edfa",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "More Ways to Connect"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Exam",
|
||||
"chapter_name": "About Exams and Certificates",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/6.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 6,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@1414ffd5143b4b508f739b563ab468b7",
|
||||
"subsection_name": "edX Exams"
|
||||
}
|
||||
],
|
||||
"aggregates": {
|
||||
"Exam": {
|
||||
"score_possible": 6,
|
||||
"score_earned": 0
|
||||
},
|
||||
"Homework": {
|
||||
"score_possible": 16,
|
||||
"score_earned": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"course_id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"email": "staff@example.com",
|
||||
"user_id": 9,
|
||||
"username": "staff",
|
||||
"full_name": "",
|
||||
"passed": false,
|
||||
"percent": 0,
|
||||
"letter_grade": null,
|
||||
"progress_page_url": "/courses/course-v1:edX+DemoX+Demo_Course/progress/9/",
|
||||
"section_breakdown": [
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Introduction",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b",
|
||||
"subsection_name": "Demo Course Overview"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 1: Getting Started",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/3.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 3,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations",
|
||||
"subsection_name": "Lesson 1 - Getting Started"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Homework",
|
||||
"chapter_name": "Example Week 1: Getting Started",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/11.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": "Ex 01",
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 11,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations",
|
||||
"subsection_name": "Homework - Question Styles"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Lesson 2 - Let's Get Interactive!"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Homework",
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/5.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": "Ex 02",
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 5,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Homework - Labs and Demos"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 2: Get Interactive",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/19.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 19,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions",
|
||||
"subsection_name": "Homework - Essays"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "Lesson 3 - Be Social"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc027bcb4fe9afb744d2e8415855",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "Homework - Find Your Study Buddy"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "Example Week 3: Be Social",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@6ab9c442501d472c8ed200e367b4edfa",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration",
|
||||
"subsection_name": "More Ways to Connect"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": "Exam",
|
||||
"chapter_name": "About Exams and Certificates",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": true,
|
||||
"grade_description": "(0.00/6.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 6,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@1414ffd5143b4b508f739b563ab468b7",
|
||||
"subsection_name": "edX Exams"
|
||||
},
|
||||
{
|
||||
"are_grades_published": true,
|
||||
"auto_grade": false,
|
||||
"category": null,
|
||||
"chapter_name": "holding section",
|
||||
"comment": "",
|
||||
"detail": "",
|
||||
"displayed_value": "0.00",
|
||||
"is_graded": false,
|
||||
"grade_description": "(0.00/0.00)",
|
||||
"is_ag": false,
|
||||
"is_average": false,
|
||||
"is_manually_graded": false,
|
||||
"label": null,
|
||||
"letter_grade": null,
|
||||
"module_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@07bc32474380492cb34f76e5f9d9a135",
|
||||
"percent": 0,
|
||||
"score_earned": 0,
|
||||
"score_possible": 0,
|
||||
"section_block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@9fca584977d04885bc911ea76a9ef29e",
|
||||
"subsection_name": "New Subsection"
|
||||
}
|
||||
],
|
||||
"aggregates": {
|
||||
"Exam": {
|
||||
"score_possible": 6,
|
||||
"score_earned": 0
|
||||
},
|
||||
"Homework": {
|
||||
"score_possible": 16,
|
||||
"score_earned": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
// CommentDetails.defaultProps = {
|
||||
// id: null,
|
||||
// postId: null,
|
||||
// name: '',
|
||||
// email: 'example@example.com',
|
||||
// body: '',
|
||||
// };
|
||||
|
||||
// CommentDetails.propTypes = {
|
||||
// id: PropTypes.number,
|
||||
// postId: PropTypes.number,
|
||||
// name: PropTypes.string,
|
||||
// email: emailPropType,
|
||||
// body: PropTypes.string,
|
||||
// };
|
||||
|
||||
|
||||
94
src/components/Header/index.jsx
Normal file
94
src/components/Header/index.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Collapse,
|
||||
Navbar,
|
||||
NavbarToggler,
|
||||
NavbarBrand,
|
||||
Nav,
|
||||
NavItem,
|
||||
NavLink,
|
||||
UncontrolledDropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from 'reactstrap';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import apiClient from '../../data/apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import EdxLogo from '../../../assets/edx-sm.png';
|
||||
|
||||
class Header extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.state = {
|
||||
mobileNavOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.setState({
|
||||
mobileNavOpen: !this.state.mobileNavOpen,
|
||||
});
|
||||
}
|
||||
|
||||
getUserProfileImageIcon() {
|
||||
const screenReaderText = `Profile image for ${this.props.username}`;
|
||||
|
||||
if (this.props.userProfileImageUrl) {
|
||||
return <img src={this.props.userProfileImageUrl} alt={screenReaderText} />;
|
||||
}
|
||||
return <Icon className={['fa', 'fa-user', 'px-3']} screenReaderText={screenReaderText} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Navbar light expand="md" className="border-bottom">
|
||||
<NavbarBrand href={configuration.LMS_BASE_URL}>
|
||||
<img src={EdxLogo} alt="edX logo" height="30" width="60" />
|
||||
</NavbarBrand>
|
||||
<NavbarToggler onClick={this.toggle} />
|
||||
<Collapse isOpen={this.state.mobileNavOpen} navbar>
|
||||
<Nav className="ml-auto" navbar>
|
||||
<UncontrolledDropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>
|
||||
{this.getUserProfileImageIcon()}
|
||||
{this.props.username}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu right>
|
||||
<DropdownItem href={`${configuration.LMS_BASE_URL}/dashboard`}>
|
||||
Dashboard
|
||||
</DropdownItem>
|
||||
<DropdownItem href={`${configuration.LMS_BASE_URL}/u/${this.props.username}`}>
|
||||
Profile
|
||||
</DropdownItem>
|
||||
<DropdownItem href={`${configuration.LMS_BASE_URL}/account/settings`}>
|
||||
Account
|
||||
</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
<DropdownItem onClick={() => apiClient.logout()}>
|
||||
Logout
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Header.defaultProps = {
|
||||
username: null,
|
||||
userProfileImageUrl: null,
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
username: PropTypes.string,
|
||||
userProfileImageUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
17
src/config/index.js
Normal file
17
src/config/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const configuration = {
|
||||
BASE_URL: process.env.BASE_URL,
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
LOGIN_URL: process.env.LOGIN_URL,
|
||||
LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
|
||||
DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
|
||||
SECURE_COOKIES: process.env.NODE_ENV !== 'development',
|
||||
SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
CSRF_COOKIE_NAME: process.env.CSRF_COOKIE_NAME,
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
export { configuration, features };
|
||||
@@ -1,15 +1,69 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Gradebook from '../../components/Gradebook';
|
||||
import { fetchComment } from '../../data/actions/comment';
|
||||
import {
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
fetchPrevNextGrades,
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
filterColumns,
|
||||
updateBanner,
|
||||
} from '../../data/actions/grades';
|
||||
import { fetchCohorts } from '../../data/actions/cohorts';
|
||||
import { fetchTracks } from '../../data/actions/tracks';
|
||||
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
||||
|
||||
const mapStateToProps = state => (
|
||||
{
|
||||
grades: state.grades.results,
|
||||
headings: state.grades.headings,
|
||||
tracks: state.tracks.results,
|
||||
cohorts: state.cohorts.results,
|
||||
selectedTrack: state.grades.selectedTrack,
|
||||
selectedCohort: state.grades.selectedCohort,
|
||||
format: state.grades.gradeFormat,
|
||||
showSuccess: state.grades.showSuccess,
|
||||
prevPage: state.grades.prevPage,
|
||||
nextPage: state.grades.nextPage,
|
||||
assignmnetTypes: state.assignmentTypes.results,
|
||||
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
|
||||
showSpinner: state.grades.showSpinner,
|
||||
}
|
||||
);
|
||||
|
||||
const mapDispatchToProps = dispatch => (
|
||||
{
|
||||
getUserGrades: (courseId, cohort, track) => {
|
||||
dispatch(fetchGrades(courseId, cohort, track));
|
||||
},
|
||||
searchForUser: (courseId, searchText, cohort, track) => {
|
||||
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, false));
|
||||
},
|
||||
getPrevNextGrades: (endpoint, cohort, track) => {
|
||||
dispatch(fetchPrevNextGrades(endpoint, cohort, track));
|
||||
},
|
||||
getCohorts: (courseId) => {
|
||||
dispatch(fetchCohorts(courseId));
|
||||
},
|
||||
getTracks: (courseId) => {
|
||||
dispatch(fetchTracks(courseId));
|
||||
},
|
||||
getAssignmentTypes: (courseId) => {
|
||||
dispatch(fetchAssignmentTypes(courseId));
|
||||
},
|
||||
updateGrades: (courseId, updateData, searchText, cohort, track) => {
|
||||
dispatch(updateGrades(courseId, updateData, searchText, cohort, track));
|
||||
},
|
||||
toggleFormat: (formatType) => {
|
||||
dispatch(toggleGradeFormat(formatType));
|
||||
},
|
||||
filterColumns: (filterType, exampleUser) => {
|
||||
dispatch(filterColumns(filterType, exampleUser));
|
||||
},
|
||||
updateBanner: (showSuccess) => {
|
||||
dispatch(updateBanner(showSuccess));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
11
src/containers/Header/index.jsx
Normal file
11
src/containers/Header/index.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchUserProfile } from '@edx/frontend-auth';
|
||||
|
||||
import Header from '../../components/Header';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
username: state.userProfile.username,
|
||||
userProfileImageUrl: state.userProfile.userProfileImageUrl,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Header);
|
||||
35
src/data/actions/assignmentTypes.js
Normal file
35
src/data/actions/assignmentTypes.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ASSIGNMENT_TYPES,
|
||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ARE_GRADES_FROZEN,
|
||||
} from '../constants/actionTypes/assignmentTypes';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const startedFetchingAssignmentTypes = () => ({ type: STARTED_FETCHING_ASSIGNMENT_TYPES });
|
||||
const errorFetchingAssignmentTypes = () => ({ type: ERROR_FETCHING_ASSIGNMENT_TYPES });
|
||||
const gotAssignmentTypes = assignmentTypes => ({ type: GOT_ASSIGNMENT_TYPES, assignmentTypes });
|
||||
const gotGradesFrozen = areGradesFrozen => ({ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen });
|
||||
|
||||
const fetchAssignmentTypes = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingAssignmentTypes());
|
||||
return LmsApiService.fetchAssignmentTypes(courseId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotAssignmentTypes(Object.keys(data.assignment_types)));
|
||||
dispatch(gotGradesFrozen(data.grades_frozen));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingAssignmentTypes());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
fetchAssignmentTypes,
|
||||
startedFetchingAssignmentTypes,
|
||||
gotAssignmentTypes,
|
||||
errorFetchingAssignmentTypes,
|
||||
};
|
||||
|
||||
91
src/data/actions/assignmentTypes.test.js
Normal file
91
src/data/actions/assignmentTypes.test.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import apiClient from '../apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
import {
|
||||
STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ASSIGNMENT_TYPES,
|
||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ARE_GRADES_FROZEN,
|
||||
} from '../constants/actionTypes/assignmentTypes';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const axiosMock = new MockAdapter(apiClient);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
describe('fetchAssignmentTypes', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const responseData = {
|
||||
assignment_types: {
|
||||
Exam: {
|
||||
drop_count: 0,
|
||||
min_count: 1,
|
||||
short_label: 'Exam',
|
||||
type: 'Exam',
|
||||
weight: 0.25,
|
||||
},
|
||||
Homework: {
|
||||
drop_count: 1,
|
||||
min_count: 3,
|
||||
short_label: 'Ex',
|
||||
type: 'Homework',
|
||||
weight: 0.75,
|
||||
},
|
||||
},
|
||||
grades_frozen: false,
|
||||
};
|
||||
it('dispatches success action after fetching fetchAssignmentTypes', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
|
||||
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: responseData.grades_frozen },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching cohorts', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
{ type: ERROR_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches frozen grade action with True value after fetching', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
|
||||
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: true },
|
||||
];
|
||||
const store = mockStore();
|
||||
responseData.grades_frozen = true;
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
31
src/data/actions/cohorts.js
Normal file
31
src/data/actions/cohorts.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const startedFetchingCohorts = () => ({ type: STARTED_FETCHING_COHORTS });
|
||||
const errorFetchingCohorts = () => ({ type: ERROR_FETCHING_COHORTS });
|
||||
const gotCohorts = cohorts => ({ type: GOT_COHORTS, cohorts });
|
||||
|
||||
const fetchCohorts = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingCohorts());
|
||||
return LmsApiService.fetchCohorts(courseId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotCohorts(data.cohorts));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingCohorts());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
fetchCohorts,
|
||||
startedFetchingCohorts,
|
||||
gotCohorts,
|
||||
errorFetchingCohorts,
|
||||
};
|
||||
74
src/data/actions/cohorts.test.js
Normal file
74
src/data/actions/cohorts.test.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import apiClient from '../apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchCohorts } from './cohorts';
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const axiosMock = new MockAdapter(apiClient);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
describe('fetchCohorts', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
|
||||
it('dispatches success action after fetching cohorts', () => {
|
||||
const responseData = {
|
||||
cohorts: [
|
||||
{
|
||||
assignment_type: 'manual',
|
||||
group_id: null,
|
||||
id: 1,
|
||||
name: 'default_group',
|
||||
user_count: 2,
|
||||
user_partition_id: null,
|
||||
},
|
||||
{
|
||||
assignment_type: 'auto',
|
||||
group_id: null,
|
||||
id: 2,
|
||||
name: 'auto_group',
|
||||
user_count: 5,
|
||||
user_partition_id: null,
|
||||
}],
|
||||
};
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: GOT_COHORTS, cohorts: responseData.cohorts },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/courses/${courseId}/cohorts/`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchCohorts(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching cohorts', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: ERROR_FETCHING_COHORTS },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/courses/${courseId}/cohorts/`)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchCohorts(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'whatwg-fetch';
|
||||
import {
|
||||
STARTED_FETCHING_COMMENT,
|
||||
FINISHED_FETCHING_COMMENT,
|
||||
ERROR_FETCHING_COMMENT,
|
||||
GET_COMMENT,
|
||||
} from '../constants/actionTypes/comment';
|
||||
|
||||
const startedFetchingComment = () => ({ type: STARTED_FETCHING_COMMENT });
|
||||
const finishedFetchingComment = () => ({ type: FINISHED_FETCHING_COMMENT });
|
||||
const errorFetchingComment = () => ({ type: ERROR_FETCHING_COMMENT });
|
||||
const getComment = comment => ({ type: GET_COMMENT, comment });
|
||||
const fetchComment = commentId => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingComment());
|
||||
return fetch(`https://jsonplaceholder.typicode.com/comments/${commentId}`)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
})
|
||||
.then((data) => {
|
||||
dispatch(getComment(data));
|
||||
dispatch(finishedFetchingComment());
|
||||
})
|
||||
.catch(() => dispatch(errorFetchingComment()));
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
startedFetchingComment,
|
||||
finishedFetchingComment,
|
||||
errorFetchingComment,
|
||||
getComment,
|
||||
fetchComment,
|
||||
};
|
||||
167
src/data/actions/grades.js
Normal file
167
src/data/actions/grades.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
GRADE_UPDATE_REQUEST,
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
SORT_GRADES,
|
||||
FILTER_COLUMNS,
|
||||
UPDATE_BANNER,
|
||||
} from '../constants/actionTypes/grades';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import store from '../store';
|
||||
import { headingMapper, gradeSortMap, sortAlphaAsc } from './utils';
|
||||
import apiClient from '../apiClient';
|
||||
|
||||
const defaultAssignmentFilter = 'All';
|
||||
|
||||
const sortGrades = (columnName, direction) => {
|
||||
const sortFn = gradeSortMap(columnName, direction);
|
||||
const { results } = store.getState().grades;
|
||||
results.sort(sortFn);
|
||||
|
||||
/* have to make a copy of results or React wont know there was
|
||||
* a change and wont trigger a re-render
|
||||
*/
|
||||
return ({ type: SORT_GRADES, results: [...results] });
|
||||
};
|
||||
|
||||
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
|
||||
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
|
||||
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
|
||||
const gotGrades = (grades, cohort, track, headings, prev, next) => ({
|
||||
type: GOT_GRADES,
|
||||
grades,
|
||||
cohort,
|
||||
track,
|
||||
headings,
|
||||
prev,
|
||||
next,
|
||||
});
|
||||
|
||||
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
|
||||
const gradeUpdateSuccess = responseData => ({
|
||||
type: GRADE_UPDATE_SUCCESS,
|
||||
payload: { responseData },
|
||||
});
|
||||
const gradeUpdateFailure = error => ({
|
||||
type: GRADE_UPDATE_FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
|
||||
const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType });
|
||||
|
||||
const filterColumns = (filterType, exampleUser) => (
|
||||
dispatch => dispatch({
|
||||
type: FILTER_COLUMNS,
|
||||
headings: headingMapper(filterType)(dispatch, exampleUser),
|
||||
})
|
||||
);
|
||||
|
||||
const updateBanner = showSuccess => ({ type: UPDATE_BANNER, showSuccess });
|
||||
|
||||
const fetchGrades = (courseId, cohort, track, showSuccess) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
return LmsApiService.fetchGradebookData(courseId, null, cohort, track)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotGrades(
|
||||
data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]),
|
||||
data.previous,
|
||||
data.next,
|
||||
));
|
||||
dispatch(finishedFetchingGrades());
|
||||
dispatch(updateBanner(!!showSuccess));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingGrades());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const fetchMatchingUserGrades = (courseId, searchText, cohort, track, showSuccess) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotGrades(
|
||||
data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]),
|
||||
data.previous,
|
||||
data.next,
|
||||
));
|
||||
dispatch(finishedFetchingGrades());
|
||||
dispatch(updateBanner(showSuccess));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingGrades());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const fetchPrevNextGrades = (endpoint, cohort, track) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
return apiClient.get(endpoint)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotGrades(
|
||||
data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]),
|
||||
data.previous,
|
||||
data.next,
|
||||
));
|
||||
dispatch(finishedFetchingGrades());
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingGrades());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
const updateGrades = (courseId, updateData, searchText, cohort, track) => (
|
||||
(dispatch) => {
|
||||
dispatch(gradeUpdateRequest());
|
||||
return LmsApiService.updateGradebookData(courseId, updateData)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gradeUpdateSuccess(data));
|
||||
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, true));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(gradeUpdateFailure(error));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
startedFetchingGrades,
|
||||
finishedFetchingGrades,
|
||||
errorFetchingGrades,
|
||||
gotGrades,
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
fetchPrevNextGrades,
|
||||
gradeUpdateRequest,
|
||||
gradeUpdateSuccess,
|
||||
gradeUpdateFailure,
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
sortGrades,
|
||||
filterColumns,
|
||||
updateBanner,
|
||||
};
|
||||
142
src/data/actions/grades.test.js
Normal file
142
src/data/actions/grades.test.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import apiClient from '../apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchGrades } from './grades';
|
||||
import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
UPDATE_BANNER,
|
||||
} from '../constants/actionTypes/grades';
|
||||
import { sortAlphaAsc } from './utils';
|
||||
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const axiosMock = new MockAdapter(apiClient);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
describe('fetchGrades', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const expectedCohort = 1;
|
||||
const expectedTrack = 'verified';
|
||||
const fetchGradesURL = `${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/?page_size=10&cohort_id=${expectedCohort}&enrollment_mode=${expectedTrack}`;
|
||||
const responseData = {
|
||||
next: `${fetchGradesURL}&cursor=2344fda`,
|
||||
previous: null,
|
||||
results: [
|
||||
{
|
||||
course_id: courseId,
|
||||
email: 'user1@example.com',
|
||||
username: 'user1',
|
||||
user_id: 1,
|
||||
percent: 0.5,
|
||||
letter_grade: null,
|
||||
section_breakdown: [
|
||||
{
|
||||
subsection_name: 'Demo Course Overview',
|
||||
score_earned: 0,
|
||||
score_possible: 0,
|
||||
percent: 0,
|
||||
displayed_value: '0.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
{
|
||||
subsection_name: 'Example Week 1: Getting Started',
|
||||
score_earned: 1,
|
||||
score_possible: 1,
|
||||
percent: 1,
|
||||
displayed_value: '1.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
course_id: courseId,
|
||||
email: 'user22@example.com',
|
||||
username: 'user22',
|
||||
user_id: 22,
|
||||
percent: 0,
|
||||
letter_grade: null,
|
||||
section_breakdown: [
|
||||
{
|
||||
subsection_name: 'Demo Course Overview',
|
||||
score_earned: 0,
|
||||
score_possible: 0,
|
||||
percent: 0,
|
||||
displayed_value: '0.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
{
|
||||
subsection_name: 'Example Week 1: Getting Started',
|
||||
score_earned: 1,
|
||||
score_possible: 1,
|
||||
percent: 0,
|
||||
displayed_value: '0.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
it('dispatches success action after fetching grades', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{
|
||||
type: GOT_GRADES,
|
||||
grades: responseData.results.sort(sortAlphaAsc),
|
||||
cohort: expectedCohort,
|
||||
track: expectedTrack,
|
||||
headings: [
|
||||
{
|
||||
columnSortable: true,
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
onSort: expect.anything(),
|
||||
},
|
||||
{
|
||||
columnSortable: true,
|
||||
key: 'total',
|
||||
label: 'Total',
|
||||
onSort: expect.anything(),
|
||||
},
|
||||
],
|
||||
prev: responseData.previous,
|
||||
next: responseData.next,
|
||||
},
|
||||
{ type: FINISHED_FETCHING_GRADES },
|
||||
{ type: UPDATE_BANNER, showSuccess: false },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(fetchGradesURL)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchGrades(courseId, expectedCohort, expectedTrack, false)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching grades', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: ERROR_FETCHING_GRADES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(fetchGradesURL)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchGrades(courseId, expectedCohort, expectedTrack, false)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import {
|
||||
STARTED_FETCHING_POSTS,
|
||||
FINISHED_FETCHING_POSTS,
|
||||
GET_POSTS,
|
||||
} from '../constants/actionTypes/posts';
|
||||
|
||||
const startedFetchingPosts = () => (
|
||||
{
|
||||
type: STARTED_FETCHING_POSTS,
|
||||
}
|
||||
);
|
||||
|
||||
const finishedFetchingPosts = () => (
|
||||
{
|
||||
type: FINISHED_FETCHING_POSTS,
|
||||
}
|
||||
);
|
||||
|
||||
const getPosts = posts => (
|
||||
{
|
||||
type: GET_POSTS,
|
||||
posts,
|
||||
}
|
||||
);
|
||||
|
||||
const fetchPosts = () => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingPosts());
|
||||
return fetch('https://jsonplaceholder.typicode.com/posts')
|
||||
// TODO: handle response error
|
||||
.then(response => response.json())
|
||||
.then((data) => {
|
||||
dispatch(getPosts(data));
|
||||
dispatch(finishedFetchingPosts());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
startedFetchingPosts,
|
||||
finishedFetchingPosts,
|
||||
getPosts,
|
||||
fetchPosts,
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import fetchMock from 'fetch-mock';
|
||||
|
||||
import {
|
||||
startedFetchingPosts,
|
||||
finishedFetchingPosts,
|
||||
getPosts,
|
||||
fetchPosts,
|
||||
} from './posts';
|
||||
import {
|
||||
STARTED_FETCHING_POSTS,
|
||||
GET_POSTS,
|
||||
FINISHED_FETCHING_POSTS,
|
||||
} from '../constants/actionTypes/posts';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it('sends started fetching post action', () => {
|
||||
const expected = { type: STARTED_FETCHING_POSTS };
|
||||
expect(startedFetchingPosts()).toEqual(expected);
|
||||
});
|
||||
|
||||
it('sends finished fetching posts', () => {
|
||||
const expected = { type: FINISHED_FETCHING_POSTS };
|
||||
expect(finishedFetchingPosts()).toEqual(expected);
|
||||
});
|
||||
|
||||
it('sends posts', () => {
|
||||
const data = 'data';
|
||||
const expected = { type: GET_POSTS, posts: data };
|
||||
expect(getPosts(data)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('fetches posts', () => {
|
||||
const posts = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'title',
|
||||
body: 'body',
|
||||
},
|
||||
];
|
||||
fetchMock.getOnce('https://jsonplaceholder.typicode.com/posts', {
|
||||
body: JSON.stringify({ posts }),
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
const store = mockStore({ posts: [] });
|
||||
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_POSTS },
|
||||
{ type: GET_POSTS, posts: { posts } },
|
||||
{ type: FINISHED_FETCHING_POSTS },
|
||||
];
|
||||
|
||||
return store.dispatch(fetchPosts()).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
31
src/data/actions/tracks.js
Normal file
31
src/data/actions/tracks.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const startedFetchingTracks = () => ({ type: STARTED_FETCHING_TRACKS });
|
||||
const errorFetchingTracks = () => ({ type: ERROR_FETCHING_TRACKS });
|
||||
const gotTracks = tracks => ({ type: GOT_TRACKS, tracks });
|
||||
|
||||
const fetchTracks = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingTracks());
|
||||
return LmsApiService.fetchTracks(courseId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotTracks(data.course_modes));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingTracks());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
fetchTracks,
|
||||
startedFetchingTracks,
|
||||
gotTracks,
|
||||
errorFetchingTracks,
|
||||
};
|
||||
80
src/data/actions/tracks.test.js
Normal file
80
src/data/actions/tracks.test.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import apiClient from '../apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchTracks } from './tracks';
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const axiosMock = new MockAdapter(apiClient);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
describe('fetchTracks', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
|
||||
it('dispatches success action after fetching tracks', () => {
|
||||
const responseData = {
|
||||
course_modes: [
|
||||
{
|
||||
slug: 'audit',
|
||||
name: 'Audit',
|
||||
min_price: 0,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: null,
|
||||
description: null,
|
||||
sku: '68EFFFF',
|
||||
bulk_sku: null,
|
||||
},
|
||||
{
|
||||
slug: 'verified',
|
||||
name: 'Verified Certificate',
|
||||
min_price: 100,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: '2021-05-04T18:08:12.644361Z',
|
||||
description: null,
|
||||
sku: '8CF08E5',
|
||||
bulk_sku: 'A5B6DBE',
|
||||
}],
|
||||
};
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: GOT_TRACKS, tracks: responseData.course_modes },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchTracks(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching tracks', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: ERROR_FETCHING_TRACKS },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}`)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchTracks(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
125
src/data/actions/utils.js
Normal file
125
src/data/actions/utils.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { sortGrades } from './grades';
|
||||
|
||||
const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||
const a = gradeRowA.username.toUpperCase();
|
||||
const b = gradeRowB.username.toUpperCase();
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const sortAlphaDesc = (gradeRowA, gradeRowB) => {
|
||||
const a = gradeRowA.username.toUpperCase();
|
||||
const b = gradeRowB.username.toUpperCase();
|
||||
if (a < b) {
|
||||
return 1;
|
||||
}
|
||||
if (a > b) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const sortNumerically = (colKey, direction) => {
|
||||
function getPercents(gradeRowA, gradeRowB) {
|
||||
if (colKey !== 'total') {
|
||||
return {
|
||||
a: gradeRowA.section_breakdown.find(x => x.label === colKey).percent,
|
||||
b: gradeRowB.section_breakdown.find(x => x.label === colKey).percent,
|
||||
};
|
||||
}
|
||||
return {
|
||||
a: gradeRowA.percent,
|
||||
b: gradeRowB.percent,
|
||||
};
|
||||
}
|
||||
|
||||
function sortNumAsc(gradeRowA, gradeRowB) {
|
||||
const { a, b } = getPercents(gradeRowA, gradeRowB);
|
||||
return a - b;
|
||||
}
|
||||
|
||||
function sortNumDesc(gradeRowA, gradeRowB) {
|
||||
const { a, b } = getPercents(gradeRowA, gradeRowB);
|
||||
return b - a;
|
||||
}
|
||||
|
||||
return direction === 'desc' ? sortNumDesc : sortNumAsc;
|
||||
};
|
||||
|
||||
function gradeSortMap(columnName, direction) {
|
||||
if (columnName === 'username' && direction === 'desc') {
|
||||
return sortAlphaDesc;
|
||||
} else if (columnName === 'username') {
|
||||
return sortAlphaAsc;
|
||||
}
|
||||
return sortNumerically(columnName, direction);
|
||||
}
|
||||
|
||||
const headingMapper = (filterKey) => {
|
||||
function all(dispatch, entry) {
|
||||
if (entry) {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { dispatch(sortGrades('username', direction)); },
|
||||
}];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label)
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: direction => dispatch(sortGrades(s.label, direction)),
|
||||
}));
|
||||
|
||||
const totals = [{
|
||||
label: 'Total',
|
||||
key: 'total',
|
||||
columnSortable: true,
|
||||
onSort: direction => dispatch(sortGrades('total', direction)),
|
||||
}];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function some(dispatch, entry) {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { dispatch(sortGrades('username', direction)); },
|
||||
}];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label && section.category === filterKey)
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: false,
|
||||
onSort: (direction) => { this.sortNumerically(s.label, direction); },
|
||||
}));
|
||||
|
||||
const totals = [{
|
||||
label: 'Total',
|
||||
key: 'total',
|
||||
columnSortable: true,
|
||||
onSort: direction => dispatch(sortGrades('total', direction)),
|
||||
}];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
}
|
||||
|
||||
return filterKey === 'All' ? all : some;
|
||||
};
|
||||
|
||||
export { headingMapper, gradeSortMap, sortAlphaAsc };
|
||||
|
||||
16
src/data/apiClient.js
Normal file
16
src/data/apiClient.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
|
||||
|
||||
import { configuration } from '../config';
|
||||
|
||||
const apiClient = getAuthenticatedAPIClient({
|
||||
appBaseUrl: configuration.BASE_URL,
|
||||
authBaseUrl: configuration.LMS_BASE_URL,
|
||||
loginUrl: configuration.LOGIN_URL,
|
||||
logoutUrl: configuration.LOGOUT_URL,
|
||||
csrfTokenApiPath: process.env.CSRF_TOKEN_API_PATH,
|
||||
refreshAccessTokenEndpoint: configuration.REFRESH_ACCESS_TOKEN_ENDPOINT,
|
||||
accessTokenCookieName: configuration.ACCESS_TOKEN_COOKIE_NAME,
|
||||
csrfCookieName: configuration.CSRF_COOKIE_NAME,
|
||||
});
|
||||
|
||||
export default apiClient;
|
||||
12
src/data/constants/actionTypes/assignmentTypes.js
Normal file
12
src/data/constants/actionTypes/assignmentTypes.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const STARTED_FETCHING_ASSIGNMENT_TYPES = 'STARTED_FETCHING_ASSIGNMENT_TYPES';
|
||||
const GOT_ASSIGNMENT_TYPES = 'GOT_ASSIGNMENT_TYPES';
|
||||
const ERROR_FETCHING_ASSIGNMENT_TYPES = 'ERROR_FETCHING_ASSIGNMENT_TYPES';
|
||||
const GOT_ARE_GRADES_FROZEN = 'GOT_ARE_GRADES_FROZEN';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ASSIGNMENT_TYPES,
|
||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ARE_GRADES_FROZEN,
|
||||
};
|
||||
|
||||
9
src/data/constants/actionTypes/cohorts.js
Normal file
9
src/data/constants/actionTypes/cohorts.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const STARTED_FETCHING_COHORTS = 'STARTED_FETCHING_COHORTS';
|
||||
const GOT_COHORTS = 'GOT_COHORTS';
|
||||
const ERROR_FETCHING_COHORTS = 'ERROR_FETCHING_COHORTS';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
const STARTED_FETCHING_COMMENT = 'STARTED_FETCHING_COMMENT';
|
||||
const FINISHED_FETCHING_COMMENT = 'FINISHED_FETCHING_COMMENT';
|
||||
const ERROR_FETCHING_COMMENT = 'ERROR_FETCHING_COMMENT';
|
||||
const GET_COMMENT = 'GET_COMMENT';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_COMMENT,
|
||||
FINISHED_FETCHING_COMMENT,
|
||||
ERROR_FETCHING_COMMENT,
|
||||
GET_COMMENT,
|
||||
};
|
||||
27
src/data/constants/actionTypes/grades.js
Normal file
27
src/data/constants/actionTypes/grades.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const STARTED_FETCHING_GRADES = 'STARTED_FETCHING_GRADES';
|
||||
const FINISHED_FETCHING_GRADES = 'FINISHED_FETCHING_GRADES';
|
||||
const ERROR_FETCHING_GRADES = 'ERROR_FETCHING_GRADES';
|
||||
const GOT_GRADES = 'GOT_GRADES';
|
||||
|
||||
const GRADE_UPDATE_REQUEST = 'GRADE_UPDATE_REQUEST';
|
||||
const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
|
||||
const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
|
||||
|
||||
const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT';
|
||||
const SORT_GRADES = 'SORT_GRADES';
|
||||
const FILTER_COLUMNS = 'FILTER_COLUMNS';
|
||||
const UPDATE_BANNER = 'UPDATE_BANNER';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_GRADES,
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
GRADE_UPDATE_REQUEST,
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
SORT_GRADES,
|
||||
FILTER_COLUMNS,
|
||||
UPDATE_BANNER,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
const STARTED_FETCHING_POSTS = 'STARTED_FETCHING_POSTS';
|
||||
const GET_POSTS = 'GET_POSTS';
|
||||
const FINISHED_FETCHING_POSTS = 'FINISHED_FETCHING_POSTS';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_POSTS,
|
||||
GET_POSTS,
|
||||
FINISHED_FETCHING_POSTS,
|
||||
};
|
||||
9
src/data/constants/actionTypes/tracks.js
Normal file
9
src/data/constants/actionTypes/tracks.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const STARTED_FETCHING_TRACKS = 'STARTED_FETCHING_TRACKS';
|
||||
const GOT_TRACKS = 'GOT_TRACKS';
|
||||
const ERROR_FETCHING_TRACKS = 'ERROR_FETCHING_TRACKS';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
};
|
||||
48
src/data/reducers/assignmentTypes.js
Normal file
48
src/data/reducers/assignmentTypes.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ASSIGNMENT_TYPES,
|
||||
GOT_ARE_GRADES_FROZEN,
|
||||
} from '../constants/actionTypes/assignmentTypes';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
|
||||
const assignmentTypes = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_ASSIGNMENT_TYPES:
|
||||
return {
|
||||
...state,
|
||||
results: action.assignmentTypes,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
case STARTED_FETCHING_ASSIGNMENT_TYPES:
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
};
|
||||
case ERROR_FETCHING_ASSIGNMENT_TYPES:
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
case GOT_ARE_GRADES_FROZEN:
|
||||
return {
|
||||
...state,
|
||||
areGradesFrozen: action.areGradesFrozen,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default assignmentTypes;
|
||||
|
||||
68
src/data/reducers/assignmentTypes.test.js
Normal file
68
src/data/reducers/assignmentTypes.test.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
import {
|
||||
STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ASSIGNMENT_TYPES,
|
||||
GOT_ARE_GRADES_FROZEN,
|
||||
} from '../constants/actionTypes/assignmentTypes';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
const assignmentTypesData = ['Exam', 'Homework'];
|
||||
|
||||
describe('assignmentTypes reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(assignmentTypes(undefined, {})).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('updates fetch assignmentTypes request state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
startedFetching: true,
|
||||
};
|
||||
expect(assignmentTypes(undefined, {
|
||||
type: STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch assignmentTypes success state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
results: assignmentTypesData,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(assignmentTypes(undefined, {
|
||||
type: GOT_ASSIGNMENT_TYPES,
|
||||
assignmentTypes: assignmentTypesData,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch assignmentTypes failure state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(assignmentTypes(undefined, {
|
||||
type: ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates areGradesFrozen success state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
areGradesFrozen: true,
|
||||
};
|
||||
expect(assignmentTypes(undefined, {
|
||||
type: GOT_ARE_GRADES_FROZEN,
|
||||
areGradesFrozen: true,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
});
|
||||
40
src/data/reducers/cohorts.js
Normal file
40
src/data/reducers/cohorts.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
|
||||
const cohorts = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_COHORTS:
|
||||
return {
|
||||
...state,
|
||||
results: action.cohorts,
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
};
|
||||
case STARTED_FETCHING_COHORTS:
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
};
|
||||
case ERROR_FETCHING_COHORTS:
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default cohorts;
|
||||
|
||||
70
src/data/reducers/cohorts.test.js
Normal file
70
src/data/reducers/cohorts.test.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import cohorts from './cohorts';
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
const cohortsData = [
|
||||
{
|
||||
assignment_type: 'manual',
|
||||
group_id: null,
|
||||
id: 1,
|
||||
name: 'default_group',
|
||||
user_count: 2,
|
||||
user_partition_id: null,
|
||||
},
|
||||
{
|
||||
assignment_type: 'auto',
|
||||
group_id: null,
|
||||
id: 2,
|
||||
name: 'auto_group',
|
||||
user_count: 5,
|
||||
user_partition_id: null,
|
||||
}];
|
||||
|
||||
describe('cohorts reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(cohorts(undefined, {})).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('updates fetch cohorts request state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
startedFetching: true,
|
||||
};
|
||||
expect(cohorts(undefined, {
|
||||
type: STARTED_FETCHING_COHORTS,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch cohorts success state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
results: cohortsData,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(cohorts(undefined, {
|
||||
type: GOT_COHORTS,
|
||||
cohorts: cohortsData,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch cohorts failure state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(cohorts(undefined, {
|
||||
type: ERROR_FETCHING_COHORTS,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
STARTED_FETCHING_COMMENT,
|
||||
ERROR_FETCHING_COMMENT,
|
||||
GET_COMMENT,
|
||||
} from '../constants/actionTypes/comment';
|
||||
|
||||
const initialState = {
|
||||
details: {
|
||||
id: null,
|
||||
postId: null,
|
||||
name: '',
|
||||
email: 'example@example.com',
|
||||
body: '',
|
||||
},
|
||||
startedFetching: false,
|
||||
finishedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
const comment = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GET_COMMENT:
|
||||
return {
|
||||
...state,
|
||||
details: { ...action.comment },
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
};
|
||||
case STARTED_FETCHING_COMMENT:
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
finishedFetching: false,
|
||||
};
|
||||
case ERROR_FETCHING_COMMENT:
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default comment;
|
||||
77
src/data/reducers/grades.js
Normal file
77
src/data/reducers/grades.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_COLUMNS,
|
||||
UPDATE_BANNER,
|
||||
SORT_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 grades = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_GRADES:
|
||||
return {
|
||||
...state,
|
||||
results: action.grades,
|
||||
headings: action.headings,
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
selectedTrack: action.track,
|
||||
selectedCohort: action.cohort,
|
||||
prevPage: action.prev,
|
||||
nextPage: action.next,
|
||||
showSpinner: false,
|
||||
};
|
||||
case STARTED_FETCHING_GRADES:
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
finishedFetching: false,
|
||||
showSpinner: true,
|
||||
};
|
||||
case ERROR_FETCHING_GRADES:
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
case TOGGLE_GRADE_FORMAT:
|
||||
return {
|
||||
...state,
|
||||
gradeFormat: action.formatType,
|
||||
};
|
||||
case FILTER_COLUMNS:
|
||||
return {
|
||||
...state,
|
||||
headings: action.headings,
|
||||
};
|
||||
case UPDATE_BANNER:
|
||||
return {
|
||||
...state,
|
||||
showSuccess: action.showSuccess,
|
||||
};
|
||||
case SORT_GRADES:
|
||||
return {
|
||||
...state,
|
||||
results: action.results,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default grades;
|
||||
186
src/data/reducers/grades.test.js
Normal file
186
src/data/reducers/grades.test.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import grades from './grades';
|
||||
import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_COLUMNS,
|
||||
UPDATE_BANNER,
|
||||
SORT_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 headingsData = [
|
||||
{ name: 'exam' },
|
||||
{ name: 'homework2' },
|
||||
];
|
||||
const gradesData = [
|
||||
{
|
||||
course_id: courseId,
|
||||
email: 'user1@example.com',
|
||||
username: 'user1',
|
||||
user_id: 1,
|
||||
percent: 0.5,
|
||||
letter_grade: null,
|
||||
section_breakdown: [
|
||||
{
|
||||
subsection_name: 'Demo Course Overview',
|
||||
score_earned: 0,
|
||||
score_possible: 0,
|
||||
percent: 0,
|
||||
displayed_value: '0.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
{
|
||||
subsection_name: 'Example Week 1: Getting Started',
|
||||
score_earned: 1,
|
||||
score_possible: 1,
|
||||
percent: 1,
|
||||
displayed_value: '1.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
course_id: courseId,
|
||||
email: 'user22@example.com',
|
||||
username: 'user22',
|
||||
user_id: 22,
|
||||
percent: 0,
|
||||
letter_grade: null,
|
||||
section_breakdown: [
|
||||
{
|
||||
subsection_name: 'Demo Course Overview',
|
||||
score_earned: 0,
|
||||
score_possible: 0,
|
||||
percent: 0,
|
||||
displayed_value: '0.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
{
|
||||
subsection_name: 'Example Week 1: Getting Started',
|
||||
score_earned: 1,
|
||||
score_possible: 1,
|
||||
percent: 0,
|
||||
displayed_value: '0.00',
|
||||
grade_description: '(0.00/0.00)',
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
describe('grades reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(grades(undefined, {})).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('updates fetch grades request state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
startedFetching: true,
|
||||
showSpinner: true,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: STARTED_FETCHING_GRADES,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch grades success state', () => {
|
||||
const expectedPrev = 'testPrevUrl';
|
||||
const expectedNext = 'testNextUrl';
|
||||
const expectedTrack = 'verified';
|
||||
const expectedCohortId = 2;
|
||||
const expected = {
|
||||
...initialState,
|
||||
results: gradesData,
|
||||
headings: headingsData,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
selectedTrack: expectedTrack,
|
||||
selectedCohort: expectedCohortId,
|
||||
prevPage: expectedPrev,
|
||||
nextPage: expectedNext,
|
||||
showSpinner: false,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: GOT_GRADES,
|
||||
grades: gradesData,
|
||||
headings: headingsData,
|
||||
prev: expectedPrev,
|
||||
next: expectedNext,
|
||||
track: expectedTrack,
|
||||
cohort: expectedCohortId,
|
||||
showSpinner: true,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates toggle grade format state success', () => {
|
||||
const formatTypeData = 'percent';
|
||||
const expected = {
|
||||
...initialState,
|
||||
gradeFormat: formatTypeData,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: TOGGLE_GRADE_FORMAT,
|
||||
formatType: formatTypeData,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates filter columns state success', () => {
|
||||
const expectedHeadings = headingsData;
|
||||
const expected = {
|
||||
...initialState,
|
||||
headings: expectedHeadings,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: FILTER_COLUMNS,
|
||||
headings: expectedHeadings,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates update_banner state success', () => {
|
||||
const expectedShowSuccess = true;
|
||||
const expected = {
|
||||
...initialState,
|
||||
showSuccess: expectedShowSuccess,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: UPDATE_BANNER,
|
||||
showSuccess: expectedShowSuccess,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates sort grades state success', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
results: gradesData,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: SORT_GRADES,
|
||||
results: gradesData,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch grades failure state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: ERROR_FETCHING_GRADES,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,25 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { userProfile } from '@edx/frontend-auth';
|
||||
|
||||
import posts from './posts';
|
||||
import comment from './comment';
|
||||
import cohorts from './cohorts';
|
||||
import grades from './grades';
|
||||
import tracks from './tracks';
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
|
||||
const identityReducer = (state) => {
|
||||
const newState = { ...state };
|
||||
return newState;
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
posts,
|
||||
comment,
|
||||
// The authentication state is added as initialState when
|
||||
// creating the store in data/store.js.
|
||||
authentication: identityReducer,
|
||||
userProfile,
|
||||
grades,
|
||||
cohorts,
|
||||
tracks,
|
||||
assignmentTypes,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import {
|
||||
GET_POSTS,
|
||||
STARTED_FETCHING_POSTS,
|
||||
FINISHED_FETCHING_POSTS,
|
||||
} from '../constants/actionTypes/posts';
|
||||
|
||||
const posts = (state = { posts: [], startedFetching: false, finishedFetching: false }, action) => {
|
||||
switch (action.type) {
|
||||
case GET_POSTS:
|
||||
return {
|
||||
...state,
|
||||
posts: action.posts,
|
||||
};
|
||||
case STARTED_FETCHING_POSTS:
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
finishedFetching: false,
|
||||
};
|
||||
case FINISHED_FETCHING_POSTS:
|
||||
return {
|
||||
...state,
|
||||
startedFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default posts;
|
||||
@@ -1,45 +0,0 @@
|
||||
import posts from './posts';
|
||||
import {
|
||||
GET_POSTS,
|
||||
STARTED_FETCHING_POSTS,
|
||||
FINISHED_FETCHING_POSTS,
|
||||
} from '../constants/actionTypes/posts';
|
||||
|
||||
const initialState = {
|
||||
posts: [],
|
||||
startedFetching: false,
|
||||
finishedFetching: false,
|
||||
};
|
||||
|
||||
describe('posts reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(posts(undefined, {})).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('adds posts', () => {
|
||||
const fetchedPosts = [1, 2, 3];
|
||||
const expected = {
|
||||
...initialState,
|
||||
posts: fetchedPosts,
|
||||
};
|
||||
expect(posts(undefined, { type: GET_POSTS, posts: fetchedPosts })).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates started fetching posts state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
startedFetching: true,
|
||||
finishedFetching: false,
|
||||
};
|
||||
expect(posts(undefined, { type: STARTED_FETCHING_POSTS })).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates finished fetching posts state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
startedFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(posts(undefined, { type: FINISHED_FETCHING_POSTS })).toEqual(expected);
|
||||
});
|
||||
});
|
||||
40
src/data/reducers/tracks.js
Normal file
40
src/data/reducers/tracks.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
|
||||
const tracks = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_TRACKS:
|
||||
return {
|
||||
...state,
|
||||
results: action.tracks,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
case STARTED_FETCHING_TRACKS:
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
};
|
||||
case ERROR_FETCHING_TRACKS:
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default tracks;
|
||||
|
||||
76
src/data/reducers/tracks.test.js
Normal file
76
src/data/reducers/tracks.test.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import tracks from './tracks';
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
const tracksData = [
|
||||
{
|
||||
slug: 'audit',
|
||||
name: 'Audit',
|
||||
min_price: 0,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: null,
|
||||
description: null,
|
||||
sku: '68EFFFF',
|
||||
bulk_sku: null,
|
||||
},
|
||||
{
|
||||
slug: 'verified',
|
||||
name: 'Verified Certificate',
|
||||
min_price: 100,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: '2021-05-04T18:08:12.644361Z',
|
||||
description: null,
|
||||
sku: '8CF08E5',
|
||||
bulk_sku: 'A5B6DBE',
|
||||
}];
|
||||
|
||||
describe('tracks reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(tracks(undefined, {})).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('updates fetch tracks request state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
startedFetching: true,
|
||||
};
|
||||
expect(tracks(undefined, {
|
||||
type: STARTED_FETCHING_TRACKS,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch tracks success state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
results: tracksData,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(tracks(undefined, {
|
||||
type: GOT_TRACKS,
|
||||
tracks: tracksData,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch tracks failure state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(tracks(undefined, {
|
||||
type: ERROR_FETCHING_TRACKS,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
});
|
||||
64
src/data/services/LmsApiService.js
Normal file
64
src/data/services/LmsApiService.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import apiClient from '../apiClient';
|
||||
import { configuration } from '../../config';
|
||||
|
||||
class LmsApiService {
|
||||
static baseUrl = configuration.LMS_BASE_URL;
|
||||
static pageSize = 10
|
||||
|
||||
static fetchGradebookData(courseId, searchText, cohort, track) {
|
||||
let gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/`;
|
||||
|
||||
gradebookUrl += `?page_size=${LmsApiService.pageSize}&`;
|
||||
if (searchText) {
|
||||
gradebookUrl += `username_contains=${searchText}&`;
|
||||
}
|
||||
if (cohort) {
|
||||
gradebookUrl += `cohort_id=${cohort}&`;
|
||||
}
|
||||
if (track) {
|
||||
gradebookUrl += `enrollment_mode=${track}`;
|
||||
}
|
||||
return apiClient.get(gradebookUrl);
|
||||
}
|
||||
|
||||
static updateGradebookData(courseId, updateData) {
|
||||
/*
|
||||
updateData is expected to be a list of objects with the keys 'user_id' (an integer),
|
||||
'usage_id' (a string) and 'grade', which is an object with the keys:
|
||||
'earned_all_override', 'possible_all_override', 'earned_graded_override', and 'possible_graded_override',
|
||||
each of which should be an integer.
|
||||
Example:
|
||||
[
|
||||
{
|
||||
"user_id": 9,
|
||||
"usage_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
|
||||
"grade": {
|
||||
"earned_all_override": 11,
|
||||
"possible_all_override": 11,
|
||||
"earned_graded_override": 11,
|
||||
"possible_graded_override": 11
|
||||
}
|
||||
}
|
||||
]
|
||||
*/
|
||||
const gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/bulk-update`;
|
||||
return apiClient.post(gradebookUrl, updateData);
|
||||
}
|
||||
|
||||
static fetchTracks(courseId) {
|
||||
const trackUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/course/${courseId}`;
|
||||
return apiClient.get(trackUrl);
|
||||
}
|
||||
|
||||
static fetchCohorts(courseId) {
|
||||
const cohortsUrl = `${LmsApiService.baseUrl}/courses/${courseId}/cohorts/`;
|
||||
return apiClient.get(cohortsUrl);
|
||||
}
|
||||
|
||||
static fetchAssignmentTypes(courseId) {
|
||||
const assignmentTypesUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`;
|
||||
return apiClient.get(assignmentTypesUrl);
|
||||
}
|
||||
}
|
||||
|
||||
export default LmsApiService;
|
||||
@@ -3,12 +3,15 @@ import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import apiClient from './apiClient';
|
||||
import reducers from './reducers';
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
const initialState = apiClient.getAuthenticationState();
|
||||
|
||||
const store = createStore(
|
||||
reducers,
|
||||
initialState,
|
||||
composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware)),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
import 'babel-polyfill';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { fetchUserProfile } from '@edx/frontend-auth';
|
||||
|
||||
import apiClient from './data/apiClient';
|
||||
import Footer from './components/Gradebook/footer';
|
||||
import GradebookPage from './containers/GradebookPage';
|
||||
import Header from './containers/Header';
|
||||
import store from './data/store';
|
||||
import './App.scss';
|
||||
|
||||
const App = () => (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<header>
|
||||
<nav>
|
||||
<ul className="nav">
|
||||
<li className="nav-item"><Link className="nav-link" to="/">Home</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path="/" component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
class App extends React.Component {
|
||||
componentDidMount() {
|
||||
const username = store.getState().authentication.username;
|
||||
store.dispatch(fetchUserProfile(apiClient, username));
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
render() {
|
||||
return <Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path="/:courseId" component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>;
|
||||
}
|
||||
}
|
||||
|
||||
if (apiClient.ensurePublicOrAuthencationAndCookies(window.location.pathname)) {
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
}
|
||||
|
||||
7
src/postcss.config.js
Normal file
7
src/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/* I'm here to allow autoprefixing in webpack.prod.config.js */
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')({ grid: true, browsers: ['>1%'] }),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -4,3 +4,7 @@ import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
// These configuration values are usually set in webpack's EnvironmentPlugin however
|
||||
// Jest does not use webpack so we need to set these so for testing
|
||||
process.env.LMS_BASE_URL = 'http://localhost:18000';
|
||||
|
||||
Reference in New Issue
Block a user