diff --git a/.env b/.env
index b266d9a..ec26f91 100644
--- a/.env
+++ b/.env
@@ -1,4 +1,5 @@
NODE_ENV='production',
+NODE_PATH=./src
BASE_URL=null,
LMS_BASE_URL=null,
LOGIN_URL=null,
diff --git a/.gitignore b/.gitignore
index 3f1af3a..0926f27 100755
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ dist/
### Emacs ###
*~
+*.swo
+*.swp
diff --git a/package-lock.json b/package-lock.json
index 56e074d..8126047 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5294,7 +5294,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"got": {
"version": "8.3.2",
@@ -5372,6 +5373,7 @@
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz",
"integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==",
"dev": true,
+ "optional": true,
"requires": {
"p-finally": "^1.0.0"
}
@@ -5413,9 +5415,10 @@
},
"bl": {
"version": "1.2.2",
- "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
"integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
"dev": true,
+ "optional": true,
"requires": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
@@ -5743,6 +5746,7 @@
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
"integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
"dev": true,
+ "optional": true,
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
@@ -5753,6 +5757,7 @@
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"dev": true,
+ "optional": true,
"requires": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
@@ -5762,19 +5767,22 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"buffer-from": {
"version": "1.1.1",
@@ -6111,6 +6119,7 @@
"resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz",
"integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==",
"dev": true,
+ "optional": true,
"requires": {
"get-proxy": "^2.0.0",
"isurl": "^1.0.0-alpha5",
@@ -6745,6 +6754,7 @@
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz",
"integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==",
"dev": true,
+ "optional": true,
"requires": {
"ini": "^1.3.4",
"proto-list": "~1.2.1"
@@ -7813,6 +7823,7 @@
"resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz",
"integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==",
"dev": true,
+ "optional": true,
"requires": {
"decompress-tar": "^4.0.0",
"decompress-tarbz2": "^4.0.0",
@@ -7829,6 +7840,7 @@
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"dev": true,
+ "optional": true,
"requires": {
"pify": "^3.0.0"
},
@@ -7837,7 +7849,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -7845,7 +7858,8 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -7863,6 +7877,7 @@
"resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz",
"integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==",
"dev": true,
+ "optional": true,
"requires": {
"file-type": "^5.2.0",
"is-stream": "^1.1.0",
@@ -7873,7 +7888,8 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz",
"integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -7882,6 +7898,7 @@
"resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz",
"integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==",
"dev": true,
+ "optional": true,
"requires": {
"decompress-tar": "^4.1.0",
"file-type": "^6.1.0",
@@ -7894,7 +7911,8 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz",
"integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -7903,6 +7921,7 @@
"resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz",
"integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==",
"dev": true,
+ "optional": true,
"requires": {
"decompress-tar": "^4.1.1",
"file-type": "^5.2.0",
@@ -7913,7 +7932,8 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz",
"integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -7922,6 +7942,7 @@
"resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz",
"integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=",
"dev": true,
+ "optional": true,
"requires": {
"file-type": "^3.8.0",
"get-stream": "^2.2.0",
@@ -7933,13 +7954,15 @@
"version": "3.9.0",
"resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
"integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"get-stream": {
"version": "2.3.1",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz",
"integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=",
"dev": true,
+ "optional": true,
"requires": {
"object-assign": "^4.0.1",
"pinkie-promise": "^2.0.0"
@@ -7949,7 +7972,8 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -8398,7 +8422,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -9596,6 +9621,7 @@
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
"integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==",
"dev": true,
+ "optional": true,
"requires": {
"mime-db": "^1.28.0"
}
@@ -9605,6 +9631,7 @@
"resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz",
"integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==",
"dev": true,
+ "optional": true,
"requires": {
"ext-list": "^2.0.0",
"sort-keys-length": "^1.0.0"
@@ -9839,6 +9866,7 @@
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
"dev": true,
+ "optional": true,
"requires": {
"pend": "~1.2.0"
}
@@ -9919,13 +9947,15 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
"integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"filenamify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz",
"integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==",
"dev": true,
+ "optional": true,
"requires": {
"filename-reserved-regex": "^2.0.0",
"strip-outer": "^1.0.0",
@@ -10190,7 +10220,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"fs-extra": {
"version": "9.0.1",
@@ -10353,6 +10384,7 @@
"resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz",
"integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==",
"dev": true,
+ "optional": true,
"requires": {
"npm-conf": "^1.1.0"
}
@@ -10660,7 +10692,8 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz",
"integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"has-symbols": {
"version": "1.0.1",
@@ -10672,6 +10705,7 @@
"resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz",
"integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==",
"dev": true,
+ "optional": true,
"requires": {
"has-symbol-support-x": "^1.4.1"
}
@@ -12149,7 +12183,8 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz",
"integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"is-npm": {
"version": "1.0.0",
@@ -12199,7 +12234,8 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz",
"integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"is-path-cwd": {
"version": "2.2.0",
@@ -12510,6 +12546,7 @@
"resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz",
"integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==",
"dev": true,
+ "optional": true,
"requires": {
"has-to-string-tag-x": "^1.2.0",
"is-object": "^1.0.1"
@@ -19629,6 +19666,7 @@
"resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz",
"integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==",
"dev": true,
+ "optional": true,
"requires": {
"config-chain": "^1.1.11",
"pify": "^3.0.0"
@@ -19638,7 +19676,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -20147,6 +20186,7 @@
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz",
"integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=",
"dev": true,
+ "optional": true,
"requires": {
"p-finally": "^1.0.0"
}
@@ -20376,7 +20416,8 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"performance-now": {
"version": "2.1.0",
@@ -21472,7 +21513,8 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"proxy-addr": {
"version": "2.0.6",
@@ -23485,6 +23527,7 @@
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz",
"integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==",
"dev": true,
+ "optional": true,
"requires": {
"commander": "^2.8.1"
},
@@ -23493,7 +23536,8 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
@@ -24280,6 +24324,7 @@
"resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
"integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=",
"dev": true,
+ "optional": true,
"requires": {
"sort-keys": "^1.0.0"
}
@@ -24847,6 +24892,7 @@
"resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz",
"integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==",
"dev": true,
+ "optional": true,
"requires": {
"is-natural-number": "^4.0.1"
}
@@ -24882,6 +24928,7 @@
"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
"integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
"dev": true,
+ "optional": true,
"requires": {
"escape-string-regexp": "^1.0.2"
}
@@ -25303,6 +25350,7 @@
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
"integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
"dev": true,
+ "optional": true,
"requires": {
"bl": "^1.0.0",
"buffer-alloc": "^1.2.0",
@@ -25337,6 +25385,7 @@
"resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz",
"integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=",
"dev": true,
+ "optional": true,
"requires": {
"temp-dir": "^1.0.0",
"uuid": "^3.0.1"
@@ -25576,7 +25625,8 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==",
- "dev": true
+ "dev": true,
+ "optional": true
},
"to-fast-properties": {
"version": "2.0.0",
@@ -25989,6 +26039,7 @@
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
"integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=",
"dev": true,
+ "optional": true,
"requires": {
"escape-string-regexp": "^1.0.2"
}
@@ -26102,6 +26153,7 @@
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
"dev": true,
+ "optional": true,
"requires": {
"buffer": "^5.2.1",
"through": "^2.3.8"
@@ -26395,7 +26447,8 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
"integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"urlgrey": {
"version": "0.4.4",
@@ -27766,6 +27819,7 @@
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
"dev": true,
+ "optional": true,
"requires": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
diff --git a/src/components/Gradebook/Assignments.jsx b/src/components/Gradebook/Assignments.jsx
new file mode 100644
index 0000000..c1df432
--- /dev/null
+++ b/src/components/Gradebook/Assignments.jsx
@@ -0,0 +1,206 @@
+/* eslint-disable react/sort-comp, react/button-has-type */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import {
+ Button,
+ Collapsible,
+ InputSelect,
+ InputText,
+} from '@edx/paragon';
+import queryString from 'query-string';
+
+import { selectableAssignmentLabels } from '../../data/selectors/filters';
+import {
+ filterAssignmentType,
+ fetchGrades,
+ updateGradesIfAssignmentGradeFiltersSet,
+} from '../../data/actions/grades';
+import {
+ updateAssignmentFilter,
+ updateAssignmentLimits,
+} from '../../data/actions/filters';
+
+
+const DECIMAL_PRECISION = 2;
+const GRADE_OVERRIDE_HISTORY_COLUMNS = [{ label: 'Date', key: 'date' }, { label: 'Grader', key: 'grader' },
+ { label: 'Reason', key: 'reason' },
+ { label: 'Adjusted grade', key: 'adjustedGrade' }];
+
+export class Assignments extends React.Component {
+ getAssignmentFilterOptions = () => [
+ { label: 'All', value: '' },
+ ...this.props.assignmentFilterOptions.map(({ label, subsectionLabel }) => ({
+ label: `${label}: ${subsectionLabel}`,
+ value: label,
+ })),
+ ];
+
+ handleAssignmentFilterChange = (assignment) => {
+ const selectedFilterOption = this.props.assignmentFilterOptions.find(assig => assig.label === assignment);
+ const { type, id } = selectedFilterOption || {};
+ const typedValue = { label: assignment, type, id };
+ this.props.updateAssignmentFilter(typedValue);
+ this.updateQueryParams({ assignment: id });
+ this.props.updateGradesIfAssignmentGradeFiltersSet(
+ this.props.courseId,
+ this.props.selectedCohort,
+ this.props.selectedTrack,
+ this.props.selectedAssignmentType,
+ );
+ };
+
+ handleSubmitAssignmentGrade = (event) => {
+ event.preventDefault();
+ const {
+ assignmentGradeMin,
+ assignmentGradeMax,
+ } = this.props;
+
+ this.props.updateAssignmentLimits(assignmentGradeMin, assignmentGradeMax);
+ this.props.getUserGrades(
+ this.props.courseId,
+ this.props.selectedCohort,
+ this.props.selectedTrack,
+ this.props.selectedAssignmentType,
+ );
+ this.props.updateQueryParams({ assignmentGradeMin, assignmentGradeMax });
+ };
+
+ mapAssignmentTypeEntries = (entries) => {
+ const mapped = [
+ { id: 0, label: 'All', value: '' },
+ ...entries.map(entry => ({ id: entry, label: entry })),
+ ];
+ return mapped;
+ };
+
+ updateAssignmentTypes = (assignmentType) => {
+ this.props.filterAssignmentType(assignmentType);
+ this.updateQueryParams({ assignmentType });
+ }
+
+
+ render() {
+ return (
+
+
+
+
+ Assignment Types:
+
+
+
+
+
+ Assignment:
+
+
+
+
Grade Range (0% - 100%)
+
+
+
+ );
+ }
+}
+
+Assignments.defaultProps = {
+ assignmentTypes: [],
+ assignmentFilterOptions: [],
+ selectedAssignment: '',
+ selectedAssignmentType: '',
+}
+
+Assignments.propTypes = {
+ assignmentGradeMin: PropTypes.string.isRequired,
+ assignmentGradeMax: PropTypes.string.isRequired,
+ courseId: PropTypes.string.isRequired,
+ setAssignmentGradeMin: PropTypes.func.isRequired,
+ setAssignmentGradeMax: PropTypes.func.isRequired,
+ updateQueryParams: PropTypes.func.isRequired,
+
+ // redux
+ assignmentTypes: PropTypes.arrayOf(PropTypes.string),
+ assignmentFilterOptions: PropTypes.arrayOf(PropTypes.shape({
+ label: PropTypes.string,
+ subsectionLabel: PropTypes.string,
+ })),
+ filterAssignmentType: PropTypes.func.isRequired,
+ selectedAssignmentType: PropTypes.string,
+ selectedAssignment: PropTypes.string,
+ selectedCohort: PropTypes.string,
+ selectedTrack: PropTypes.string,
+ updateGradesIfAssignmentGradeFiltersSet: PropTypes.func.isRequired,
+ updateAssignmentFilter: PropTypes.func.isRequired,
+ updateAssignmentLimits: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = (state) => ({
+ assignmentTypes: state.assignmentTypes.results,
+ assignmentFilterOptions: selectableAssignmentLabels(state),
+ selectedAssignment: (state.filters.assignment || {}).label,
+ selectedAssignmentTypes: state.filters.assignmentType,
+ selectedCohort: state.filters.cohort,
+ selectedTrack: state.filters.track,
+});
+
+export const mapDispatchToProps = {
+ fetchGrades,
+ filterAssignmentType,
+ updateAssignmentFilter,
+ updateAssignmentLimits,
+ updateGradesIfAssignmentGradeFiltersSet,
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Assignments);
diff --git a/src/components/Gradebook/BulkManagement.jsx b/src/components/Gradebook/BulkManagement.jsx
new file mode 100644
index 0000000..76af794
--- /dev/null
+++ b/src/components/Gradebook/BulkManagement.jsx
@@ -0,0 +1,200 @@
+/* eslint-disable react/sort-comp, react/button-has-type */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import {
+ Button,
+ StatusAlert,
+ Table,
+ Tab,
+} from '@edx/paragon';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faDownload } from '@fortawesome/free-solid-svg-icons';
+import { configuration } from '../../config';
+
+import { submitFileUploadFormData } from '../../data/actions/grades';
+import { getBulkManagementHistory } from '../../data/selectors/grades';
+
+export class BulkManagement extends React.Component {
+ constructor(props) {
+ super(props);
+ this.fileFormRef = React.createRef();
+ this.fileInputRef = React.createRef();
+ }
+
+ formatHistoryRow = (row) => {
+ const {
+ summaryOfRowsProcessed: {
+ total,
+ successfullyProcessed,
+ failed,
+ skipped,
+ },
+ unique_id: courseId,
+ originalFilename,
+ id,
+ user: username,
+ ...rest
+ } = row;
+ const resultsText = [
+ `${total} Students: ${successfullyProcessed} processed`,
+ ...(skipped > 0 ? [`${skipped} skipped`] : []),
+ ...(failed > 0 ? [`${failed} failed`] : []),
+ ].join(', ');
+ const resultsSummary = (
+
+
+ {resultsText}
+
+ );
+ const createWrappedCell = (text) => ({text});
+ const filename = createWrappedCell(originalFilename);
+ const user = createWrappedCell(username);
+ return {
+ resultsSummary,
+ filename,
+ user,
+ ...rest,
+ };
+ };
+
+ handleClickImportGrades = () => {
+ const fileInput = this.fileInputRef.current;
+ if (fileInput) {
+ fileInput.click();
+ }
+ };
+
+ handleFileInputChange = (event) => {
+ const fileInput = event.target;
+ const file = fileInput.files[0];
+ const form = this.fileFormRef.current;
+ if (file && form) {
+ const formData = new FormData(form);
+ this.props.submitFileUploadFormData(this.props.courseId, formData).then(() => {
+ fileInput.value = null;
+ });
+ }
+ };
+
+
+ render() {
+ return (
+
+ Use this feature by downloading a CSV for bulk management,
+ overriding grades locally, and coming back here to upload.
+
+
+
+
+ Results appear in the table below.
+ Grade processing may take a few seconds.
+
+
+
+ );
+ }
+}
+
+BulkManagement.defaultProps = {
+ bulkImportError: '',
+ bulkManagementHistory: [],
+ uploadSuccess: false,
+};
+
+BulkManagement.propTypes = {
+ courseId: PropTypes.string,
+ gradeExportUrl: PropTypes.string.isRequired,
+
+ // redux
+ bulkImportError: PropTypes.string,
+ bulkManagementHistory: PropTypes.arrayOf(PropTypes.shape({
+ originalFilename: PropTypes.string.isRequired,
+ user: PropTypes.string.isRequired,
+ timeUploaded: PropTypes.string.isRequired,
+ summaryOfRowsProcessed: PropTypes.shape({
+ total: PropTypes.number.isRequired,
+ successfullyProcessed: PropTypes.number.isRequired,
+ failed: PropTypes.number.isRequired,
+ skipped: PropTypes.number.isRequired,
+ }).isRequired,
+ })),
+ submitFileUploadFormData: PropTypes.func.isRequired,
+ uploadSuccess: PropTypes.bool,
+};
+
+export const mapStateToProps = (state) => ({
+ bulkImportError: state.grades.bulkManagement
+ && state.grades.bulkManagement.errorMessages
+ ? `Errors while processing: ${state.grades.bulkManagement.errorMessages.join(', ')}`
+ : '',
+ bulkManagementHistory: getBulkManagementHistory(state),
+ uploadSuccess: !!(state.grades.bulkManagement && state.grades.bulkManagement.uploadSuccess),
+});
+
+export const mapDispatchToProps = {
+ submitFileUploadFormData,
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(BulkManagement);
diff --git a/src/components/Gradebook/BulkManagementControls.jsx b/src/components/Gradebook/BulkManagementControls.jsx
new file mode 100644
index 0000000..3c800f8
--- /dev/null
+++ b/src/components/Gradebook/BulkManagementControls.jsx
@@ -0,0 +1,87 @@
+/* eslint-disable react/sort-comp, react/button-has-type */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import { StatefulButton } from '@edx/paragon';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faDownload, faSpinner } from '@fortawesome/free-solid-svg-icons';
+
+import {
+ downloadBulkGradesReport,
+ downloadInterventionReport
+} from '../../data/actions/grades';
+
+
+
+export class BulkManagementControls extends React.Component {
+ handleClickDownloadInterventions = () => {
+ this.props.downloadInterventionReport(this.props.courseId);
+ window.location = this.props.interventionExportUrl;
+ };
+
+ // At present, we don't store label and value in google analytics. By setting the label
+ // property of the below events, I want to verify that we can set the label of google anlatyics
+ // The following properties of a google analytics event are:
+ // category (used), name(used), lavel(not used), value(not used)
+ handleClickExportGrades = () => {
+ this.props.downloadBulkGradesReport(this.props.courseId);
+ window.location = this.props.gradeExportUrl;
+ };
+
+ render() {
+ return (
+
+ ,
+ pending: ,
+ }}
+ disabledStates={['pending']}
+ />
+ ,
+ pending: ,
+ }}
+ disabledStates={['pending']}
+ />
+
+ );
+ }
+}
+
+BulkManagementControls.propTypes = {
+ courseId: PropTypes.string,
+ gradeExportUrl: PropTypes.string.isRequired,
+ interventionExportUrl: PropTypes.string.isRequired,
+ showSpinner: PropTypes.bool,
+
+ // redux
+ downloadBulkGradesReport: PropTypes.func.isRequired,
+ downloadInterventionReport: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = (state) => ({ });
+
+export const mapDispatchToProps = {
+ downloadBulkGradesReport,
+ downloadInterventionReport,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);
diff --git a/src/components/Gradebook/EditModal.jsx b/src/components/Gradebook/EditModal.jsx
new file mode 100644
index 0000000..9b80480
--- /dev/null
+++ b/src/components/Gradebook/EditModal.jsx
@@ -0,0 +1,214 @@
+/* eslint-disable react/sort-comp, react/button-has-type */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import {
+ Button,
+ Modal,
+ StatusAlert,
+ Table,
+} from '@edx/paragon';
+
+import {
+ doneViewingAssignment,
+ updateGrades
+} from '../../data/actions/grades';
+
+const GRADE_OVERRIDE_HISTORY_COLUMNS = [{ label: 'Date', key: 'date' }, { label: 'Grader', key: 'grader' },
+ { label: 'Reason', key: 'reason' },
+ { label: 'Adjusted grade', key: 'adjustedGrade' }];
+
+export class EditModal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.overrideReasonInput = React.createRef();
+ }
+
+ componentDidMount() {
+ this.overrideReasonInput.current.focus();
+ }
+
+ handleAdjustedGradeClick = () => {
+ this.props.updateGrades(
+ this.props.courseId, [
+ {
+ user_id: this.props.updateUserId,
+ usage_id: this.props.updateModuleId,
+ grade: {
+ earned_graded_override: this.props.adjustedGradeValue,
+ comment: this.props.reasonForChange,
+ },
+ },
+ ],
+ this.props.filterValue,
+ this.props.selectedCohort,
+ this.props.selectedTrack,
+ );
+
+ this.closeAssignmentModal();
+ }
+
+ closeAssignmentModal = () => {
+ this.props.doneViewingAssignment();
+ this.props.setGradebookState({
+ adjustedGradePossible: '',
+ adjustedGradeValue: '',
+ modalOpen: false,
+ reasonForChange: '',
+ updateModuleId: null,
+ updateUserId: null,
+ });
+ };
+
+ render() {
+ return (
+
+
+
Assignment:
+
{this.props.assignmentName}
+
Student:
+
{this.props.updateUserName}
+
Original Grade:
+
{this.props.gradeOriginalEarnedGraded}
+
Current Grade:
+
{this.props.gradeOverrideCurrentEarnedGradedOverride}
+
+
+ {!this.props.gradeOverrideHistoryError && (
+ ),
+ adjustedGrade: (
+
+
+ {(this.props.adjustedGradePossible || this.props.gradeOriginalPossibleGraded) && ' / '}
+ {this.props.adjustedGradePossible || this.props.gradeOriginalPossibleGraded}
+ ),
+ }]}
+ />
+ )}
+
+ Showing most recent actions (max 5). To see more, please contact
+ support.
+
+ Note: Once you save, your changes will be visible to students.
+
+ )}
+ buttons={[
+ ,
+ ]}
+ onClose={this.closeAssignmentModal}
+ />
+ );
+ }
+}
+
+EditModal.defaultProps = {
+
+ areGradesFrozen: false,
+ assignmentTypes: [],
+ assignmentFilterOptions: [],
+ canUserViewGradebook: false,
+ cohorts: [],
+ gradeOverrides: [],
+ gradeOverrideCurrentEarnedGradedOverride: null,
+ gradeOriginalEarnedGraded: null,
+ gradeOriginalPossibleGraded: null,
+ location: {
+ search: '',
+ },
+ courseId: '',
+ selectedCohort: null,
+ selectedTrack: null,
+ selectedAssignmentType: '',
+ selectedAssignment: '',
+ showSpinner: false,
+ tracks: [],
+ bulkImportError: '',
+ uploadSuccess: false,
+ showBulkManagement: false,
+ bulkManagementHistory: [],
+ gradeOverrideHistoryError: '',
+ totalUsersCount: null,
+ filteredUsersCount: null,
+};
+
+EditModal.propTypes = {
+
+ assignmentName: PropTypes.string,
+ adjustedGradePossible: PropTypes.string,
+ adjustedGradeValue: PropTypes.number,
+ courseId: PropTypes.string,
+ filterValue: PropTypes.string,
+ open: PropTypes.bool,
+ reasonForChange: PropTypes.string,
+ setAdjustedGradeValue: PropTypes.func,
+ setGradebookState: PropTypes.func,
+ setReasonForChange: PropTypes.func,
+ todaysDate: PropTypes.string,
+ updateModuleId: PropTypes.string,
+ updateUserId: PropTypes.string,
+ updateUserName: PropTypes.string,
+
+ // redux
+ doneViewingAssignment: PropTypes.func.isRequired,
+ gradeOverrideCurrentEarnedGradedOverride: PropTypes.number,
+ gradeOverrideHistoryError: PropTypes.string,
+ gradeOverrides: PropTypes.arrayOf(PropTypes.shape({
+ date: PropTypes.string,
+ grader: PropTypes.string,
+ reason: PropTypes.string,
+ adjustedGrade: PropTypes.number,
+ })),
+ gradeOriginalEarnedGraded: PropTypes.number,
+ gradeOriginalPossibleGraded: PropTypes.number,
+ selectedCohort: PropTypes.string,
+ selectedTrack: PropTypes.string,
+ updateGrades: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = (state) => ({
+ gradeOverrides: state.grades.gradeOverrideHistoryResults,
+ gradeOverrideCurrentEarnedGradedOverride: state.grades.gradeOverrideCurrentEarnedGradedOverride,
+ gradeOverrideHistoryError: state.grades.gradeOverrideHistoryError,
+ gradeOriginalEarnedGraded: state.grades.gradeOriginalEarnedGraded,
+ grdaeOriginalPossibleGraded: state.grades.grdaeOriginalPossibleGraded,
+ selectedCohort: state.filters.cohort,
+ selectedTrack: state.filters.track,
+})
+
+export const mapDispatchToProps = {
+ doneViewingAssignment,
+ updateGrades,
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(EditModal);
diff --git a/src/components/Gradebook/GradebookTable.jsx b/src/components/Gradebook/GradebookTable.jsx
new file mode 100644
index 0000000..9c047c7
--- /dev/null
+++ b/src/components/Gradebook/GradebookTable.jsx
@@ -0,0 +1,224 @@
+/* eslint-disable react/sort-comp, react/button-has-type */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import { Table } from '@edx/paragon';
+
+import { formatDateForDisplay } from '../../data/actions/utils';
+import { getHeadings } from '../../data/selectors/grades';
+import { fetchGradeOverrideHistory } from '../../data/actions/grades';
+const DECIMAL_PRECISION = 2;
+
+export class GradebookTable extends React.Component {
+ setNewModalState = (userEntry, subsection) => {
+ this.props.fetchGradeOverrideHistory(
+ subsection.module_id,
+ userEntry.user_id,
+ );
+
+ let adjustedGradePossible = '';
+ if (subsection.attempted) {
+ adjustedGradePossible = subsection.score_possible;
+ }
+
+ this.props.setGradebookState({
+ adjustedGradePossible,
+ adjustedGradeValue: '',
+ modalAssignmentName: `${subsection.subsection_name}`,
+ modalOpen: true,
+ reasonForChange: '',
+ todaysDate: formatDateForDisplay(new Date()),
+ updateModuleId: subsection.module_id,
+ updateUserId: userEntry.user_id,
+ updateUserName: userEntry.username,
+ });
+ }
+
+
+ getLearnerInformation = entry => (
+
+
{entry.username}
+ {entry.external_user_key &&
{entry.external_user_key}
}
+
+ )
+
+ roundGrade = percent => parseFloat((percent || 0).toFixed(DECIMAL_PRECISION));
+
+ formatter = {
+ percent: (entries, areGradesFrozen) => entries.map((entry) => {
+ const learnerInformation = this.getLearnerInformation(entry);
+ const results = {
+ Username: (
+ {learnerInformation}
+ ),
+ Email: (
+ {entry.email}
+ ),
+ };
+
+ const assignments = entry.section_breakdown
+ .reduce((acc, subsection) => {
+ if (areGradesFrozen) {
+ acc[subsection.label] = `${this.roundGrade(subsection.percent * 100)} %`;
+ } else {
+ acc[subsection.label] = (
+
+ );
+ }
+ return acc;
+ }, {});
+ const totals = { Total: `${this.roundGrade(entry.percent * 100)}%` };
+ return Object.assign(results, assignments, totals);
+ }),
+
+ absolute: (entries, areGradesFrozen) => entries.map((entry) => {
+ const learnerInformation = this.getLearnerInformation(entry);
+ const results = {
+ Username: (
+ {learnerInformation}
+ ),
+ Email: (
+ {entry.email}
+ ),
+ };
+
+ const assignments = entry.section_breakdown
+ .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] = (
+
+ );
+ }
+ return acc;
+ }, {});
+
+ const totals = { Total: `${this.roundGrade(entry.percent * 100)}/100` };
+ return Object.assign(results, assignments, totals);
+ }),
+ };
+
+ formatHeadings = () => {
+ let headings = [...this.props.headings];
+
+ if (headings.length > 0) {
+ const userInformationHeadingLabel = (
+
+
Username
+
Student Key*
+
+ );
+ const emailHeadingLabel = 'Email*';
+
+ headings = headings.map(heading => ({ label: heading, key: heading, width: 'col-1' }));
+
+ // replace username heading label to include additional user data
+ headings[0].label = userInformationHeadingLabel;
+ headings[0].width = 'col-2';
+ headings[1].label = emailHeadingLabel;
+ headings[1].width = 'col-2';
+ }
+
+ return headings;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+GradebookTable.defaultProps = {
+ areGradesFrozen: false,
+ grades: [],
+};
+
+GradebookTable.propTypes = {
+ setGradebookState: PropTypes.func.isRequired,
+ // redux
+ areGradesFrozen: PropTypes.bool.isRequired,
+ format: PropTypes.string.isRequired,
+ grades: PropTypes.arrayOf(PropTypes.shape({
+ percent: PropTypes.number,
+ section_breakdown: PropTypes.arrayOf(PropTypes.shape({
+ attempted: PropTypes.bool,
+ category: PropTypes.string,
+ label: PropTypes.string,
+ module_id: PropTypes.string,
+ percent: PropTypes.number,
+ scoreEarned: PropTypes.number,
+ scorePossible: PropTypes.number,
+ subsection_name: PropTypes.string,
+ })),
+ user_id: PropTypes.number,
+ user_name: PropTypes.string,
+ })),
+ headings: PropTypes.arrayOf(PropTypes.string).isRequired,
+ fetchGradeOverrideHistory: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = (state) => ({
+ areGradesFrozen: state.assignmentTypes.areGradesFrozen,
+ format: state.grades.gradeFormat,
+ grades: state.grades.results,
+ headings: getHeadings(state),
+});
+
+export const mapDispatchToProps = {
+ fetchGradeOverrideHistory,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(GradebookTable);
diff --git a/src/components/Gradebook/index.jsx b/src/components/Gradebook/index.jsx
index 7feaed6..8f44741 100644
--- a/src/components/Gradebook/index.jsx
+++ b/src/components/Gradebook/index.jsx
@@ -25,30 +25,33 @@ import { formatDateForDisplay } from '../../data/actions/utils';
import initialFilters from '../../data/constants/filters';
import ConnectedFilterBadges from '../FilterBadges';
-const DECIMAL_PRECISION = 2;
-const GRADE_OVERRIDE_HISTORY_COLUMNS = [{ label: 'Date', key: 'date' }, { label: 'Grader', key: 'grader' },
- { label: 'Reason', key: 'reason' },
- { label: 'Adjusted grade', key: 'adjustedGrade' }];
+import Assignments from './Assignments';
+import BulkManagement from './BulkManagement';
+import BulkManagementControls from './BulkManagementControls';
+import EditModal from './EditModal';
+import GradebookTable from './GradebookTable';
export default class Gradebook extends React.Component {
constructor(props) {
super(props);
this.state = {
- filterValue: '',
- courseGradeMin: '0',
- courseGradeMax: '100',
- modalOpen: false,
+ adjustedGradePossible: '',
adjustedGradeValue: 0,
- updateModuleId: null,
- updateUserId: null,
- reasonForChange: '',
assignmentGradeMin: '0',
assignmentGradeMax: '100',
+ assignmentName: '',
+ courseGradeMin: '0',
+ courseGradeMax: '100',
+ filterValue: '',
isMinCourseGradeFilterValid: true,
isMaxCourseGradeFilterValid: true,
+ modalAssignmentName: '',
+ modalOpen: false,
+ reasonForChange: '',
+ todaysDate: '',
+ updateModuleId: null,
+ updateUserId: null,
};
- this.fileFormRef = React.createRef();
- this.fileInputRef = React.createRef();
this.myRef = React.createRef();
}
@@ -56,7 +59,6 @@ export default class Gradebook extends React.Component {
const urlQuery = queryString.parse(this.props.location.search);
this.props.initializeFilters(urlQuery);
this.props.getRoles(this.props.courseId);
- this.overrideReasonInput.focus();
const newStateFields = {};
['assignmentGradeMin', 'assignmentGradeMax', 'courseGradeMin', 'courseGradeMax'].forEach((attr) => {
@@ -72,37 +74,6 @@ export default class Gradebook extends React.Component {
this.setState({ [e.target.name]: e.target.value });
}
- setNewModalState = (userEntry, subsection) => {
- this.props.fetchGradeOverrideHistory(
- subsection.module_id,
- userEntry.user_id,
- );
-
- let adjustedGradePossible = '';
- if (subsection.attempted) {
- adjustedGradePossible = subsection.score_possible;
- }
-
- this.setState({
- modalAssignmentName: `${subsection.subsection_name}`,
- modalOpen: true,
- updateModuleId: subsection.module_id,
- updateUserId: userEntry.user_id,
- updateUserName: userEntry.username,
- todaysDate: formatDateForDisplay(new Date()),
- adjustedGradePossible,
- reasonForChange: '',
- adjustedGradeValue: '',
- });
- }
-
- getLearnerInformation = entry => (
-
-
{entry.username}
- {entry.external_user_key &&
{entry.external_user_key}
}
-
- )
-
getActiveTabs = () => {
if (this.props.showBulkManagement) {
return ['Grades', 'Bulk Management'];
@@ -110,17 +81,6 @@ export default class Gradebook extends React.Component {
return ['Grades'];
};
- getAssignmentFilterOptions = () => [
- { label: 'All', value: '' },
- ...this.props.assignmentFilterOptions.map((assignment) => {
- const { label, subsectionLabel } = assignment;
- return {
- label: `${label}: ${subsectionLabel}`,
- value: label,
- };
- }),
- ];
-
getCourseGradeFilterAlertDialog = () => {
let dialog = '';
@@ -133,52 +93,6 @@ export default class Gradebook extends React.Component {
return dialog;
};
- handleAdjustedGradeClick = () => {
- this.props.updateGrades(
- this.props.courseId, [
- {
- user_id: this.state.updateUserId,
- usage_id: this.state.updateModuleId,
- grade: {
- earned_graded_override: this.state.adjustedGradeValue,
- comment: this.state.reasonForChange,
- },
- },
- ],
- this.state.filterValue,
- this.props.selectedCohort,
- this.props.selectedTrack,
- );
-
- this.closeAssignmentModal();
- }
-
- closeAssignmentModal = () => {
- this.props.doneViewingAssignment();
- this.setState({
- adjustedGradePossible: '',
- adjustedGradeValue: '',
- modalOpen: false,
- reasonForChange: '',
- updateModuleId: null,
- updateUserId: null,
- });
- };
-
- handleAssignmentFilterChange = (assignment) => {
- const selectedFilterOption = this.props.assignmentFilterOptions.find(assig => assig.label === assignment);
- const { type, id } = selectedFilterOption || {};
- const typedValue = { label: assignment, type, id };
- this.props.updateAssignmentFilter(typedValue);
- this.updateQueryParams({ assignment: id });
- this.props.updateGradesIfAssignmentGradeFiltersSet(
- this.props.courseId,
- this.props.selectedCohort,
- this.props.selectedTrack,
- this.props.selectedAssignmentType,
- );
- };
-
updateQueryParams = (queryParams) => {
const parsed = queryString.parse(this.props.location.search);
Object.keys(queryParams).forEach((key) => {
@@ -191,15 +105,6 @@ export default class Gradebook extends React.Component {
this.props.history.push(`?${queryString.stringify(parsed)}`);
};
- mapAssignmentTypeEntries = (entries) => {
- const mapped = entries.map(entry => ({
- id: entry,
- label: entry,
- }));
- mapped.unshift({ id: 0, label: 'All', value: '' });
- return mapped;
- };
-
mapCohortsEntries = (entries) => {
const mapped = entries.map(entry => ({
id: entry.id,
@@ -218,58 +123,6 @@ export default class Gradebook extends React.Component {
return mapped;
};
- formatHistoryRow = (row) => {
- const {
- summaryOfRowsProcessed: {
- total,
- successfullyProcessed,
- failed,
- skipped,
- },
- unique_id: courseId,
- originalFilename,
- id,
- user: username,
- ...rest
- } = row;
- const resultsText = [
- `${total} Students: ${successfullyProcessed} processed`,
- ...(skipped > 0 ? [`${skipped} skipped`] : []),
- ...(failed > 0 ? [`${failed} failed`] : []),
- ].join(', ');
- const resultsSummary = (
-
-
- {resultsText}
-
- );
- const filename = (
-
- {originalFilename}
-
- );
- const user = (
-
- {username}
-
- );
- return {
- resultsSummary,
- filename,
- user,
- ...rest,
- };
- };
-
- updateAssignmentTypes = (assignmentType) => {
- this.props.filterAssignmentType(assignmentType);
- this.updateQueryParams({ assignmentType });
- }
-
updateTracks = (event) => {
const selectedTrackItem = this.props.tracks.find(x => x.name === event);
let selectedTrackSlug = null;
@@ -300,60 +153,6 @@ export default class Gradebook extends React.Component {
this.updateQueryParams({ cohort: selectedCohortId });
};
- // At present, we don't store label and value in google analytics. By setting the label
- // property of the below events, I want to verify that we can set the label of google anlatyics
- // The following properties of a google analytics event are:
- // category (used), name(used), lavel(not used), value(not used)
- handleClickExportGrades = () => {
- this.props.downloadBulkGradesReport(this.props.courseId);
- window.location = this.props.gradeExportUrl;
- };
-
- handleClickDownloadInterventions = () => {
- this.props.downloadInterventionReport(this.props.courseId);
- window.location = this.props.interventionExportUrl;
- };
-
- handleClickImportGrades = () => {
- const fileInput = this.fileInputRef.current;
- if (fileInput) {
- fileInput.click();
- }
- };
-
- handleFileInputChange = (event) => {
- const fileInput = event.target;
- const file = fileInput.files[0];
- const form = this.fileFormRef.current;
- if (file && form) {
- const formData = new FormData(form);
- this.props.submitFileUploadFormData(this.props.courseId, formData).then(() => {
- fileInput.value = null;
- });
- }
- };
-
- handleSubmitAssignmentGrade = (event) => {
- event.preventDefault();
- const {
- assignmentGradeMin,
- assignmentGradeMax,
- } = this.state;
-
- this.props.updateAssignmentLimits(assignmentGradeMin, assignmentGradeMax);
- this.props.getUserGrades(
- this.props.courseId,
- this.props.selectedCohort,
- this.props.selectedTrack,
- this.props.selectedAssignmentType,
- );
- this.updateQueryParams({ assignmentGradeMin, assignmentGradeMax });
- };
-
- handleMinAssigGradeChange = assignmentGradeMin => this.setState({ assignmentGradeMin });
-
- handleMaxAssigGradeChange = assignmentGradeMax => this.setState({ assignmentGradeMax });
-
mapSelectedCohortEntry = (entry) => {
const selectedCohortEntry = this.props.cohorts.find(x => x.id === parseInt(entry, 10));
if (selectedCohortEntry) {
@@ -370,103 +169,8 @@ export default class Gradebook extends React.Component {
return 'Tracks';
};
- roundGrade = percent => parseFloat((percent || 0).toFixed(DECIMAL_PRECISION));
-
- formatter = {
- percent: (entries, areGradesFrozen) => entries.map((entry) => {
- const learnerInformation = this.getLearnerInformation(entry);
- const results = {
- Username: (
- {learnerInformation}
- ),
- Email: (
- {entry.email}
- ),
- };
-
- const assignments = entry.section_breakdown
- .reduce((acc, subsection) => {
- if (areGradesFrozen) {
- acc[subsection.label] = `${this.roundGrade(subsection.percent * 100)} %`;
- } else {
- acc[subsection.label] = (
-
- );
- }
- return acc;
- }, {});
- const totals = { Total: `${this.roundGrade(entry.percent * 100)}%` };
- return Object.assign(results, assignments, totals);
- }),
-
- absolute: (entries, areGradesFrozen) => entries.map((entry) => {
- const learnerInformation = this.getLearnerInformation(entry);
- const results = {
- Username: (
- {learnerInformation}
- ),
- Email: (
- {entry.email}
- ),
- };
-
- const assignments = entry.section_breakdown
- .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] = (
-
- );
- }
- 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`;
- formatHeadings = () => {
- let headings = [...this.props.headings];
-
- if (headings.length > 0) {
- const userInformationHeadingLabel = (
-
-
Username
-
Student Key*
-
- );
- const emailHeadingLabel = 'Email*';
-
- headings = headings.map(heading => ({ label: heading, key: heading }));
-
- // replace username heading label to include additional user data
- headings[0].label = userInformationHeadingLabel;
- headings[1].label = emailHeadingLabel;
- }
-
- return headings;
- }
-
handleCourseGradeFilterChange = (type, value) => {
const filterValue = value;
@@ -539,6 +243,26 @@ export default class Gradebook extends React.Component {
);
}
+ createStateFieldSetter = (key) => (value) => this.setState({ [key]: value });
+
+ createLimitedSetter = (...keys) => (values) => this.setState(
+ keys.reduce(
+ (obj, key) => ( values[key] === undefined ? obj : { ...obj, [key]: values[key] } ),
+ {}
+ )
+ )
+
+ safeSetState = this.createLimitedSetter(
+ 'adjustedGradePossible',
+ 'adjustedGradeValue',
+ 'modalOpen',
+ 'reasonForChange',
+ 'todaysDate',
+ 'updateModuleId',
+ 'updateUserId',
+ 'updateUserName',
+ );
+
render() {
return (
{this.props.showBulkManagement && (
-
- ,
- pending: ,
- }}
- disabledStates={['pending']}
- />
- ,
- pending: ,
- }}
- disabledStates={['pending']}
- />
-
+
)}
-
+
{PageButtons(this.props)}
* available for learners in the Master's track only
-
-
-
Assignment:
{this.state.modalAssignmentName}
-
Student:
{this.state.updateUserName}
-
Original Grade:
{this.props.gradeOriginalEarnedGraded}
-
Current Grade:
{this.props.gradeOverrideCurrentEarnedGradedOverride}
-
-
- {!this.props.gradeOverrideHistoryError && (
- this.onChange(value)}
- ref={(input) => { this.overrideReasonInput = input; }}
- />),
- adjustedGrade: (
-
- this.onChange(value)}
- />
- {(this.state.adjustedGradePossible
- || this.props.gradeOriginalPossibleGraded)
- && ' / '}
- {this.state.adjustedGradePossible
- || this.props.gradeOriginalPossibleGraded}
- ),
- }]}
- />
- )}
-
- Showing most recent actions (max 5). To see more, please contact
- support.
-
- Note: Once you save, your changes will be visible to students.
-
- )}
- buttons={[
- ,
- ]}
- onClose={this.closeAssignmentModal}
+ reasonForChange={this.state.reasonForChange}
+ setAdjustedGradeValue={this.createStateFieldSetter('adjustedGradeValue')}
+ setGradebookState={this.safeSetState}
+ setReasonForChange={this.createStateFieldSetter('reasonForChange')}
+ todaysDate={this.state.todaysDate}
+ updateModuleId={this.state.updateModuleId}
+ updateUserId={this.state.updateUserId}
+ updateUserName={this.state.updateUserName}
/>
+
- {this.props.showBulkManagement && (
-
- Use this feature by downloading a CSV for bulk management,
- overriding grades locally, and coming back here to upload.
-
-
-
-
- Results appear in the table below.
- Grade processing may take a few seconds.
-
-
-
- )}
+ {this.props.showBulkManagement &&
+
+ }
)}
@@ -824,73 +401,14 @@ export default class Gradebook extends React.Component {
>
)}
>
-
-
-
-
- Assignment Types:
-
-
-
-
-
- Assignment:
-
-
-
-
Grade Range (0% - 100%)
-
-
-
+
(
{
- courseId: ownProps.match.params.courseId,
- grades: state.grades.results,
- gradeOverrides: state.grades.gradeOverrideHistoryResults,
- gradeOverrideCurrentEarnedAllOverride: state.grades.gradeOverrideCurrentEarnedAllOverride,
- gradeOverrideCurrentPossibleAllOverride: state.grades.gradeOverrideCurrentPossibleAllOverride,
- gradeOverrideCurrentEarnedGradedOverride: state.grades.gradeOverrideCurrentEarnedGradedOverride,
- gradeOverrideCurrentPossibleGradedOverride:
- state.grades.gradeOverrideCurrentPossibleGradedOverride,
- gradeOriginalEarnedGraded: state.grades.gradeOriginalEarnedGraded,
- gradeOriginalPossibleGraded: state.grades.gradeOriginalPossibleGraded,
- headings: getHeadings(state),
- tracks: state.tracks.results,
- cohorts: state.cohorts.results,
- selectedTrack: state.filters.track,
- selectedCohort: state.filters.cohort,
- selectedAssignmentType: state.filters.assignmentType,
- selectedAssignment: (state.filters.assignment || {}).label,
- format: state.grades.gradeFormat,
- showSuccess: state.grades.showSuccess,
- gradeOverrideHistoryError: state.grades.overrideHistoryError,
- prevPage: state.grades.prevPage,
- nextPage: state.grades.nextPage,
+ areGradesFrozen: state.assignmentTypes.areGradesFrozen,
assignmentTypes: state.assignmentTypes.results,
assignmentFilterOptions: selectableAssignmentLabels(state),
- areGradesFrozen: state.assignmentTypes.areGradesFrozen,
- showSpinner: shouldShowSpinner(state),
+ bulkImportError: state.grades.bulkManagement
+ && state.grades.bulkManagement.errorMessages
+ ? `Errors while processing: ${state.grades.bulkManagement.errorMessages.join(', ')}`
+ : '',
+ bulkManagementHistory: getBulkManagementHistory(state),
+ cohorts: state.cohorts.results,
+ courseId: ownProps.match.params.courseId,
canUserViewGradebook: state.roles.canUserViewGradebook,
+ filteredUsersCount: state.grades.filteredUsersCount,
+ format: state.grades.gradeFormat,
gradeExportUrl: LmsApiService.getGradeExportCsvUrl(ownProps.match.params.courseId, {
cohort: getCohortNameById(state, state.filters.cohort),
track: state.filters.track,
@@ -90,6 +73,8 @@ const mapStateToProps = (state, ownProps) => (
courseGradeMin: formatMinCourseGrade(state.filters.courseGradeMin),
courseGradeMax: formatMaxCourseGrade(state.filters.courseGradeMax),
}),
+ grades: state.grades.results,
+ headings: getHeadings(state),
interventionExportUrl:
LmsApiService.getInterventionExportCsvUrl(ownProps.match.params.courseId, {
cohort: getCohortNameById(state, state.filters.cohort),
@@ -106,42 +91,42 @@ const mapStateToProps = (state, ownProps) => (
courseGradeMin: formatMinCourseGrade(state.filters.courseGradeMin),
courseGradeMax: formatMaxCourseGrade(state.filters.courseGradeMax),
}),
- bulkImportError: state.grades.bulkManagement
- && state.grades.bulkManagement.errorMessages
- ? `Errors while processing: ${state.grades.bulkManagement.errorMessages.join(', ')}`
- : '',
+ nextPage: state.grades.nextPage,
+ prevPage: state.grades.prevPage,
+ selectedTrack: state.filters.track,
+ selectedCohort: state.filters.cohort,
+ selectedAssignmentType: state.filters.assignmentType,
+ selectedAssignment: (state.filters.assignment || {}).label,
+ showBulkManagement: stateHasMastersTrack(state) && state.config.bulkManagementAvailable,
+ showSpinner: shouldShowSpinner(state),
+ showSuccess: state.grades.showSuccess,
+ totalUsersCount: state.grades.totalUsersCount,
+ tracks: state.tracks.results,
uploadSuccess: !!(state.grades.bulkManagement
&& state.grades.bulkManagement.uploadSuccess),
- showBulkManagement: stateHasMastersTrack(state) && state.config.bulkManagementAvailable,
- bulkManagementHistory: getBulkManagementHistory(state),
- totalUsersCount: state.grades.totalUsersCount,
- filteredUsersCount: state.grades.filteredUsersCount,
}
);
const mapDispatchToProps = {
- doneViewingAssignment,
- getUserGrades: fetchGrades,
- fetchGradeOverrideHistory,
- searchForUser: fetchMatchingUserGrades,
- getPrevNextGrades: fetchPrevNextGrades,
- getCohorts: fetchCohorts,
- getTracks: fetchTracks,
- getAssignmentTypes: fetchAssignmentTypes,
- updateGrades,
- toggleFormat: toggleGradeFormat,
- filterAssignmentType,
closeBanner,
- getRoles,
- submitFileUploadFormData,
- initializeFilters,
- resetFilters,
- updateAssignmentFilter,
- updateAssignmentLimits,
- updateGradesIfAssignmentGradeFiltersSet,
- updateCourseGradeFilter,
downloadBulkGradesReport,
downloadInterventionReport,
+ fetchGradeOverrideHistory,
+ filterAssignmentType,
+ getAssignmentTypes: fetchAssignmentTypes,
+ getCohorts: fetchCohorts,
+ getPrevNextGrades: fetchPrevNextGrades,
+ getRoles,
+ getTracks: fetchTracks,
+ getUserGrades: fetchGrades,
+ initializeFilters,
+ resetFilters,
+ searchForUser: fetchMatchingUserGrades,
+ submitFileUploadFormData,
+ toggleFormat: toggleGradeFormat,
+ updateAssignmentFilter,
+ updateAssignmentLimits,
+ updateCourseGradeFilter,
};
const GradebookPage = connect(