Compare commits
29 Commits
open-relea
...
v1.4.29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ae048d9f1 | ||
|
|
f0f3212843 | ||
|
|
3e7a79c3e1 | ||
|
|
0e4541b7e3 | ||
|
|
452b39ddc5 | ||
|
|
126787b50f | ||
|
|
c295207ed2 | ||
|
|
c0d4e3a8f3 | ||
|
|
e7bfdb7c8d | ||
|
|
0736c44d80 | ||
|
|
2335f4b9e6 | ||
|
|
1385b1a31a | ||
|
|
7e0e286efe | ||
|
|
92035af5d7 | ||
|
|
0220efcc0b | ||
|
|
4be9ba9aa4 | ||
|
|
9d6cf2e06b | ||
|
|
38324a0fc9 | ||
|
|
ced162356a | ||
|
|
e8aca4fde2 | ||
|
|
b7004e6e86 | ||
|
|
12a85abb96 | ||
|
|
dfe6dbae8f | ||
|
|
21ec5fbbe5 | ||
|
|
83701acc16 | ||
|
|
b6b431dc37 | ||
|
|
751d6f4a42 | ||
|
|
b2a737e936 | ||
|
|
1f93215648 |
342
package-lock.json
generated
342
package-lock.json
generated
@@ -4233,6 +4233,24 @@
|
||||
"resolved": "https://registry.npmjs.org/@redux-beacon/segment/-/segment-1.1.0.tgz",
|
||||
"integrity": "sha512-NLRoP3Jfx5z99YX6TFFznwXIMjqjD6/qdMZIKFRgGO8NtMWrCruA8EeQYPJZUBnuOjw6RtOA1UdjbqyRmdhc/Q=="
|
||||
},
|
||||
"@reduxjs/toolkit": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.5.1.tgz",
|
||||
"integrity": "sha512-PngZKuwVZsd+mimnmhiOQzoD0FiMjqVks6ituO1//Ft5UEX5Ca9of13NEjo//pU22Jk7z/mdXVsmDfgsig1osA==",
|
||||
"requires": {
|
||||
"immer": "^8.0.1",
|
||||
"redux": "^4.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"immer": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.4.tgz",
|
||||
"integrity": "sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@restart/context": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
|
||||
@@ -6162,6 +6180,23 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"util": "0.10.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
|
||||
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
|
||||
"dev": true
|
||||
},
|
||||
"util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"assert-ok": {
|
||||
@@ -6250,6 +6285,14 @@
|
||||
"postcss-value-parser": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"available-typed-arrays": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
|
||||
"integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==",
|
||||
"requires": {
|
||||
"array-filter": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
@@ -11823,6 +11866,11 @@
|
||||
"integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
|
||||
"dev": true
|
||||
},
|
||||
"foreach": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
|
||||
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
@@ -13619,8 +13667,7 @@
|
||||
"is-arguments": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
|
||||
"integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA=="
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
@@ -13797,6 +13844,11 @@
|
||||
"integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-generator-function": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.9.tgz",
|
||||
"integrity": "sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A=="
|
||||
},
|
||||
"is-gif": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-gif/-/is-gif-3.0.0.tgz",
|
||||
@@ -14067,6 +14119,117 @@
|
||||
"text-extensions": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-typed-array": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.5.tgz",
|
||||
"integrity": "sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug==",
|
||||
"requires": {
|
||||
"available-typed-arrays": "^1.0.2",
|
||||
"call-bind": "^1.0.2",
|
||||
"es-abstract": "^1.18.0-next.2",
|
||||
"foreach": "^2.0.5",
|
||||
"has-symbols": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"es-abstract": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz",
|
||||
"integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.2",
|
||||
"is-callable": "^1.2.3",
|
||||
"is-negative-zero": "^2.0.1",
|
||||
"is-regex": "^1.1.2",
|
||||
"is-string": "^1.0.5",
|
||||
"object-inspect": "^1.9.0",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.2",
|
||||
"string.prototype.trimend": "^1.0.4",
|
||||
"string.prototype.trimstart": "^1.0.4",
|
||||
"unbox-primitive": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"has-symbols": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-callable": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
|
||||
"integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
|
||||
},
|
||||
"is-regex": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
|
||||
"integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz",
|
||||
"integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA=="
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
|
||||
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"object-keys": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"string.prototype.trimend": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
|
||||
"integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"string.prototype.trimstart": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
|
||||
"integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
@@ -16884,7 +17047,7 @@
|
||||
"dependencies": {
|
||||
"query-string": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
|
||||
"resolved": "http://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
|
||||
"integrity": "sha1-fbBmZCCAS6qSrp8miWKFWnYUPfs=",
|
||||
"requires": {
|
||||
"strict-uri-encode": "^1.0.0"
|
||||
@@ -25190,6 +25353,11 @@
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"reselect": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
|
||||
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
|
||||
@@ -29061,20 +29229,16 @@
|
||||
}
|
||||
},
|
||||
"util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
|
||||
"dev": true,
|
||||
"version": "0.12.3",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.3.tgz",
|
||||
"integrity": "sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog==",
|
||||
"requires": {
|
||||
"inherits": "2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
|
||||
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
|
||||
"dev": true
|
||||
}
|
||||
"inherits": "^2.0.3",
|
||||
"is-arguments": "^1.0.4",
|
||||
"is-generator-function": "^1.0.7",
|
||||
"is-typed-array": "^1.1.3",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"which-typed-array": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
@@ -30064,6 +30228,152 @@
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
|
||||
},
|
||||
"which-typed-array": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.4.tgz",
|
||||
"integrity": "sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==",
|
||||
"requires": {
|
||||
"available-typed-arrays": "^1.0.2",
|
||||
"call-bind": "^1.0.0",
|
||||
"es-abstract": "^1.18.0-next.1",
|
||||
"foreach": "^2.0.5",
|
||||
"function-bind": "^1.1.1",
|
||||
"has-symbols": "^1.0.1",
|
||||
"is-typed-array": "^1.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"es-abstract": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz",
|
||||
"integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.2",
|
||||
"is-callable": "^1.2.3",
|
||||
"is-negative-zero": "^2.0.1",
|
||||
"is-regex": "^1.1.2",
|
||||
"is-string": "^1.0.5",
|
||||
"object-inspect": "^1.9.0",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.2",
|
||||
"string.prototype.trimend": "^1.0.4",
|
||||
"string.prototype.trimstart": "^1.0.4",
|
||||
"unbox-primitive": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-callable": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
|
||||
"integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
|
||||
},
|
||||
"is-regex": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
|
||||
"integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-symbols": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz",
|
||||
"integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA=="
|
||||
},
|
||||
"object.assign": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
|
||||
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0",
|
||||
"define-properties": "^1.1.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"object-keys": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"string.prototype.trimend": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
|
||||
"integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"string.prototype.trimstart": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
|
||||
"integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
|
||||
"requires": {
|
||||
"call-bind": "^1.0.2",
|
||||
"define-properties": "^1.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"wide-align": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.4.28",
|
||||
"version": "1.4.29",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -35,6 +35,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.5",
|
||||
"@redux-beacon/segment": "^1.0.0",
|
||||
"@reduxjs/toolkit": "^1.5.1",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "3.6.5",
|
||||
"email-prop-type": "^1.1.7",
|
||||
@@ -58,6 +59,7 @@
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"util": "^0.12.3",
|
||||
"whatwg-fetch": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -12,8 +12,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { configuration } from '../../config';
|
||||
import { submitFileUploadFormData } from '../../data/actions/grades';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import { configuration } from 'config';
|
||||
|
||||
export class BulkManagement extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -193,7 +193,7 @@ export const mapStateToProps = (state) => {
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
submitFileUploadFormData,
|
||||
submitFileUploadFormData: thunkActions.grades.submitFileUploadFormData,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagement);
|
||||
|
||||
@@ -7,10 +7,7 @@ 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';
|
||||
import actions from 'data/actions';
|
||||
|
||||
export class BulkManagementControls extends React.Component {
|
||||
handleClickDownloadInterventions = () => {
|
||||
@@ -83,8 +80,8 @@ BulkManagementControls.propTypes = {
|
||||
export const mapStateToProps = () => ({ });
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
downloadBulkGradesReport,
|
||||
downloadInterventionReport,
|
||||
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
|
||||
downloadInterventionReport: actions.grades.downloadReport.intervention,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);
|
||||
|
||||
@@ -11,10 +11,8 @@ import {
|
||||
} from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import {
|
||||
doneViewingAssignment,
|
||||
updateGrades,
|
||||
} from '../../data/actions/grades';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
const GRADE_OVERRIDE_HISTORY_COLUMNS = [
|
||||
{ label: 'Date', key: 'date' },
|
||||
@@ -203,8 +201,8 @@ export const mapStateToProps = (state) => {
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
doneViewingAssignment,
|
||||
updateGrades,
|
||||
doneViewingAssignment: actions.grades.doneViewingAssignment,
|
||||
updateGrades: thunkActions.grades.updateGrades,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditModal);
|
||||
|
||||
@@ -3,12 +3,14 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import * as gradesActions from 'data/actions/grades';
|
||||
import * as filterActions from 'data/actions/filters';
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
|
||||
const { updateGradesIfAssignmentGradeFiltersSet } = thunkActions.grades;
|
||||
|
||||
export class AssignmentFilter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -97,8 +99,8 @@ export const mapStateToProps = (state) => {
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
updateAssignmentFilter: filterActions.updateAssignmentFilter,
|
||||
updateGradesIfAssignmentGradeFiltersSet: gradesActions.updateGradesIfAssignmentGradeFiltersSet,
|
||||
updateAssignmentFilter: actions.filters.update.assignment,
|
||||
updateGradesIfAssignmentGradeFiltersSet,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentFilter);
|
||||
|
||||
@@ -2,14 +2,18 @@ import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { updateAssignmentFilter } from 'data/actions/filters';
|
||||
import { updateGradesIfAssignmentGradeFiltersSet } from 'data/actions/grades';
|
||||
import actions from 'data/actions';
|
||||
import { updateGradesIfAssignmentGradeFiltersSet } from 'data/thunkActions/grades';
|
||||
import {
|
||||
AssignmentFilter,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('data/thunkActions/grades', () => ({
|
||||
updateGradesIfAssignmentGradeFiltersSet: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
/** Mocking to use passed state for validation purposes */
|
||||
filters: {
|
||||
@@ -160,7 +164,7 @@ describe('AssignmentFilter', () => {
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('updateAssignmentFilter', () => {
|
||||
expect(mapDispatchToProps.updateAssignmentFilter).toEqual(
|
||||
updateAssignmentFilter,
|
||||
actions.filters.update.assignment,
|
||||
);
|
||||
});
|
||||
test('updateGradesIfAsssignmentGradeFiltersSet', () => {
|
||||
|
||||
@@ -5,9 +5,9 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import * as gradesActions from 'data/actions/grades';
|
||||
import * as filterActions from 'data/actions/filters';
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import PercentGroup from '../PercentGroup';
|
||||
|
||||
@@ -25,10 +25,10 @@ export class AssignmentGradeFilter extends React.Component {
|
||||
assignmentGradeMax,
|
||||
} = this.props.filterValues;
|
||||
|
||||
this.props.updateAssignmentLimits(
|
||||
assignmentGradeMin,
|
||||
assignmentGradeMax,
|
||||
);
|
||||
this.props.updateAssignmentLimits({
|
||||
maxGrade: assignmentGradeMax,
|
||||
minGrade: assignmentGradeMin,
|
||||
});
|
||||
this.props.getUserGrades(
|
||||
this.props.courseId,
|
||||
this.props.selectedCohort,
|
||||
@@ -118,8 +118,8 @@ export const mapStateToProps = (state) => {
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
getUserGrades: gradesActions.fetchGrades,
|
||||
updateAssignmentLimits: filterActions.updateAssignmentLimits,
|
||||
getUserGrades: thunkActions.grades.fetchGrades,
|
||||
updateAssignmentLimits: actions.filters.update.assignmentLimits,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentGradeFilter);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
|
||||
import { updateAssignmentLimits } from 'data/actions/filters';
|
||||
import { fetchGrades } from 'data/actions/grades';
|
||||
import actions from 'data/actions';
|
||||
import { fetchGrades } from 'data/thunkActions/grades';
|
||||
|
||||
import {
|
||||
AssignmentGradeFilter,
|
||||
@@ -43,10 +43,12 @@ describe('AssignmentGradeFilter', () => {
|
||||
el.instance().handleSubmit();
|
||||
});
|
||||
it('calls props.updateAssignmentLimits with min and max', () => {
|
||||
expect(props.updateAssignmentLimits).toHaveBeenCalledWith(
|
||||
props.filterValues.assignmentGradeMin,
|
||||
props.filterValues.assignmentGradeMax,
|
||||
);
|
||||
expect(
|
||||
props.updateAssignmentLimits,
|
||||
).toHaveBeenCalledWith({
|
||||
maxGrade: props.filterValues.assignmentGradeMax,
|
||||
minGrade: props.filterValues.assignmentGradeMin,
|
||||
});
|
||||
});
|
||||
it('calls getUserGrades w/ selection', () => {
|
||||
expect(props.getUserGrades).toHaveBeenCalledWith(
|
||||
@@ -167,7 +169,7 @@ describe('AssignmentGradeFilter', () => {
|
||||
expect(
|
||||
mapDispatchToProps.updateAssignmentLimits,
|
||||
).toEqual(
|
||||
updateAssignmentLimits,
|
||||
actions.filters.update.assignmentLimits,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,9 +3,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import * as gradesActions from 'data/actions/grades';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import SelectGroup from '../SelectGroup';
|
||||
|
||||
export class AssignmentTypeFilter extends React.Component {
|
||||
@@ -72,7 +71,7 @@ export const mapStateToProps = (state) => ({
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
filterAssignmentType: gradesActions.filterAssignmentType,
|
||||
filterAssignmentType: actions.filters.update.assignmentType,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentTypeFilter);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { filterAssignmentType } from 'data/actions/grades';
|
||||
import actions from 'data/actions';
|
||||
|
||||
import {
|
||||
AssignmentTypeFilter,
|
||||
@@ -128,7 +128,7 @@ describe('AssignmentTypeFilter', () => {
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('filterAssignmentType', () => {
|
||||
expect(mapDispatchToProps.filterAssignmentType).toEqual(
|
||||
filterAssignmentType,
|
||||
actions.filters.update.assignmentType,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { updateCourseGradeFilter } from 'data/actions/filters';
|
||||
import { fetchGrades } from 'data/actions/grades';
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import PercentGroup from '../PercentGroup';
|
||||
|
||||
export class CourseGradeFilter extends React.Component {
|
||||
@@ -37,11 +37,11 @@ export class CourseGradeFilter extends React.Component {
|
||||
|
||||
updateCourseGradeFilters() {
|
||||
const { courseGradeMin, courseGradeMax } = this.props.filterValues;
|
||||
this.props.updateFilter(
|
||||
this.props.updateFilter({
|
||||
courseGradeMin,
|
||||
courseGradeMax,
|
||||
this.props.courseId,
|
||||
);
|
||||
courseId: this.props.courseId,
|
||||
});
|
||||
this.props.getUserGrades(
|
||||
this.props.courseId,
|
||||
this.props.selectedCohort,
|
||||
@@ -129,8 +129,8 @@ export const mapStateToProps = (state) => {
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
updateFilter: updateCourseGradeFilter,
|
||||
getUserGrades: fetchGrades,
|
||||
updateFilter: actions.filters.update.courseGradeLimits,
|
||||
getUserGrades: thunkActions.grades.fetchGrades,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CourseGradeFilter);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { updateCourseGradeFilter } from 'data/actions/filters';
|
||||
import { fetchGrades } from 'data/actions/grades';
|
||||
import actions from 'data/actions';
|
||||
import { fetchGrades } from 'data/thunkActions/grades';
|
||||
import {
|
||||
CourseGradeFilter,
|
||||
mapStateToProps,
|
||||
@@ -91,11 +91,11 @@ describe('CourseGradeFilter', () => {
|
||||
el.instance().updateCourseGradeFilters();
|
||||
});
|
||||
it('calls props.updateFilter with selection', () => {
|
||||
expect(props.updateFilter).toHaveBeenCalledWith(
|
||||
props.filterValues.courseGradeMin,
|
||||
props.filterValues.courseGradeMax,
|
||||
props.courseId,
|
||||
);
|
||||
expect(props.updateFilter).toHaveBeenCalledWith({
|
||||
courseGradeMin: props.filterValues.courseGradeMin,
|
||||
courseGradeMax: props.filterValues.courseGradeMax,
|
||||
courseId: props.courseId,
|
||||
});
|
||||
});
|
||||
it('calls props.getUserGrades with selection', () => {
|
||||
expect(props.getUserGrades).toHaveBeenCalledWith(
|
||||
@@ -184,7 +184,7 @@ describe('CourseGradeFilter', () => {
|
||||
describe('mapDispatchToProps', () => {
|
||||
describe('updateFilter', () => {
|
||||
test('from updateCourseGradeFilter', () => {
|
||||
expect(mapDispatchToProps.updateFilter).toEqual(updateCourseGradeFilter);
|
||||
expect(mapDispatchToProps.updateFilter).toEqual(actions.filters.update.courseGradeLimits);
|
||||
});
|
||||
});
|
||||
describe('getUserGrades', () => {
|
||||
|
||||
@@ -3,8 +3,9 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { fetchGrades } from 'data/actions/grades';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
|
||||
export class StudentGroupsFilter extends React.Component {
|
||||
@@ -147,7 +148,7 @@ export const mapStateToProps = (state) => {
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
getUserGrades: fetchGrades,
|
||||
getUserGrades: thunkActions.grades.fetchGrades,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { fetchGrades } from 'data/actions/grades';
|
||||
import { fetchGrades } from 'data/thunkActions/grades';
|
||||
import {
|
||||
StudentGroupsFilter,
|
||||
mapStateToProps,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { Collapsible, Form } from '@edx/paragon';
|
||||
|
||||
import * as filterActions from 'data/actions/filters';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import AssignmentTypeFilter from './AssignmentTypeFilter';
|
||||
@@ -114,7 +114,7 @@ export const mapStateToProps = (state) => ({
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
updateIncludeCourseRoleMembers: filterActions.updateIncludeCourseRoleMembers,
|
||||
updateIncludeCourseRoleMembers: actions.filters.update.includeCourseRoleMembers,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradebookFilters);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { updateIncludeCourseRoleMembers } from 'data/actions/filters';
|
||||
import actions from 'data/actions';
|
||||
|
||||
import {
|
||||
GradebookFilters,
|
||||
@@ -98,7 +98,7 @@ describe('GradebookFilters', () => {
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('updateIncludeCourseRoleMembers', () => {
|
||||
expect(mapDispatchToProps.updateIncludeCourseRoleMembers).toEqual(
|
||||
updateIncludeCourseRoleMembers,
|
||||
actions.filters.update.includeCourseRoleMembers,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
Table, OverlayTrigger, Tooltip, Icon,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { formatDateForDisplay } from '../../data/actions/utils';
|
||||
import { fetchGradeOverrideHistory } from '../../data/actions/grades';
|
||||
import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../../data/constants/grades';
|
||||
import selectors from '../../data/selectors';
|
||||
import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from 'data/constants/grades';
|
||||
import selectors from 'data/selectors';
|
||||
import { formatDateForDisplay } from 'data/actions/utils';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
const DECIMAL_PRECISION = 2;
|
||||
|
||||
@@ -228,7 +228,7 @@ export const mapStateToProps = (state) => {
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
fetchGradeOverrideHistory,
|
||||
fetchGradeOverrideHistory: thunkActions.grades.fetchGradeOverrideHistory,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradebookTable);
|
||||
|
||||
@@ -5,10 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { Button, Icon, SearchField } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import {
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
} from '../../data/actions/grades';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
/**
|
||||
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
|
||||
@@ -107,8 +104,8 @@ export const mapStateToProps = (state) => {
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
getUserGrades: fetchGrades,
|
||||
searchForUser: fetchMatchingUserGrades,
|
||||
getUserGrades: thunkActions.grades.fetchGrades,
|
||||
searchForUser: thunkActions.grades.fetchMatchingUserGrades,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SearchControls);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { shallow } from 'enzyme';
|
||||
import {
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
} from '../../data/actions/grades';
|
||||
} from '../../data/thunkActions/grades';
|
||||
import { mapDispatchToProps, mapStateToProps, SearchControls } from './SearchControls';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { StatusAlert } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { closeBanner } from '../../data/actions/grades';
|
||||
import actions from 'data/actions';
|
||||
|
||||
export const maxCourseGradeInvalidMessage = 'Maximum course grade value must be between 0 and 100. ';
|
||||
export const minCourseGradeInvalidMessage = 'Minimum course grade value must be between 0 and 100. ';
|
||||
@@ -66,7 +66,7 @@ export const mapStateToProps = (state) => ({
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
handleCloseSuccessBanner: closeBanner,
|
||||
handleCloseSuccessBanner: actions.grades.banner.close,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusAlerts);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import {
|
||||
StatusAlerts,
|
||||
mapDispatchToProps,
|
||||
@@ -7,7 +9,6 @@ import {
|
||||
maxCourseGradeInvalidMessage,
|
||||
minCourseGradeInvalidMessage,
|
||||
} from './StatusAlerts';
|
||||
import { closeBanner } from '../../data/actions/grades';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
StatusAlert: 'StatusAlert',
|
||||
@@ -92,7 +93,7 @@ describe('StatusAlerts', () => {
|
||||
expect(
|
||||
mapDispatchToProps.handleCloseSuccessBanner,
|
||||
).toEqual(
|
||||
closeBanner,
|
||||
actions.grades.banner.close,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import Gradebook from '../../components/Gradebook';
|
||||
import {
|
||||
fetchGradeOverrideHistory,
|
||||
fetchGrades,
|
||||
fetchPrevNextGrades,
|
||||
filterAssignmentType,
|
||||
submitFileUploadFormData,
|
||||
toggleGradeFormat,
|
||||
downloadBulkGradesReport,
|
||||
downloadInterventionReport,
|
||||
} from '../../data/actions/grades';
|
||||
import { fetchCohorts } from '../../data/actions/cohorts';
|
||||
import { fetchTracks } from '../../data/actions/tracks';
|
||||
import {
|
||||
initializeFilters,
|
||||
resetFilters,
|
||||
updateAssignmentFilter,
|
||||
updateAssignmentLimits,
|
||||
} from '../../data/actions/filters';
|
||||
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
||||
import { getRoles } from '../../data/actions/roles';
|
||||
|
||||
import Gradebook from 'components/Gradebook';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const {
|
||||
@@ -61,22 +44,24 @@ const mapStateToProps = (state, ownProps) => {
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
downloadBulkGradesReport,
|
||||
downloadInterventionReport,
|
||||
fetchGradeOverrideHistory,
|
||||
filterAssignmentType,
|
||||
getAssignmentTypes: fetchAssignmentTypes,
|
||||
getCohorts: fetchCohorts,
|
||||
getPrevNextGrades: fetchPrevNextGrades,
|
||||
getRoles,
|
||||
getTracks: fetchTracks,
|
||||
getUserGrades: fetchGrades,
|
||||
initializeFilters,
|
||||
resetFilters,
|
||||
submitFileUploadFormData,
|
||||
toggleFormat: toggleGradeFormat,
|
||||
updateAssignmentFilter,
|
||||
updateAssignmentLimits,
|
||||
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
|
||||
downloadInterventionReport: actions.grades.downloadReport.intervention,
|
||||
toggleFormat: actions.grades.toggleGradeFormat,
|
||||
|
||||
filterAssignmentType: actions.filters.update.assignmentType,
|
||||
initializeFilters: actions.filters.initialize,
|
||||
resetFilters: actions.filters.reset,
|
||||
updateAssignmentFilter: actions.filters.update.assignment,
|
||||
updateAssignmentLimits: actions.filters.update.assignmentLimits,
|
||||
|
||||
fetchGradeOverrideHistory: thunkActions.grades.fetchGradeOverrideHistory,
|
||||
getAssignmentTypes: thunkActions.assignmentTypes.fetchAssignmentTypes,
|
||||
getCohorts: thunkActions.cohorts.fetchCohorts,
|
||||
getPrevNextGrades: thunkActions.grades.fetchPrevNextGrades,
|
||||
getRoles: thunkActions.roles.fetchRoles,
|
||||
getTracks: thunkActions.tracks.fetchTracks,
|
||||
getUserGrades: thunkActions.grades.fetchGrades,
|
||||
submitFileUploadFormData: thunkActions.grades.submitFileUploadFormData,
|
||||
};
|
||||
|
||||
const GradebookPage = connect(
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
import {
|
||||
STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ASSIGNMENT_TYPES,
|
||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ARE_GRADES_FROZEN,
|
||||
} from '../constants/actionTypes/assignmentTypes';
|
||||
import GOT_BULK_MANAGEMENT_CONFIG from '../constants/actionTypes/config';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import { StrictDict } from 'utils';
|
||||
import { createActionFactory } from './utils';
|
||||
|
||||
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 gotBulkManagementConfig = bulkManagementEnabled => ({
|
||||
type: GOT_BULK_MANAGEMENT_CONFIG,
|
||||
data: bulkManagementEnabled,
|
||||
});
|
||||
export const dataKey = 'assignmentTypes';
|
||||
const createAction = createActionFactory(dataKey);
|
||||
|
||||
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));
|
||||
dispatch(gotBulkManagementConfig(data.can_see_bulk_management));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingAssignmentTypes());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
fetchAssignmentTypes,
|
||||
startedFetchingAssignmentTypes,
|
||||
gotAssignmentTypes,
|
||||
errorFetchingAssignmentTypes,
|
||||
const fetching = {
|
||||
error: createAction('fetching/error'),
|
||||
started: createAction('fetching/started'),
|
||||
received: createAction('fetching/received'),
|
||||
};
|
||||
const gotGradesFrozen = createAction('gotGradesFrozen');
|
||||
|
||||
export default StrictDict({
|
||||
fetching: StrictDict(fetching),
|
||||
gotGradesFrozen,
|
||||
});
|
||||
|
||||
@@ -1,101 +1,22 @@
|
||||
import axios from 'axios';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
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';
|
||||
import GOT_BULK_MANAGEMENT_CONFIG from '../constants/actionTypes/config';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
getAuthenticatedHttpClient.mockReturnValue(axios);
|
||||
axios.isAccessTokenExpired = jest.fn();
|
||||
axios.isAccessTokenExpired.mockReturnValue(false);
|
||||
import actions, { dataKey } from './assignmentTypes';
|
||||
import { testAction, testActionTypes } from './testUtils';
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
describe('action types', () => {
|
||||
const actionTypes = [
|
||||
actions.fetching.error,
|
||||
actions.fetching.started,
|
||||
actions.fetching.received,
|
||||
actions.gotGradesFrozen,
|
||||
].map(action => action.toString());
|
||||
testActionTypes(actionTypes, dataKey);
|
||||
});
|
||||
|
||||
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,
|
||||
can_see_bulk_management: true,
|
||||
};
|
||||
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 },
|
||||
{ type: GOT_BULK_MANAGEMENT_CONFIG, data: true },
|
||||
];
|
||||
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 },
|
||||
{ type: GOT_BULK_MANAGEMENT_CONFIG, data: 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);
|
||||
});
|
||||
describe('actions provided', () => {
|
||||
describe('fetching actions', () => {
|
||||
test('error action', () => testAction(actions.fetching.error));
|
||||
test('started action', () => testAction(actions.fetching.started));
|
||||
test('received action', () => testAction(actions.fetching.received));
|
||||
});
|
||||
test('gotGradesFrozen action', () => testAction(actions.gotGradesFrozen));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import { StrictDict } from 'utils';
|
||||
import { createActionFactory } from './utils';
|
||||
|
||||
const startedFetchingCohorts = () => ({ type: STARTED_FETCHING_COHORTS });
|
||||
const errorFetchingCohorts = () => ({ type: ERROR_FETCHING_COHORTS });
|
||||
const gotCohorts = cohorts => ({ type: GOT_COHORTS, cohorts });
|
||||
export const dataKey = 'cohorts';
|
||||
const createAction = createActionFactory(dataKey);
|
||||
|
||||
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,
|
||||
const fetching = {
|
||||
started: createAction('fetching/started'),
|
||||
error: createAction('fetching/error'),
|
||||
received: createAction('fetching/received'),
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
fetching: StrictDict(fetching),
|
||||
});
|
||||
|
||||
@@ -1,80 +1,20 @@
|
||||
import axios from 'axios';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
import actions, { dataKey } from './cohorts';
|
||||
import { testAction, testActionTypes } from './testUtils';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
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]);
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
getAuthenticatedHttpClient.mockReturnValue(axios);
|
||||
axios.isAccessTokenExpired = jest.fn();
|
||||
axios.isAccessTokenExpired.mockReturnValue(false);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
describe('actions.cohorts', () => {
|
||||
describe('action types', () => {
|
||||
const actionTypes = [
|
||||
actions.fetching.error,
|
||||
actions.fetching.started,
|
||||
actions.fetching.received,
|
||||
].map(action => action.toString());
|
||||
testActionTypes(actionTypes, dataKey);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
describe('actions provided', () => {
|
||||
describe('fecthing actions', () => {
|
||||
test('error action', () => testAction(actions.fetching.error));
|
||||
test('started action', () => testAction(actions.fetching.started));
|
||||
test('received action', () => testAction(actions.fetching.received));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
11
src/data/actions/config.js
Normal file
11
src/data/actions/config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { createActionFactory } from './utils';
|
||||
|
||||
export const dataKey = 'config';
|
||||
const createAction = createActionFactory(dataKey);
|
||||
|
||||
const gotBulkManagementConfig = createAction('gotBulkManagement');
|
||||
|
||||
export default StrictDict({
|
||||
gotBulkManagementConfig,
|
||||
});
|
||||
14
src/data/actions/config.test.js
Normal file
14
src/data/actions/config.test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import actions, { dataKey } from './config';
|
||||
import { testAction, testActionTypes } from './testUtils';
|
||||
|
||||
describe('actions.cohorts', () => {
|
||||
describe('action types', () => {
|
||||
const actionTypes = [
|
||||
actions.gotBulkManagementConfig,
|
||||
].map(action => action.toString());
|
||||
testActionTypes(actionTypes, dataKey);
|
||||
});
|
||||
describe('actions provided', () => {
|
||||
test('gotBulkManagementConfig action', () => testAction(actions.gotBulkManagementConfig));
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,11 @@
|
||||
import filterSelectors from 'data/selectors/filters';
|
||||
import { StrictDict } from 'utils';
|
||||
import initialFilters from '../constants/filters';
|
||||
import {
|
||||
INITIALIZE_FILTERS,
|
||||
RESET_FILTERS,
|
||||
UPDATE_ASSIGNMENT_FILTER,
|
||||
UPDATE_ASSIGNMENT_LIMITS,
|
||||
UPDATE_COURSE_GRADE_LIMITS,
|
||||
UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
|
||||
} from '../constants/actionTypes/filters';
|
||||
import { fetchGrades } from './grades';
|
||||
import { createActionFactory } from './utils';
|
||||
|
||||
const { allFilters } = filterSelectors;
|
||||
export const dataKey = 'filters';
|
||||
const createAction = createActionFactory(dataKey);
|
||||
|
||||
const initializeFilters = ({
|
||||
const initialize = createAction('initialize', ({
|
||||
assignment = initialFilters.assignment,
|
||||
assignmentType = initialFilters.assignmentType,
|
||||
track = initialFilters.track,
|
||||
@@ -23,8 +16,7 @@ const initializeFilters = ({
|
||||
courseGradeMax = initialFilters.assignmentGradeMax,
|
||||
includeCourseRoleMembers = initialFilters.includeCourseRoleMembers,
|
||||
}) => ({
|
||||
type: INITIALIZE_FILTERS,
|
||||
data: {
|
||||
payload: {
|
||||
assignment: { id: assignment },
|
||||
assignmentType,
|
||||
track,
|
||||
@@ -35,47 +27,19 @@ const initializeFilters = ({
|
||||
courseGradeMax,
|
||||
includeCourseRoleMembers: Boolean(includeCourseRoleMembers),
|
||||
},
|
||||
}));
|
||||
|
||||
const reset = createAction('reset');
|
||||
const update = StrictDict({
|
||||
assignment: createAction('update/assignment'),
|
||||
assignmentType: createAction('update/assignmentType'),
|
||||
assignmentLimits: createAction('update/assignmentLimits'),
|
||||
courseGradeLimits: createAction('update/courseGradeLimits'),
|
||||
includeCourseRoleMembers: createAction('update/includeCourseRoleMembers'),
|
||||
});
|
||||
|
||||
const resetFilters = filterNames => ({
|
||||
type: RESET_FILTERS,
|
||||
filterNames,
|
||||
export default StrictDict({
|
||||
initialize,
|
||||
reset,
|
||||
update: StrictDict(update),
|
||||
});
|
||||
|
||||
const updateAssignmentFilter = assignment => ({
|
||||
type: UPDATE_ASSIGNMENT_FILTER,
|
||||
data: assignment,
|
||||
});
|
||||
|
||||
const updateAssignmentLimits = (minGrade, maxGrade) => ({
|
||||
type: UPDATE_ASSIGNMENT_LIMITS,
|
||||
data: { minGrade, maxGrade },
|
||||
});
|
||||
|
||||
const updateCourseGradeFilter = (courseGradeMin, courseGradeMax, courseId) => ({
|
||||
type: UPDATE_COURSE_GRADE_LIMITS,
|
||||
data: {
|
||||
courseGradeMin,
|
||||
courseGradeMax,
|
||||
courseId,
|
||||
},
|
||||
});
|
||||
|
||||
const updateIncludeCourseRoleMembersFilter = (includeCourseRoleMembers) => ({
|
||||
type: UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
|
||||
data: {
|
||||
includeCourseRoleMembers,
|
||||
},
|
||||
});
|
||||
|
||||
const updateIncludeCourseRoleMembers = includeCourseRoleMembers => (dispatch, getState) => {
|
||||
dispatch(updateIncludeCourseRoleMembersFilter(includeCourseRoleMembers));
|
||||
const state = getState();
|
||||
const { cohort, track, assignmentType } = allFilters(state);
|
||||
dispatch(fetchGrades(state.grades.courseId, cohort, track, assignmentType));
|
||||
};
|
||||
|
||||
export {
|
||||
initializeFilters, resetFilters, updateAssignmentFilter,
|
||||
updateAssignmentLimits, updateCourseGradeFilter, updateIncludeCourseRoleMembers,
|
||||
};
|
||||
|
||||
68
src/data/actions/filters.test.js
Normal file
68
src/data/actions/filters.test.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import actions, { dataKey } from './filters';
|
||||
import initialFilters from '../constants/filters';
|
||||
import { testAction, testActionTypes } from './testUtils';
|
||||
|
||||
describe('actions.filters', () => {
|
||||
describe('action types', () => {
|
||||
const actionTypes = [
|
||||
actions.initialize,
|
||||
actions.reset,
|
||||
actions.update.assignment,
|
||||
actions.update.assignmentType,
|
||||
actions.update.assignmentLimits,
|
||||
actions.update.courseGradeLimits,
|
||||
actions.update.includeCourseRoleMembers,
|
||||
].map(action => action.toString());
|
||||
testActionTypes(actionTypes, dataKey);
|
||||
});
|
||||
describe('actions provided', () => {
|
||||
describe('initialize action', () => {
|
||||
it('sets initialFilters values for missing args', () => {
|
||||
testAction(actions.initialize, {}, {
|
||||
assignment: { id: initialFilters.assignment },
|
||||
assignmentType: initialFilters.assignmentType,
|
||||
cohort: initialFilters.cohort,
|
||||
track: initialFilters.track,
|
||||
assignmentGradeMin: initialFilters.assignmentGradeMin,
|
||||
assignmentGradeMax: initialFilters.assignmentGradeMax,
|
||||
courseGradeMin: initialFilters.courseGradeMin,
|
||||
courseGradeMax: initialFilters.courseGradeMax,
|
||||
includeCourseRoleMembers: initialFilters.includeCourseRoleMembers,
|
||||
});
|
||||
});
|
||||
it('loads filters from args', () => {
|
||||
const expected = {
|
||||
assignment: { id: 'assIGNmentId' },
|
||||
assignmentType: 'aType',
|
||||
track: 'masters',
|
||||
cohort: 3,
|
||||
assignmentGradeMin: 23,
|
||||
assignmentGradeMax: 98,
|
||||
courseGradeMin: 11,
|
||||
courseGradeMax: 87,
|
||||
includeCourseRoleMembers: true,
|
||||
};
|
||||
const args = { ...expected, assignment: expected.assignment.id, also: 'other stuff' };
|
||||
testAction(actions.initialize, args, expected);
|
||||
});
|
||||
});
|
||||
test('reset action', () => testAction(actions.reset));
|
||||
describe('update actions', () => {
|
||||
test('update.assignment action', () => (
|
||||
testAction(actions.update.assignment)
|
||||
));
|
||||
test('update.assignmentType action', () => (
|
||||
testAction(actions.update.assignmentType)
|
||||
));
|
||||
test('update.assignmentLimits action', () => (
|
||||
testAction(actions.update.assignmentLimits)
|
||||
));
|
||||
test('update.courseGradeLimits action', () => (
|
||||
testAction(actions.update.courseGradeLimits)
|
||||
));
|
||||
test('update.includeCourseRoleMembers action', () => (
|
||||
testAction(actions.update.includeCourseRoleMembers)
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,359 +1,103 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import gradesSelectors from 'data/selectors/grades';
|
||||
import filtersSelectors from 'data/selectors/filters';
|
||||
import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
GRADE_UPDATE_REQUEST,
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_BY_ASSIGNMENT_TYPE,
|
||||
OPEN_BANNER,
|
||||
CLOSE_BANNER,
|
||||
START_UPLOAD,
|
||||
UPLOAD_COMPLETE,
|
||||
UPLOAD_ERR,
|
||||
GOT_BULK_HISTORY,
|
||||
BULK_HISTORY_ERR,
|
||||
GOT_GRADE_OVERRIDE_HISTORY,
|
||||
DONE_VIEWING_ASSIGNMENT,
|
||||
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
UPLOAD_OVERRIDE,
|
||||
UPLOAD_OVERRIDE_ERROR,
|
||||
BULK_GRADE_REPORT_DOWNLOADED,
|
||||
INTERVENTION_REPORT_DOWNLOADED,
|
||||
} from '../constants/actionTypes/grades';
|
||||
import GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG from '../constants/errors';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import { sortAlphaAsc, formatDateForDisplay } from './utils';
|
||||
import { StrictDict } from 'utils';
|
||||
import { createActionFactory } from './utils';
|
||||
|
||||
const {
|
||||
formatMaxAssignmentGrade,
|
||||
formatMinAssignmentGrade,
|
||||
formatMaxCourseGrade,
|
||||
formatMinCourseGrade,
|
||||
} = gradesSelectors;
|
||||
const { allFilters } = filtersSelectors;
|
||||
export const dataKey = 'grades';
|
||||
const createAction = createActionFactory(dataKey);
|
||||
|
||||
const defaultAssignmentFilter = 'All';
|
||||
const banner = {
|
||||
open: createAction('banner/open'),
|
||||
close: createAction('banner/close'),
|
||||
};
|
||||
|
||||
const startedCsvUpload = () => ({ type: START_UPLOAD });
|
||||
const finishedCsvUpload = () => ({ type: UPLOAD_COMPLETE });
|
||||
const csvUploadError = data => ({ type: UPLOAD_ERR, data });
|
||||
const gotBulkHistory = data => ({ type: GOT_BULK_HISTORY, data });
|
||||
const bulkHistoryError = () => ({ type: BULK_HISTORY_ERR });
|
||||
const bulkHistory = {
|
||||
received: createAction('bulkHistory/received'),
|
||||
error: createAction('bulkHistory/error'),
|
||||
};
|
||||
|
||||
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
|
||||
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
|
||||
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
|
||||
const errorFetchingGradeOverrideHistory = errorMessage => ({
|
||||
type: ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
errorMessage,
|
||||
});
|
||||
const csvUpload = {
|
||||
started: createAction('csvUpload/started'),
|
||||
finished: createAction('csvUpload/finished'),
|
||||
error: createAction('csvUpload/error'),
|
||||
};
|
||||
|
||||
const gotGrades = ({
|
||||
grades, cohort, track, assignmentType, headings, prev,
|
||||
next, courseId, totalUsersCount, filteredUsersCount,
|
||||
}) => ({
|
||||
type: GOT_GRADES,
|
||||
grades,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
headings,
|
||||
prev,
|
||||
next,
|
||||
courseId,
|
||||
totalUsersCount,
|
||||
filteredUsersCount,
|
||||
});
|
||||
const doneViewingAssignment = createAction('doneViewingAssignment');
|
||||
|
||||
const gotGradeOverrideHistory = ({
|
||||
overrideHistory, currentEarnedAllOverride, currentPossibleAllOverride,
|
||||
currentEarnedGradedOverride, currentPossibleGradedOverride,
|
||||
originalGradeEarnedAll, originalGradePossibleAll, originalGradeEarnedGraded,
|
||||
originalGradePossibleGraded,
|
||||
}) => ({
|
||||
type: GOT_GRADE_OVERRIDE_HISTORY,
|
||||
overrideHistory,
|
||||
currentEarnedAllOverride,
|
||||
currentPossibleAllOverride,
|
||||
currentEarnedGradedOverride,
|
||||
currentPossibleGradedOverride,
|
||||
originalGradeEarnedAll,
|
||||
originalGradePossibleAll,
|
||||
originalGradeEarnedGraded,
|
||||
originalGradePossibleGraded,
|
||||
});
|
||||
// for segment tracking
|
||||
const downloadReport = {
|
||||
bulkGrades: createAction('downloadReport/bulkGrades'),
|
||||
intervention: createAction('downloadReport/intervention'),
|
||||
};
|
||||
|
||||
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
|
||||
const gradeUpdateSuccess = (courseId, responseData) => ({
|
||||
type: GRADE_UPDATE_SUCCESS,
|
||||
courseId,
|
||||
payload: { responseData },
|
||||
});
|
||||
const gradeUpdateFailure = (courseId, error) => ({
|
||||
type: GRADE_UPDATE_FAILURE,
|
||||
courseId,
|
||||
payload: { error },
|
||||
});
|
||||
const uploadOverrideSuccess = courseId => ({
|
||||
type: UPLOAD_OVERRIDE,
|
||||
courseId,
|
||||
});
|
||||
// This action for google analytics only. Doesn't change redux state.
|
||||
const downloadBulkGradesReport = courseId => ({
|
||||
type: BULK_GRADE_REPORT_DOWNLOADED,
|
||||
courseId,
|
||||
});
|
||||
// This action for google analytics only. Doesn't change redux state.
|
||||
const downloadInterventionReport = courseId => ({
|
||||
type: INTERVENTION_REPORT_DOWNLOADED,
|
||||
courseId,
|
||||
});
|
||||
const uploadOverrideFailure = (courseId, error) => ({
|
||||
type: UPLOAD_OVERRIDE_ERROR,
|
||||
courseId,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType });
|
||||
|
||||
const filterAssignmentType = filterType => (
|
||||
dispatch => dispatch({
|
||||
type: FILTER_BY_ASSIGNMENT_TYPE,
|
||||
filterType,
|
||||
})
|
||||
);
|
||||
|
||||
const openBanner = () => ({ type: OPEN_BANNER });
|
||||
const closeBanner = () => ({ type: CLOSE_BANNER });
|
||||
|
||||
const fetchGrades = (
|
||||
courseId,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
options = {},
|
||||
) => (
|
||||
(dispatch, getState) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
const {
|
||||
assignment,
|
||||
assignmentGradeMax: assignmentMax,
|
||||
assignmentGradeMin: assignmentMin,
|
||||
courseGradeMin,
|
||||
courseGradeMax,
|
||||
includeCourseRoleMembers,
|
||||
} = allFilters(getState());
|
||||
const { id: assignmentId } = assignment || {};
|
||||
const assignmentGradeMax = formatMaxAssignmentGrade(assignmentMax, { assignmentId });
|
||||
const assignmentGradeMin = formatMinAssignmentGrade(assignmentMin, { assignmentId });
|
||||
const courseGradeMinFormatted = formatMinCourseGrade(courseGradeMin);
|
||||
const courseGradeMaxFormatted = formatMaxCourseGrade(courseGradeMax);
|
||||
return LmsApiService.fetchGradebookData(
|
||||
courseId,
|
||||
options.searchText || null,
|
||||
cohort,
|
||||
track,
|
||||
{
|
||||
assignment: assignmentId,
|
||||
assignmentGradeMax,
|
||||
assignmentGradeMin,
|
||||
courseGradeMin: courseGradeMinFormatted,
|
||||
courseGradeMax: courseGradeMaxFormatted,
|
||||
includeCourseRoleMembers,
|
||||
const fetching = {
|
||||
started: createAction('fetching/started'),
|
||||
finished: createAction('fetching/finished'),
|
||||
error: createAction('fetching/error'),
|
||||
// for segment tracking
|
||||
received: createAction(
|
||||
'fetching/received',
|
||||
(data) => ({
|
||||
payload: {
|
||||
grades: data.grades,
|
||||
cohort: data.cohort,
|
||||
track: data.track,
|
||||
assignmentType: data.assignmentType,
|
||||
headings: data.headings,
|
||||
prev: data.prev,
|
||||
next: data.next,
|
||||
courseId: data.courseId,
|
||||
totalUsersCount: data.totalUsersCount,
|
||||
filteredUsersCount: data.filteredUsersCount,
|
||||
},
|
||||
|
||||
)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotGrades({
|
||||
grades: data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
prev: data.previous,
|
||||
next: data.next,
|
||||
courseId,
|
||||
totalUsersCount: data.total_users_count,
|
||||
filteredUsersCount: data.filtered_users_count,
|
||||
}));
|
||||
dispatch(finishedFetchingGrades());
|
||||
if (options.showSuccess) {
|
||||
dispatch(openBanner());
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingGrades());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const formatGradeOverrideForDisplay = historyArray => historyArray.map(item => ({
|
||||
date: formatDateForDisplay(new Date(item.history_date)),
|
||||
grader: item.history_user,
|
||||
reason: item.override_reason,
|
||||
adjustedGrade: item.earned_graded_override,
|
||||
}));
|
||||
|
||||
const doneViewingAssignment = () => dispatch => dispatch({
|
||||
type: DONE_VIEWING_ASSIGNMENT,
|
||||
});
|
||||
const fetchGradeOverrideHistory = (subsectionId, userId) => (
|
||||
dispatch => LmsApiService.fetchGradeOverrideHistory(subsectionId, userId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
dispatch(gotGradeOverrideHistory({
|
||||
overrideHistory: formatGradeOverrideForDisplay(data.history),
|
||||
currentEarnedAllOverride: data.override ? data.override.earned_all_override : null,
|
||||
currentPossibleAllOverride: data.override ? data.override.possible_all_override : null,
|
||||
currentEarnedGradedOverride: data.override ? data.override.earned_graded_override : null,
|
||||
currentPossibleGradedOverride: data.override
|
||||
? data.override.possible_graded_override : null,
|
||||
originalGradeEarnedAll: data.original_grade ? data.original_grade.earned_all : null,
|
||||
originalGradePossibleAll: data.original_grade ? data.original_grade.possible_all : null,
|
||||
originalGradeEarnedGraded: data.original_grade
|
||||
? data.original_grade.earned_graded : null,
|
||||
originalGradePossibleGraded: data.original_grade
|
||||
? data.original_grade.possible_graded : null,
|
||||
}));
|
||||
} else {
|
||||
dispatch(errorFetchingGradeOverrideHistory(data.error_message));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingGradeOverrideHistory(GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG));
|
||||
})
|
||||
);
|
||||
|
||||
const fetchMatchingUserGrades = (
|
||||
courseId,
|
||||
searchText,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
showSuccess,
|
||||
options = {},
|
||||
) => {
|
||||
const newOptions = { ...options, searchText, showSuccess };
|
||||
return fetchGrades(courseId, cohort, track, assignmentType, newOptions);
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
return getAuthenticatedHttpClient().get(endpoint)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotGrades({
|
||||
grades: data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
prev: data.previous,
|
||||
next: data.next,
|
||||
courseId,
|
||||
totalUsersCount: data.total_users_count,
|
||||
filteredUsersCount: data.filtered_users_count,
|
||||
}));
|
||||
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(courseId, data));
|
||||
dispatch(fetchMatchingUserGrades(
|
||||
courseId,
|
||||
searchText,
|
||||
cohort,
|
||||
track,
|
||||
defaultAssignmentFilter,
|
||||
true,
|
||||
{ searchText },
|
||||
));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(gradeUpdateFailure(courseId, error));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const submitFileUploadFormData = (courseId, formData) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedCsvUpload());
|
||||
return LmsApiService.uploadGradeCsv(courseId, formData).then(() => {
|
||||
dispatch(finishedCsvUpload());
|
||||
dispatch(uploadOverrideSuccess(courseId));
|
||||
}).catch((err) => {
|
||||
dispatch(uploadOverrideFailure(courseId, err));
|
||||
if (err.status === 200 && err.data.error_messages.length) {
|
||||
const { error_messages: errorMessages, saved, total } = err.data;
|
||||
return dispatch(csvUploadError({ errorMessages, saved, total }));
|
||||
}
|
||||
return dispatch(csvUploadError({ errorMessages: ['Unknown error.'] }));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const fetchBulkUpgradeHistory = courseId => (
|
||||
// todo add loading effect
|
||||
dispatch => LmsApiService.fetchGradeBulkOperationHistory(courseId).then(
|
||||
(response) => { dispatch(gotBulkHistory(response)); },
|
||||
).catch(() => dispatch(bulkHistoryError()))
|
||||
);
|
||||
|
||||
const updateGradesIfAssignmentGradeFiltersSet = (
|
||||
courseId,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
) => (dispatch, getState) => {
|
||||
const { filters } = getState();
|
||||
const hasAssignmentGradeFiltersSet = filters.assignmentGradeMax || filters.assignmentGradeMin;
|
||||
if (hasAssignmentGradeFiltersSet) {
|
||||
dispatch(fetchGrades(
|
||||
courseId,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
));
|
||||
}
|
||||
const overrideHistory = {
|
||||
error: createAction('overrideHistory/errorFetching'),
|
||||
received: createAction(
|
||||
'overrideHistory/received',
|
||||
(data) => ({
|
||||
payload: {
|
||||
overrideHistory: data.overrideHistory,
|
||||
currentEarnedAllOverride: data.currentEarnedAllOverride,
|
||||
currentPossibleAllOverride: data.currentPossibleAllOverride,
|
||||
currentEarnedGradedOverride: data.currentEarnedGradedOverride,
|
||||
currentPossibleGradedOverride: data.currentPossibleGradedOverride,
|
||||
originalGradeEarnedAll: data.originalGradeEarnedAll,
|
||||
originalGradePossibleAll: data.originalGradePossibleAll,
|
||||
originalGradeEarnedGraded: data.originalGradeEarnedGraded,
|
||||
originalGradePossibleGraded: data.originalGradePossibleGraded,
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
export {
|
||||
startedFetchingGrades,
|
||||
finishedFetchingGrades,
|
||||
errorFetchingGrades,
|
||||
gotGrades,
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
fetchPrevNextGrades,
|
||||
gradeUpdateRequest,
|
||||
gradeUpdateSuccess,
|
||||
gradeUpdateFailure,
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
filterAssignmentType,
|
||||
closeBanner,
|
||||
submitFileUploadFormData,
|
||||
fetchBulkUpgradeHistory,
|
||||
const toggleGradeFormat = createAction('toggleGradeFormat');
|
||||
|
||||
const update = {
|
||||
request: createAction('update/request'),
|
||||
success: createAction('update/success'),
|
||||
failure: createAction('update/failure', (courseId, error) => ({
|
||||
payload: { courseId, error },
|
||||
})),
|
||||
};
|
||||
|
||||
const uploadOverride = {
|
||||
success: createAction('uploadOverride/success'),
|
||||
failure: createAction('uploadOverride/failure', (courseId, error) => ({
|
||||
payload: { courseId, error },
|
||||
})),
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
banner: StrictDict(banner),
|
||||
bulkHistory: StrictDict(bulkHistory),
|
||||
csvUpload: StrictDict(csvUpload),
|
||||
doneViewingAssignment,
|
||||
fetchGradeOverrideHistory,
|
||||
updateGradesIfAssignmentGradeFiltersSet,
|
||||
downloadBulkGradesReport,
|
||||
downloadInterventionReport,
|
||||
};
|
||||
downloadReport: StrictDict(downloadReport),
|
||||
fetching: StrictDict(fetching),
|
||||
overrideHistory: StrictDict(overrideHistory),
|
||||
toggleGradeFormat,
|
||||
update: StrictDict(update),
|
||||
uploadOverride: StrictDict(uploadOverride),
|
||||
});
|
||||
|
||||
@@ -1,274 +1,115 @@
|
||||
import axios from 'axios';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
import actions, { dataKey } from './grades';
|
||||
import { testAction, testActionTypes } from './testUtils';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchGrades, fetchGradeOverrideHistory } from './grades';
|
||||
import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
GOT_GRADE_OVERRIDE_HISTORY,
|
||||
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
} from '../constants/actionTypes/grades';
|
||||
import GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG from '../constants/errors';
|
||||
import { sortAlphaAsc } from './utils';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
getAuthenticatedHttpClient.mockReturnValue(axios);
|
||||
axios.isAccessTokenExpired = jest.fn();
|
||||
axios.isAccessTokenExpired.mockReturnValue(false);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
describe('actions.grades', () => {
|
||||
describe('action types', () => {
|
||||
const actionTypes = [
|
||||
actions.banner.open,
|
||||
actions.banner.close,
|
||||
actions.bulkHistory.received,
|
||||
actions.bulkHistory.error,
|
||||
actions.csvUpload.started,
|
||||
actions.csvUpload.finished,
|
||||
actions.csvUpload.error,
|
||||
actions.doneViewingAssignment,
|
||||
actions.downloadReport.bulkGrades,
|
||||
actions.downloadReport.intervention,
|
||||
actions.fetching.started,
|
||||
actions.fetching.finished,
|
||||
actions.fetching.error,
|
||||
actions.fetching.received,
|
||||
actions.overrideHistory.error,
|
||||
actions.overrideHistory.received,
|
||||
actions.toggleGradeFormat,
|
||||
actions.update.request,
|
||||
actions.update.success,
|
||||
actions.update.failure,
|
||||
actions.uploadOverride.success,
|
||||
actions.uploadOverride.failure,
|
||||
].map(action => action.toString());
|
||||
testActionTypes(actionTypes, dataKey);
|
||||
});
|
||||
|
||||
describe('fetchGrades', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const expectedCohort = 1;
|
||||
const expectedTrack = 'verified';
|
||||
const expectedAssignmentType = 'Exam';
|
||||
const fetchGradesURL = `${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/?page_size=25&cohort_id=${expectedCohort}&enrollment_mode=${expectedTrack}&excluded_course_roles=all`;
|
||||
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,
|
||||
assignmentType: expectedAssignmentType,
|
||||
prev: responseData.previous,
|
||||
next: responseData.next,
|
||||
courseId,
|
||||
},
|
||||
{ type: FINISHED_FETCHING_GRADES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(fetchGradesURL)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchGrades(
|
||||
courseId,
|
||||
expectedCohort,
|
||||
expectedTrack,
|
||||
expectedAssignmentType,
|
||||
false,
|
||||
)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('actions provided', () => {
|
||||
describe('banner', () => {
|
||||
test('open action', () => testAction(actions.banner.open));
|
||||
test('close action', () => testAction(actions.banner.close));
|
||||
});
|
||||
|
||||
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,
|
||||
expectedAssignmentType,
|
||||
false,
|
||||
)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('bulkHistory', () => {
|
||||
test('received action', () => testAction(actions.bulkHistory.received));
|
||||
test('error action', () => testAction(actions.bulkHistory.error));
|
||||
});
|
||||
|
||||
it('dispatches success action on empty response after fetching grades', () => {
|
||||
const emptyResponseData = {
|
||||
next: responseData.next,
|
||||
previous: responseData.previous,
|
||||
results: [],
|
||||
};
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{
|
||||
type: GOT_GRADES,
|
||||
grades: [],
|
||||
cohort: expectedCohort,
|
||||
track: expectedTrack,
|
||||
assignmentType: expectedAssignmentType,
|
||||
prev: responseData.previous,
|
||||
next: responseData.next,
|
||||
courseId,
|
||||
},
|
||||
{ type: FINISHED_FETCHING_GRADES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(fetchGradesURL)
|
||||
.replyOnce(200, JSON.stringify(emptyResponseData));
|
||||
|
||||
return store.dispatch(fetchGrades(
|
||||
courseId,
|
||||
expectedCohort,
|
||||
expectedTrack,
|
||||
expectedAssignmentType,
|
||||
false,
|
||||
)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('csvUpload', () => {
|
||||
test('started action', () => testAction(actions.csvUpload.started));
|
||||
test('finished action', () => testAction(actions.csvUpload.finished));
|
||||
test('error action', () => testAction(actions.csvUpload.error));
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchGradeOverridHistory', () => {
|
||||
const subsectionId = 'subsectionId-11111';
|
||||
const userId = 'user-id-11111';
|
||||
|
||||
const fetchOverridesURL = `${LmsApiService.baseUrl}/api/grades/v1/subsection/${subsectionId}/?user_id=${userId}&history_record_limit=5`;
|
||||
|
||||
const originalGrade = {
|
||||
earned_all: 1.0,
|
||||
possible_all: 12.0,
|
||||
earned_graded: 3.0,
|
||||
possible_graded: 8.0,
|
||||
};
|
||||
|
||||
const override = {
|
||||
earned_all_override: 13.0,
|
||||
possible_all_override: 13.0,
|
||||
earned_graded_override: 10.0,
|
||||
possible_graded_override: 10.0,
|
||||
};
|
||||
|
||||
it('dispatches success action after successfully getting override info', () => {
|
||||
const responseData = {
|
||||
success: true,
|
||||
original_grade: originalGrade,
|
||||
history: [],
|
||||
override,
|
||||
};
|
||||
|
||||
axiosMock.onGet(fetchOverridesURL)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: GOT_GRADE_OVERRIDE_HISTORY,
|
||||
overrideHistory: [],
|
||||
currentEarnedAllOverride: override.earned_all_override,
|
||||
currentPossibleAllOverride: override.possible_all_override,
|
||||
currentEarnedGradedOverride: override.earned_graded_override,
|
||||
currentPossibleGradedOverride: override.possible_graded_override,
|
||||
originalGradeEarnedAll: originalGrade.earned_all,
|
||||
originalGradePossibleAll: originalGrade.possible_all,
|
||||
originalGradeEarnedGraded: originalGrade.earned_graded,
|
||||
originalGradePossibleGraded: originalGrade.possible_graded,
|
||||
},
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
return store.dispatch(fetchGradeOverrideHistory(subsectionId, userId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
test('doneViewingAssignment', () => testAction(actions.doneViewingAssignment));
|
||||
describe('downloadReport', () => {
|
||||
test('bulkGrades action', () => testAction(actions.downloadReport.bulkGrades));
|
||||
test('intervention action', () => testAction(actions.downloadReport.intervention));
|
||||
});
|
||||
|
||||
describe('dispatches failure action with expected message', () => {
|
||||
test('on failure response', () => {
|
||||
const responseData = {
|
||||
success: false,
|
||||
error_message: 'There was an error!!!!!!!!!',
|
||||
};
|
||||
|
||||
axiosMock.onGet(fetchOverridesURL).replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
const expectedActions = [{
|
||||
type: ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
errorMessage: responseData.error_message,
|
||||
}];
|
||||
const store = mockStore();
|
||||
|
||||
return store.dispatch(fetchGradeOverrideHistory(subsectionId, userId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
describe('fetching', () => {
|
||||
test('started action', () => testAction(actions.fetching.started));
|
||||
test('finished action', () => testAction(actions.fetching.finished));
|
||||
test('error action', () => testAction(actions.fetching.error));
|
||||
describe('received', () => {
|
||||
it('loads grades data from data', () => {
|
||||
const data = {
|
||||
grades: ['some', 'grades'],
|
||||
cohort: 2,
|
||||
track: 'summoners',
|
||||
assignmentType: 'potion',
|
||||
headings: ['H', 'E', 'a', 'd', 'Ing', 'sssss'],
|
||||
prev: 'prEEEV',
|
||||
next: 'NEEEExt',
|
||||
courseId: 'fake ID',
|
||||
totalUsersCount: 2,
|
||||
filteredUsersCount: 999,
|
||||
};
|
||||
testAction(actions.fetching.received, { ...data, other: 'fields' }, data);
|
||||
});
|
||||
});
|
||||
|
||||
test('on 500 error', () => {
|
||||
axiosMock.onGet(fetchOverridesURL).replyOnce(500);
|
||||
|
||||
const expectedActions = [{
|
||||
type: ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
errorMessage: GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG,
|
||||
}];
|
||||
const store = mockStore();
|
||||
|
||||
return store.dispatch(fetchGradeOverrideHistory(subsectionId, userId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('overrideHistory', () => {
|
||||
test('error action', () => testAction(actions.overrideHistory.error));
|
||||
describe('received', () => {
|
||||
it('loads override history from data', () => {
|
||||
const data = {
|
||||
overrideHistory: 'some History',
|
||||
currentEarnedAllOverride: 123,
|
||||
currentPossibleAllOverride: 243,
|
||||
currentEarnedGradedOverride: 1236,
|
||||
currentPossibleGradedOverride: 52,
|
||||
originalGradeEarnedAll: 323,
|
||||
originalGradePossibleAll: 6223,
|
||||
originalGradeEarnedGraded: 1232,
|
||||
originalGradePossibleGraded: 512,
|
||||
};
|
||||
testAction(actions.overrideHistory.received, { ...data, other: 'fields' }, data);
|
||||
});
|
||||
});
|
||||
});
|
||||
test('toggleGradeFormat', () => testAction(actions.toggleGradeFormat));
|
||||
describe('update', () => {
|
||||
const courseId = 'fake ID';
|
||||
const error = 'Try Again??';
|
||||
test('request action', () => testAction(actions.update.request));
|
||||
test('success action', () => testAction(actions.update.success));
|
||||
test('failure action', () => testAction(
|
||||
actions.update.failure,
|
||||
[courseId, error],
|
||||
{ courseId, error },
|
||||
));
|
||||
});
|
||||
describe('uploadOverride', () => {
|
||||
const courseId = 'fake ID';
|
||||
const error = 'Try Again??';
|
||||
test('success action', () => testAction(actions.uploadOverride.success));
|
||||
test('failure action', () => testAction(
|
||||
actions.uploadOverride.failure,
|
||||
[courseId, error],
|
||||
{ courseId, error },
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
19
src/data/actions/index.js
Normal file
19
src/data/actions/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
import cohorts from './cohorts';
|
||||
import config from './config';
|
||||
import filters from './filters';
|
||||
import grades from './grades';
|
||||
import roles from './roles';
|
||||
import tracks from './tracks';
|
||||
|
||||
export default StrictDict({
|
||||
assignmentTypes,
|
||||
cohorts,
|
||||
config,
|
||||
filters,
|
||||
grades,
|
||||
roles,
|
||||
tracks,
|
||||
});
|
||||
@@ -1,46 +1,14 @@
|
||||
import filtersSelectors from 'data/selectors/filters';
|
||||
import {
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES,
|
||||
} from '../constants/actionTypes/roles';
|
||||
import { fetchGrades } from './grades';
|
||||
import { fetchTracks } from './tracks';
|
||||
import { fetchCohorts } from './cohorts';
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import { StrictDict } from 'utils';
|
||||
import { createActionFactory } from './utils';
|
||||
|
||||
const { allFilters } = filtersSelectors;
|
||||
export const dataKey = 'roles';
|
||||
const createAction = createActionFactory(dataKey);
|
||||
|
||||
const allowedRoles = ['staff', 'instructor', 'support'];
|
||||
|
||||
const gotRoles = (canUserViewGradebook, courseId) => ({
|
||||
type: GOT_ROLES,
|
||||
canUserViewGradebook,
|
||||
courseId,
|
||||
});
|
||||
const errorFetchingRoles = () => ({ type: ERROR_FETCHING_ROLES });
|
||||
|
||||
const getRoles = courseId => (
|
||||
(dispatch, getState) => LmsApiService.fetchUserRoles(courseId)
|
||||
.then(response => response.data)
|
||||
.then((response) => {
|
||||
const canUserViewGradebook = response.is_staff
|
||||
|| (response.roles.some(role => (role.course_id === courseId)
|
||||
&& allowedRoles.includes(role.role)));
|
||||
dispatch(gotRoles(canUserViewGradebook, courseId));
|
||||
const { cohort, track, assignmentType } = allFilters(getState());
|
||||
if (canUserViewGradebook) {
|
||||
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
||||
dispatch(fetchTracks(courseId));
|
||||
dispatch(fetchCohorts(courseId));
|
||||
dispatch(fetchAssignmentTypes(courseId));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingRoles());
|
||||
}));
|
||||
|
||||
export {
|
||||
getRoles,
|
||||
errorFetchingRoles,
|
||||
const fetching = {
|
||||
error: createAction('fetching/error'),
|
||||
received: createAction('fetching/received'),
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
fetching: StrictDict(fetching),
|
||||
});
|
||||
|
||||
@@ -1,166 +1,18 @@
|
||||
import axios from 'axios';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import actions, { dataKey } from './roles';
|
||||
import { testAction, testActionTypes } from './testUtils';
|
||||
|
||||
import { configuration } from '../../config';
|
||||
import { getRoles } from './roles';
|
||||
import {
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES,
|
||||
} from '../constants/actionTypes/roles';
|
||||
import { STARTED_FETCHING_GRADES } from '../constants/actionTypes/grades';
|
||||
import { STARTED_FETCHING_TRACKS } from '../constants/actionTypes/tracks';
|
||||
import { STARTED_FETCHING_COHORTS } from '../constants/actionTypes/cohorts';
|
||||
import { STARTED_FETCHING_ASSIGNMENT_TYPES } from '../constants/actionTypes/assignmentTypes';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
getAuthenticatedHttpClient.mockReturnValue(axios);
|
||||
axios.isAccessTokenExpired = jest.fn();
|
||||
axios.isAccessTokenExpired.mockReturnValue(false);
|
||||
|
||||
const course1Id = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const course2Id = 'course-v1:edX+DemoX+Demo_Course_2';
|
||||
const rolesUrl = `${configuration.LMS_BASE_URL}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(course1Id)}`;
|
||||
|
||||
function makeRoleListObj(roles, isGlobalStaff) {
|
||||
return {
|
||||
roles,
|
||||
is_staff: isGlobalStaff,
|
||||
};
|
||||
}
|
||||
function makeRoleObj(courseId, role) {
|
||||
return {
|
||||
course_id: courseId,
|
||||
role,
|
||||
};
|
||||
}
|
||||
|
||||
const course1StaffRole = makeRoleObj(course1Id, 'staff');
|
||||
const course1DummyRole = makeRoleObj(course1Id, 'dummy');
|
||||
const course2StaffRole = makeRoleObj(course2Id, 'staff');
|
||||
const course2DummyRole = makeRoleObj(course2Id, 'dummy');
|
||||
const urlParams = { cohort: null, track: null };
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
describe('actions.roles', () => {
|
||||
describe('action types', () => {
|
||||
const actionTypes = [
|
||||
actions.fetching.error,
|
||||
actions.fetching.received,
|
||||
].map(action => action.toString());
|
||||
testActionTypes(actionTypes, dataKey);
|
||||
});
|
||||
|
||||
describe('getRoles', () => {
|
||||
it('dispatches got_roles action and subsequent actions after fetching role that allows gradebook', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: true, courseId: course1Id },
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([course1StaffRole, course2DummyRole], false)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and other actions after fetching irrelevent roles but user is global staff', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: true, courseId: course1Id },
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([course1DummyRole, course2DummyRole], true)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and no other actions after fetching role that disallows gradebook', () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: GOT_ROLES, canUserViewGradebook: false, courseId: course1Id,
|
||||
},
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([course1DummyRole, course2StaffRole], false)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and no other actions after fetching empty roles', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: false, courseId: course1Id },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([], false)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and other actions after fetching empty roles but user is global staff', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: true, courseId: course1Id },
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([], true)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches error action after getting an error when trying to get roles', () => {
|
||||
const expectedActions = [
|
||||
{ type: ERROR_FETCHING_ROLES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl).replyOnce(400);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('actions provided', () => {
|
||||
describe('fecthing actions', () => {
|
||||
test('error action', () => testAction(actions.fetching.error));
|
||||
test('received action', () => testAction(actions.fetching.received));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
48
src/data/actions/testUtils.js
Normal file
48
src/data/actions/testUtils.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* testActionTypes(actionTypes, dataKey)
|
||||
* Takes a list of actionTypes and a module dataKey, and verifies that
|
||||
* * all actionTypes are unique
|
||||
* * all actionTypes begin with the dataKey
|
||||
* @param {string[]} actionTypes - list of action types
|
||||
* @param {string} dataKey - module data key
|
||||
*/
|
||||
export const testActionTypes = (actionTypes, dataKey) => {
|
||||
test('all types are unique', () => {
|
||||
expect(actionTypes.length).toEqual((new Set(actionTypes)).size);
|
||||
});
|
||||
test('all types begin with the module dataKey', () => {
|
||||
actionTypes.forEach(type => {
|
||||
expect(type.startsWith(dataKey)).toEqual(true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* testAction(action, args, expectedPayload)
|
||||
* Multi-purpose action creator test function.
|
||||
* If args/expectedPayload are passed, verifies that it produces the expected output when called
|
||||
* with the given args.
|
||||
* If none are passed, (for action creators with basic definition) it tests against a default
|
||||
* test payload.
|
||||
* @param {object} action - action creator object/method
|
||||
* @param {[object]} args - optional payload argument
|
||||
* @param {[object]} expectedPayload - optional expected payload.
|
||||
*/
|
||||
export const testAction = (action, args, expectedPayload) => {
|
||||
const type = action.toString();
|
||||
if (args) {
|
||||
if (Array.isArray(args)) {
|
||||
expect(action(...args)).toEqual({ type, payload: expectedPayload });
|
||||
} else {
|
||||
expect(action(args)).toEqual({ type, payload: expectedPayload });
|
||||
}
|
||||
} else {
|
||||
const payload = { test: 'PAYload' };
|
||||
expect(action(payload)).toEqual({ type, payload });
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
testAction,
|
||||
testActionTypes,
|
||||
};
|
||||
@@ -1,39 +1,15 @@
|
||||
/* eslint-disable camelcase */
|
||||
import tracksSelectors from 'data/selectors/tracks';
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
import { fetchBulkUpgradeHistory } from './grades';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import { StrictDict } from 'utils';
|
||||
import { createActionFactory } from './utils';
|
||||
|
||||
const { hasMastersTrack } = tracksSelectors;
|
||||
export const dataKey = 'tracks';
|
||||
const createAction = createActionFactory(dataKey);
|
||||
|
||||
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(({ data }) => data)
|
||||
.then(({ course_modes }) => {
|
||||
dispatch(gotTracks(course_modes));
|
||||
if (hasMastersTrack(course_modes)) {
|
||||
dispatch(fetchBulkUpgradeHistory(courseId));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingTracks());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
fetchTracks,
|
||||
startedFetchingTracks,
|
||||
gotTracks,
|
||||
errorFetchingTracks,
|
||||
const fetching = {
|
||||
started: createAction('fetching/started'),
|
||||
error: createAction('fetching/error'),
|
||||
received: createAction('fetching/received'),
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
fetching: StrictDict(fetching),
|
||||
});
|
||||
|
||||
@@ -1,87 +1,20 @@
|
||||
import axios from 'axios';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
import actions, { dataKey } from './tracks';
|
||||
import { testAction, testActionTypes } from './testUtils';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
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]);
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
getAuthenticatedHttpClient.mockReturnValue(axios);
|
||||
axios.isAccessTokenExpired = jest.fn();
|
||||
axios.isAccessTokenExpired.mockReturnValue(false);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
describe('actions.tracks', () => {
|
||||
describe('action types', () => {
|
||||
const actionTypes = [
|
||||
actions.fetching.error,
|
||||
actions.fetching.started,
|
||||
actions.fetching.received,
|
||||
].map(action => action.toString());
|
||||
testActionTypes(actionTypes, dataKey);
|
||||
});
|
||||
|
||||
describe('fetchTracks', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const trackUrl = `${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}?include_expired=1`;
|
||||
|
||||
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(trackUrl)
|
||||
.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(trackUrl)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchTracks(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('actions provided', () => {
|
||||
describe('fecthing actions', () => {
|
||||
test('error action', () => testAction(actions.fetching.error));
|
||||
test('started action', () => testAction(actions.fetching.started));
|
||||
test('received action', () => testAction(actions.fetching.received));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
const formatDateForDisplay = (inputDate) => {
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
@@ -26,4 +28,12 @@ const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
export { sortAlphaAsc, formatDateForDisplay };
|
||||
const createActionFactory = (dataKey) => (actionKey, ...args) => (
|
||||
createAction(`${dataKey}/${actionKey}`, ...args)
|
||||
);
|
||||
|
||||
export {
|
||||
createActionFactory,
|
||||
sortAlphaAsc,
|
||||
formatDateForDisplay,
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
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,3 +0,0 @@
|
||||
const GOT_BULK_MANAGEMENT_CONFIG = 'GOT_BULK_MANAGEMENT_CONFIG';
|
||||
|
||||
export default GOT_BULK_MANAGEMENT_CONFIG;
|
||||
@@ -1,10 +0,0 @@
|
||||
const INITIALIZE_FILTERS = 'INITIALIZE_FILTERS';
|
||||
const RESET_FILTERS = 'RESET_FILTERS';
|
||||
const UPDATE_ASSIGNMENT_FILTER = 'UPDATE_ASSIGNMENT_FILTER';
|
||||
const UPDATE_ASSIGNMENT_LIMITS = 'UPDATE_ASSIGNMENT_LIMITS';
|
||||
const UPDATE_COURSE_GRADE_LIMITS = 'UPDATE_COURSE_GRADE_LIMITS';
|
||||
const UPDATE_INCLUDE_COURSE_ROLE_MEMBERS = 'UPDATE_INCLUDE_COURSE_ROLE_MEMBERS';
|
||||
export {
|
||||
INITIALIZE_FILTERS, RESET_FILTERS, UPDATE_ASSIGNMENT_FILTER,
|
||||
UPDATE_ASSIGNMENT_LIMITS, UPDATE_COURSE_GRADE_LIMITS, UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
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 DONE_VIEWING_ASSIGNMENT = 'DONE_VIEWING_ASSIGNMENT';
|
||||
const GOT_GRADE_OVERRIDE_HISTORY = 'GOT_GRADE_OVERRIDE_HISTORY';
|
||||
const ERROR_FETCHING_GRADE_OVERRIDE_HISTORY = 'ERROR_FETCHING_GRADE_OVERRIDE_HISTORY';
|
||||
|
||||
const FILTER_SELECTED = 'FILTER_SELECTED';
|
||||
const GRADE_OVERRIDE = 'GRADE_OVERRIDE';
|
||||
const REPORT_DOWNLOADED = 'REPORT_DOWNLOADED';
|
||||
const UPLOAD_OVERRIDE = 'UPLOAD_OVERRIDE';
|
||||
const UPLOAD_OVERRIDE_ERROR = 'UPLOAD_OVERRIDE_ERROR';
|
||||
|
||||
const GRADE_UPDATE_REQUEST = 'GRADE_UPDATE_REQUEST';
|
||||
const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
|
||||
const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
|
||||
|
||||
const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT';
|
||||
const FILTER_BY_ASSIGNMENT_TYPE = 'FILTER_BY_ASSIGNMENT_TYPE';
|
||||
const CLOSE_BANNER = 'CLOSE_BANNER';
|
||||
const OPEN_BANNER = 'OPEN_BANNER';
|
||||
|
||||
const START_UPLOAD = 'START_UPLOAD';
|
||||
const UPLOAD_COMPLETE = 'UPLOAD_COMPLETE';
|
||||
const UPLOAD_ERR = 'UPLOAD_ERR';
|
||||
const GOT_BULK_HISTORY = 'GOT_BULK_HISTORY';
|
||||
const BULK_HISTORY_ERR = 'BULK_HISTORY_ERR';
|
||||
const BULK_GRADE_REPORT_DOWNLOADED = 'BULK_GRADE_REPORT_DOWNLOADED';
|
||||
const INTERVENTION_REPORT_DOWNLOADED = 'INTERVENTION_REPORT_DOWNLOADED';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_GRADES,
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
GRADE_UPDATE_REQUEST,
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_BY_ASSIGNMENT_TYPE,
|
||||
OPEN_BANNER,
|
||||
CLOSE_BANNER,
|
||||
START_UPLOAD,
|
||||
UPLOAD_COMPLETE,
|
||||
UPLOAD_ERR,
|
||||
GOT_BULK_HISTORY,
|
||||
BULK_HISTORY_ERR,
|
||||
DONE_VIEWING_ASSIGNMENT,
|
||||
GOT_GRADE_OVERRIDE_HISTORY,
|
||||
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
FILTER_SELECTED,
|
||||
GRADE_OVERRIDE,
|
||||
REPORT_DOWNLOADED,
|
||||
UPLOAD_OVERRIDE,
|
||||
UPLOAD_OVERRIDE_ERROR,
|
||||
BULK_GRADE_REPORT_DOWNLOADED,
|
||||
INTERVENTION_REPORT_DOWNLOADED,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
const GOT_ROLES = 'GOT_ROLES';
|
||||
const ERROR_FETCHING_ROLES = 'ERROR_FETCHING_ROLES';
|
||||
|
||||
export {
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ASSIGNMENT_TYPES,
|
||||
GOT_ARE_GRADES_FROZEN,
|
||||
} from '../constants/actionTypes/assignmentTypes';
|
||||
import actions from '../actions/assignmentTypes';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
@@ -11,30 +6,30 @@ const initialState = {
|
||||
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:
|
||||
const assignmentTypes = (state = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case actions.fetching.started.toString():
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
};
|
||||
case ERROR_FETCHING_ASSIGNMENT_TYPES:
|
||||
case actions.fetching.received.toString():
|
||||
return {
|
||||
...state,
|
||||
results: payload,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
case actions.fetching.error.toString():
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
case GOT_ARE_GRADES_FROZEN:
|
||||
case actions.gotGradesFrozen.toString():
|
||||
return {
|
||||
...state,
|
||||
areGradesFrozen: action.areGradesFrozen,
|
||||
areGradesFrozen: payload,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
@@ -43,4 +38,5 @@ const assignmentTypes = (state = initialState, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState };
|
||||
export default assignmentTypes;
|
||||
|
||||
@@ -1,68 +1,71 @@
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
import {
|
||||
STARTED_FETCHING_ASSIGNMENT_TYPES,
|
||||
ERROR_FETCHING_ASSIGNMENT_TYPES,
|
||||
GOT_ASSIGNMENT_TYPES,
|
||||
GOT_ARE_GRADES_FROZEN,
|
||||
} from '../constants/actionTypes/assignmentTypes';
|
||||
import assignmentTypes, { initialState } from './assignmentTypes';
|
||||
import actions from '../actions/assignmentTypes';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
const testingState = {
|
||||
...initialState,
|
||||
results: ['Exam', 'Homework'],
|
||||
arbitraryField: 'arbitrary',
|
||||
};
|
||||
|
||||
const assignmentTypesData = ['Exam', 'Homework'];
|
||||
|
||||
describe('assignmentTypes reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(assignmentTypes(undefined, {})).toEqual(initialState);
|
||||
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);
|
||||
describe('handling actions.fetching.started', () => {
|
||||
it('sets startedFetching=true', () => {
|
||||
const expected = {
|
||||
...testingState,
|
||||
startedFetching: true,
|
||||
};
|
||||
expect(
|
||||
assignmentTypes(testingState, actions.fetching.started()),
|
||||
).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);
|
||||
describe('handling actions.fetching.received', () => {
|
||||
it('loads the results and sets finishedFetching=true and errorFetching=false', () => {
|
||||
const expectedResults = ['Exam'];
|
||||
const expected = {
|
||||
...testingState,
|
||||
results: expectedResults,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(
|
||||
assignmentTypes(testingState, actions.fetching.received(expectedResults)),
|
||||
).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);
|
||||
describe('handling actions.fetching.error', () => {
|
||||
it('sets errorFetching=true and finishedFetching=true', () => {
|
||||
const expected = {
|
||||
...testingState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(
|
||||
assignmentTypes(testingState, actions.fetching.error()),
|
||||
).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);
|
||||
describe('handling actions.gotGradesFrozen', () => {
|
||||
it('loads areGradesFrozen and sets errorFetching=false and finishedFetching=true', () => {
|
||||
const expectedAreGradesFrozen = true;
|
||||
const expected = {
|
||||
...testingState,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
areGradesFrozen: expectedAreGradesFrozen,
|
||||
};
|
||||
expect(
|
||||
assignmentTypes(testingState, actions.gotGradesFrozen(expectedAreGradesFrozen)),
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
import actions from '../actions/cohorts';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
@@ -12,19 +8,19 @@ const initialState = {
|
||||
|
||||
const cohorts = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_COHORTS:
|
||||
return {
|
||||
...state,
|
||||
results: action.cohorts,
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
};
|
||||
case STARTED_FETCHING_COHORTS:
|
||||
case actions.fetching.started.toString():
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
};
|
||||
case ERROR_FETCHING_COHORTS:
|
||||
case actions.fetching.received.toString():
|
||||
return {
|
||||
...state,
|
||||
results: action.payload,
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
};
|
||||
case actions.fetching.error.toString():
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
@@ -35,4 +31,5 @@ const cohorts = (state = initialState, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState };
|
||||
export default cohorts;
|
||||
|
||||
@@ -1,70 +1,61 @@
|
||||
import cohorts from './cohorts';
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
import cohorts, { initialState } from './cohorts';
|
||||
import actions from '../actions/cohorts';
|
||||
|
||||
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,
|
||||
}];
|
||||
{ arbitraryCohortField: 'some data' },
|
||||
{ anotherArbitraryCohortField: 'some data' },
|
||||
];
|
||||
|
||||
const testingState = {
|
||||
...initialState,
|
||||
results: cohortsData,
|
||||
arbitraryField: 'arbitrary',
|
||||
};
|
||||
|
||||
describe('cohorts reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(cohorts(undefined, {})).toEqual(initialState);
|
||||
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);
|
||||
describe('handling actions.fetching.started', () => {
|
||||
it('sets startedFetching=true', () => {
|
||||
const expected = {
|
||||
...testingState,
|
||||
startedFetching: true,
|
||||
};
|
||||
expect(
|
||||
cohorts(testingState, actions.fetching.started()),
|
||||
).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);
|
||||
describe('handling actions.fetching.received', () => {
|
||||
it('loads results and sets finishedFetching=true and errorFetching=false', () => {
|
||||
const newCohortData = [{ newResultFields: 'recieved data' }];
|
||||
const expected = {
|
||||
...testingState,
|
||||
results: newCohortData,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(
|
||||
cohorts(testingState, actions.fetching.received(newCohortData)),
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates fetch cohorts failure state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(cohorts(undefined, {
|
||||
type: ERROR_FETCHING_COHORTS,
|
||||
})).toEqual(expected);
|
||||
describe('handling actions.fetching.error', () => {
|
||||
it('sets finishedFetching=true and errorFetching=true', () => {
|
||||
const expected = {
|
||||
...testingState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(
|
||||
cohorts(testingState, actions.fetching.error()),
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import GOT_BULK_MANAGEMENT_CONFIG from '../constants/actionTypes/config';
|
||||
import actions from '../actions/config';
|
||||
|
||||
const reducer = (state = {}, action) => {
|
||||
const initialState = {};
|
||||
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_BULK_MANAGEMENT_CONFIG:
|
||||
case actions.gotBulkManagementConfig.toString():
|
||||
return {
|
||||
...state,
|
||||
bulkManagementAvailable: action.data,
|
||||
bulkManagementAvailable: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState };
|
||||
export default reducer;
|
||||
|
||||
25
src/data/reducers/config.test.js
Normal file
25
src/data/reducers/config.test.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import config, { initialState } from './config';
|
||||
import actions from '../actions/config';
|
||||
|
||||
const testingState = {
|
||||
abitraryField: 'abitrary',
|
||||
};
|
||||
|
||||
describe('config reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(
|
||||
config(undefined, {}),
|
||||
).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('loads bulkManagementAvailable from payload', () => {
|
||||
const expectedBulkManagementAvailable = true;
|
||||
const expected = {
|
||||
...testingState,
|
||||
bulkManagementAvailable: expectedBulkManagementAvailable,
|
||||
};
|
||||
expect(
|
||||
config(testingState, actions.gotBulkManagementConfig(expectedBulkManagementAvailable)),
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,86 +1,83 @@
|
||||
import filterSelectors from 'data/selectors/filters';
|
||||
import { GOT_GRADES, FILTER_BY_ASSIGNMENT_TYPE } from '../constants/actionTypes/grades';
|
||||
import {
|
||||
INITIALIZE_FILTERS,
|
||||
UPDATE_ASSIGNMENT_FILTER,
|
||||
UPDATE_ASSIGNMENT_LIMITS,
|
||||
UPDATE_COURSE_GRADE_LIMITS,
|
||||
RESET_FILTERS,
|
||||
UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
|
||||
} from '../constants/actionTypes/filters';
|
||||
import selectors from 'data/selectors';
|
||||
import actions from '../actions/filters';
|
||||
import gradeActions from '../actions/grades';
|
||||
import initialFilters from '../constants/filters';
|
||||
|
||||
const { getAssignmentsFromResultsSubstate, chooseRelevantAssignmentData } = filterSelectors;
|
||||
const initialState = {};
|
||||
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case FILTER_BY_ASSIGNMENT_TYPE:
|
||||
const reducer = (state = initialState, { type: actionType, payload }) => {
|
||||
switch (actionType) {
|
||||
case actions.initialize.toString():
|
||||
return {
|
||||
...state,
|
||||
assignmentType: action.filterType,
|
||||
...payload,
|
||||
};
|
||||
case actions.reset.toString(): {
|
||||
const result = { ...state };
|
||||
payload.forEach((filterName) => {
|
||||
result[filterName] = initialFilters[filterName];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
case actions.update.assignment.toString():
|
||||
return {
|
||||
...state,
|
||||
assignment: payload,
|
||||
};
|
||||
case actions.update.assignmentLimits.toString():
|
||||
return {
|
||||
...state,
|
||||
assignmentGradeMin: payload.minGrade,
|
||||
assignmentGradeMax: payload.maxGrade,
|
||||
};
|
||||
case actions.update.assignmentType.toString():
|
||||
return {
|
||||
...state,
|
||||
assignmentType: payload,
|
||||
assignment: (
|
||||
action.filterType !== ''
|
||||
&& (state.assignment || {}).type !== action.filterType)
|
||||
? '' : state.assignment,
|
||||
(
|
||||
payload !== ''
|
||||
&& (state.assignment || {}).type !== payload
|
||||
) ? '' : state.assignment
|
||||
),
|
||||
};
|
||||
case INITIALIZE_FILTERS:
|
||||
case actions.update.courseGradeLimits.toString():
|
||||
return {
|
||||
...state,
|
||||
...action.data,
|
||||
courseGradeMin: payload.courseGradeMin,
|
||||
courseGradeMax: payload.courseGradeMax,
|
||||
};
|
||||
case GOT_GRADES: {
|
||||
case actions.update.includeCourseRoleMembers.toString():
|
||||
return {
|
||||
...state,
|
||||
includeCourseRoleMembers: payload,
|
||||
};
|
||||
case gradeActions.fetching.received.toString(): {
|
||||
const { assignment } = state;
|
||||
const { id, type } = assignment || {};
|
||||
if (!type) {
|
||||
const relevantAssignment = getAssignmentsFromResultsSubstate(action.grades)
|
||||
.map(chooseRelevantAssignmentData)
|
||||
.find(assig => assig.id === id);
|
||||
if (id && !type) {
|
||||
const { relevantAssignmentDataFromResults } = selectors.filters;
|
||||
const relevantAssignment = relevantAssignmentDataFromResults(
|
||||
payload.grades,
|
||||
id,
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
track: action.track,
|
||||
cohort: action.cohort,
|
||||
track: payload.track,
|
||||
cohort: payload.cohort,
|
||||
assignment: relevantAssignment,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
track: action.track,
|
||||
cohort: action.cohort,
|
||||
track: payload.track,
|
||||
cohort: payload.cohort,
|
||||
};
|
||||
}
|
||||
case RESET_FILTERS: {
|
||||
const result = { ...state };
|
||||
action.filterNames.forEach((filterName) => {
|
||||
result[filterName] = initialFilters[filterName];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
case UPDATE_ASSIGNMENT_FILTER:
|
||||
return {
|
||||
...state,
|
||||
assignment: action.data,
|
||||
};
|
||||
case UPDATE_ASSIGNMENT_LIMITS:
|
||||
return {
|
||||
...state,
|
||||
assignmentGradeMin: action.data.minGrade,
|
||||
assignmentGradeMax: action.data.maxGrade,
|
||||
};
|
||||
case UPDATE_COURSE_GRADE_LIMITS:
|
||||
return {
|
||||
...state,
|
||||
courseGradeMin: action.data.courseGradeMin,
|
||||
courseGradeMax: action.data.courseGradeMax,
|
||||
};
|
||||
case UPDATE_INCLUDE_COURSE_ROLE_MEMBERS:
|
||||
return {
|
||||
...state,
|
||||
includeCourseRoleMembers: action.data.includeCourseRoleMembers,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState };
|
||||
export default reducer;
|
||||
|
||||
201
src/data/reducers/filters.test.js
Normal file
201
src/data/reducers/filters.test.js
Normal file
@@ -0,0 +1,201 @@
|
||||
import selectors from 'data/selectors';
|
||||
import filter, { initialState } from './filters';
|
||||
import actions from '../actions/filters';
|
||||
import gradeActions from '../actions/grades';
|
||||
import initialFilters from '../constants/filters';
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
filters: {
|
||||
relevantAssignmentDataFromResults: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const expectedFilterType = 'homework';
|
||||
const expectedAssignmentId = 'assignment 1';
|
||||
const expectedAssignment = {
|
||||
type: expectedFilterType,
|
||||
id: expectedAssignmentId,
|
||||
};
|
||||
const testingState = {
|
||||
...initialState,
|
||||
arbitraryField: 'arbirary',
|
||||
assignmentType: 'exam',
|
||||
assignment: { ...expectedAssignment },
|
||||
};
|
||||
|
||||
describe('filter reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(
|
||||
filter(undefined, {}),
|
||||
).toEqual(initialState);
|
||||
});
|
||||
|
||||
describe('handling actions.initialize', () => {
|
||||
it('replaces all passed fields', () => {
|
||||
const payload = {
|
||||
assignment: { ...expectedAssignment },
|
||||
assignmentType: expectedFilterType,
|
||||
track: 'verified',
|
||||
cohort: 5,
|
||||
assignmentGradeMin: 50,
|
||||
assignmentGradeMax: 100,
|
||||
courseGradeMin: 50,
|
||||
courseGradeMax: 100,
|
||||
includeCourseRoleMembers: true,
|
||||
};
|
||||
const action = { type: actions.initialize.toString(), payload };
|
||||
expect(filter(testingState, action)).toEqual({ ...testingState, ...payload });
|
||||
});
|
||||
it('only replaces passed fields', () => {
|
||||
const payload = { otherField: 'some data' };
|
||||
const action = { type: actions.initialize.toString(), payload };
|
||||
expect(filter(testingState, action)).toEqual({ ...testingState, ...payload });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling actions.reset', () => {
|
||||
it('resets the all attribute existed in filter to initial filter', () => {
|
||||
expect(
|
||||
filter(testingState, actions.reset(Object.keys(initialFilters))),
|
||||
).toEqual({ ...testingState, ...initialFilters });
|
||||
});
|
||||
it('only resets keys passed in the action', () => {
|
||||
const payload = ['assignment', 'assignmentType'];
|
||||
expect(filter(testingState, actions.reset(payload))).toEqual({
|
||||
...testingState,
|
||||
[payload[0]]: initialFilters[payload[0]],
|
||||
[payload[1]]: initialFilters[payload[1]],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle actions.update.assignment', () => {
|
||||
it('loads assignment from payload', () => {
|
||||
expect(
|
||||
filter(testingState, actions.update.assignment(expectedAssignment)),
|
||||
).toEqual({ ...testingState, assignment: expectedAssignment });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle actions.update.assignmentLimits', () => {
|
||||
it('loads assignmentGrade[Min/Max] from payload [min/max]grade', () => {
|
||||
const expectedMinGrade = 50;
|
||||
const expectedMaxGrade = 100;
|
||||
expect(
|
||||
filter(testingState, actions.update.assignmentLimits({
|
||||
minGrade: expectedMinGrade,
|
||||
maxGrade: expectedMaxGrade,
|
||||
})),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
assignmentGradeMin: expectedMinGrade,
|
||||
assignmentGradeMax: expectedMaxGrade,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle actions.update.assignmentType', () => {
|
||||
const action = actions.update.assignmentType;
|
||||
describe('new non-empty type', () => {
|
||||
const newType = 'new ASsignment TYpe';
|
||||
it('loads assignmentType and clears assignment', () => {
|
||||
expect(
|
||||
filter(testingState, action(newType)),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
assignmentType: newType,
|
||||
assignment: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('empty string type', () => {
|
||||
it('does not clear assignment if the type is empty', () => {
|
||||
expect(
|
||||
filter(testingState, action('')),
|
||||
).toEqual({ ...testingState, assignmentType: '' });
|
||||
});
|
||||
});
|
||||
describe('matching type', () => {
|
||||
it('does not clear the assignment if the type still matches the assignment', () => {
|
||||
expect(
|
||||
filter(testingState, action(testingState.assignment.type)),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
assignmentType: testingState.assignment.type,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling actions.update.courseGradeLimits', () => {
|
||||
it('updates courseGrade[Min/Max]', () => {
|
||||
const payload = {
|
||||
courseGradeMin: 20,
|
||||
courseGradeMax: 70,
|
||||
};
|
||||
expect(
|
||||
filter(initialState, actions.update.courseGradeLimits(payload)),
|
||||
).toEqual({ ...initialState, ...payload });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling actions.update.includeCourseRoleMembers', () => {
|
||||
it('updates includeCourseRoleMembers from payload', () => {
|
||||
const includeCourseRoleMembers = true;
|
||||
expect(
|
||||
filter(initialState, actions.update.includeCourseRoleMembers(includeCourseRoleMembers)),
|
||||
).toEqual({ ...initialState, includeCourseRoleMembers });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling gradeActions.fetching.received', () => {
|
||||
const mockSelector = (val) => {
|
||||
selectors.filters.relevantAssignmentDataFromResults.mockImplementation(
|
||||
(...args) => ({ args, val }),
|
||||
);
|
||||
};
|
||||
const assignmentId = 'fake ID';
|
||||
const action = gradeActions.fetching.received;
|
||||
const payload = {
|
||||
cohort: 'aCohoRT',
|
||||
track: 'ATRacK',
|
||||
grades: 'someGrades',
|
||||
};
|
||||
const relevantAssignment = { relevant: 'assignment' };
|
||||
describe('with non-typed assignment filter', () => {
|
||||
const state = { ...testingState, assignment: { id: assignmentId } };
|
||||
it('loads relevant assignment data by id with track and cohort from payload', () => {
|
||||
mockSelector(relevantAssignment);
|
||||
expect(filter(state, action(payload))).toEqual({
|
||||
...state,
|
||||
cohort: payload.cohort,
|
||||
track: payload.track,
|
||||
assignment: { args: [payload.grades, assignmentId], val: relevantAssignment },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('with empty assignment filter', () => {
|
||||
const state = { ...testingState, assignment: '' };
|
||||
it('loads cohort and track from payload', () => {
|
||||
expect(filter(state, action(payload))).toEqual({
|
||||
...state,
|
||||
cohort: payload.cohort,
|
||||
track: payload.track,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('with typed assignment filter', () => {
|
||||
const state = { ...testingState, assignment: { id: assignmentId, type: 'homework' } };
|
||||
it('loads cohort and track from payload', () => {
|
||||
expect(filter(state, action(payload))).toEqual({
|
||||
...state,
|
||||
cohort: payload.cohort,
|
||||
track: payload.track,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,5 @@
|
||||
import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_BY_ASSIGNMENT_TYPE,
|
||||
OPEN_BANNER,
|
||||
CLOSE_BANNER,
|
||||
START_UPLOAD,
|
||||
UPLOAD_COMPLETE,
|
||||
UPLOAD_ERR,
|
||||
GOT_BULK_HISTORY,
|
||||
DONE_VIEWING_ASSIGNMENT,
|
||||
GOT_GRADE_OVERRIDE_HISTORY,
|
||||
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
} from '../constants/actionTypes/grades';
|
||||
import actions from '../actions/grades';
|
||||
import filterActions from '../actions/filters';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
@@ -41,23 +27,53 @@ const initialState = {
|
||||
filteredUsersCount: 0,
|
||||
};
|
||||
|
||||
const grades = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_GRADES:
|
||||
const grades = (state = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case actions.banner.open.toString():
|
||||
return {
|
||||
...state,
|
||||
results: action.grades,
|
||||
headings: action.headings,
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
prevPage: action.prev,
|
||||
nextPage: action.next,
|
||||
showSpinner: false,
|
||||
courseId: action.courseId,
|
||||
totalUsersCount: action.totalUsersCount,
|
||||
filteredUsersCount: action.filteredUsersCount,
|
||||
showSuccess: true,
|
||||
};
|
||||
case DONE_VIEWING_ASSIGNMENT: {
|
||||
case actions.banner.close.toString():
|
||||
return {
|
||||
...state,
|
||||
showSuccess: false,
|
||||
};
|
||||
case actions.bulkHistory.received.toString():
|
||||
return {
|
||||
...state,
|
||||
bulkManagement: {
|
||||
...state.bulkManagement,
|
||||
history: payload,
|
||||
},
|
||||
};
|
||||
case actions.csvUpload.started.toString(): {
|
||||
const { errorMessages, uploadSuccess, ...rest } = state.bulkManagement;
|
||||
return {
|
||||
...state,
|
||||
showSpinner: true,
|
||||
bulkManagement: rest,
|
||||
};
|
||||
}
|
||||
case actions.csvUpload.finished.toString():
|
||||
return {
|
||||
...state,
|
||||
showSpinner: false,
|
||||
bulkManagement: {
|
||||
...state.bulkManagement,
|
||||
uploadSuccess: true,
|
||||
},
|
||||
};
|
||||
case actions.csvUpload.error.toString():
|
||||
return {
|
||||
...state,
|
||||
showSpinner: false,
|
||||
bulkManagement: {
|
||||
...state.bulkManagement,
|
||||
...payload,
|
||||
},
|
||||
};
|
||||
case actions.doneViewingAssignment.toString(): {
|
||||
const {
|
||||
gradeOverrideHistoryResults,
|
||||
gradeOverrideCurrentEarnedAllOverride,
|
||||
@@ -72,96 +88,63 @@ const grades = (state = initialState, action) => {
|
||||
} = state;
|
||||
return rest;
|
||||
}
|
||||
case GOT_GRADE_OVERRIDE_HISTORY:
|
||||
return {
|
||||
...state,
|
||||
gradeOverrideHistoryResults: action.overrideHistory,
|
||||
gradeOverrideCurrentEarnedAllOverride: action.currentEarnedAllOverride,
|
||||
gradeOverrideCurrentPossibleAllOverride: action.currentPossibleAllOverride,
|
||||
gradeOverrideCurrentEarnedGradedOverride: action.currentEarnedGradedOverride,
|
||||
gradeOverrideCurrentPossibleGradedOverride: action.currentPossibleGradedOverride,
|
||||
gradeOriginalEarnedAll: action.originalGradeEarnedAll,
|
||||
gradeOriginalPossibleAll: action.originalGradePossibleAll,
|
||||
gradeOriginalEarnedGraded: action.originalGradeEarnedGraded,
|
||||
gradeOriginalPossibleGraded: action.originalGradePossibleGraded,
|
||||
overrideHistoryError: '',
|
||||
};
|
||||
|
||||
case ERROR_FETCHING_GRADE_OVERRIDE_HISTORY:
|
||||
return {
|
||||
...state,
|
||||
finishedFetchingOverrideHistory: true,
|
||||
overrideHistoryError: action.errorMessage,
|
||||
};
|
||||
|
||||
case STARTED_FETCHING_GRADES:
|
||||
case actions.fetching.started.toString():
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
finishedFetching: false,
|
||||
showSpinner: true,
|
||||
};
|
||||
case ERROR_FETCHING_GRADES:
|
||||
case actions.fetching.error.toString():
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
case TOGGLE_GRADE_FORMAT:
|
||||
return {
|
||||
...state,
|
||||
gradeFormat: action.formatType,
|
||||
};
|
||||
case FILTER_BY_ASSIGNMENT_TYPE:
|
||||
return {
|
||||
...state,
|
||||
selectedAssignmentType: action.filterType,
|
||||
headings: action.headings,
|
||||
};
|
||||
case OPEN_BANNER:
|
||||
return {
|
||||
...state,
|
||||
showSuccess: true,
|
||||
};
|
||||
case CLOSE_BANNER:
|
||||
return {
|
||||
...state,
|
||||
showSuccess: false,
|
||||
};
|
||||
case START_UPLOAD: {
|
||||
const { errorMessages, uploadSuccess, ...rest } = state.bulkManagement;
|
||||
return {
|
||||
...state,
|
||||
showSpinner: true,
|
||||
bulkManagement: rest,
|
||||
};
|
||||
}
|
||||
case UPLOAD_COMPLETE: {
|
||||
case actions.fetching.received.toString():
|
||||
return {
|
||||
...state,
|
||||
results: payload.grades,
|
||||
headings: payload.headings,
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
prevPage: payload.prev,
|
||||
nextPage: payload.next,
|
||||
showSpinner: false,
|
||||
bulkManagement: {
|
||||
...state.bulkManagement,
|
||||
uploadSuccess: true,
|
||||
},
|
||||
courseId: payload.courseId,
|
||||
totalUsersCount: payload.totalUsersCount,
|
||||
filteredUsersCount: payload.filteredUsersCount,
|
||||
};
|
||||
}
|
||||
case UPLOAD_ERR:
|
||||
case actions.overrideHistory.received.toString():
|
||||
return {
|
||||
...state,
|
||||
showSpinner: false,
|
||||
bulkManagement: {
|
||||
...state.bulkManagement,
|
||||
...action.data,
|
||||
},
|
||||
gradeOverrideHistoryResults: payload.overrideHistory,
|
||||
gradeOverrideCurrentEarnedAllOverride: payload.currentEarnedAllOverride,
|
||||
gradeOverrideCurrentPossibleAllOverride: payload.currentPossibleAllOverride,
|
||||
gradeOverrideCurrentEarnedGradedOverride: payload.currentEarnedGradedOverride,
|
||||
gradeOverrideCurrentPossibleGradedOverride: payload.currentPossibleGradedOverride,
|
||||
gradeOriginalEarnedAll: payload.originalGradeEarnedAll,
|
||||
gradeOriginalPossibleAll: payload.originalGradePossibleAll,
|
||||
gradeOriginalEarnedGraded: payload.originalGradeEarnedGraded,
|
||||
gradeOriginalPossibleGraded: payload.originalGradePossibleGraded,
|
||||
overrideHistoryError: '',
|
||||
};
|
||||
case GOT_BULK_HISTORY:
|
||||
case actions.overrideHistory.error.toString():
|
||||
return {
|
||||
...state,
|
||||
bulkManagement: {
|
||||
...state.bulkManagement,
|
||||
history: action.data,
|
||||
},
|
||||
finishedFetchingOverrideHistory: true,
|
||||
overrideHistoryError: payload,
|
||||
};
|
||||
case actions.toggleGradeFormat.toString():
|
||||
return {
|
||||
...state,
|
||||
gradeFormat: payload,
|
||||
};
|
||||
case filterActions.update.assignmentType.toString():
|
||||
return {
|
||||
...state,
|
||||
selectedAssignmentType: payload.filterType,
|
||||
headings: payload.headings,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -1,178 +1,248 @@
|
||||
import grades, { initialGradesState as initialState } from './grades';
|
||||
import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_BY_ASSIGNMENT_TYPE,
|
||||
OPEN_BANNER,
|
||||
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
} from '../constants/actionTypes/grades';
|
||||
import actions from '../actions/grades';
|
||||
import filterActions from '../actions/filters';
|
||||
|
||||
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)',
|
||||
},
|
||||
],
|
||||
|
||||
const testingState = {
|
||||
...initialState,
|
||||
bulkManagement: {
|
||||
errorMessages: 'some error message',
|
||||
uploadSuccess: false,
|
||||
},
|
||||
{
|
||||
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)',
|
||||
},
|
||||
],
|
||||
}];
|
||||
arbitraryField: 'abitrary',
|
||||
};
|
||||
|
||||
describe('grades reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(grades(undefined, {})).toEqual(initialState);
|
||||
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);
|
||||
});
|
||||
describe('action handlers', () => {
|
||||
describe('actions.banner.open', () => {
|
||||
it('sets showSuccess to true', () => {
|
||||
expect(
|
||||
grades(undefined, actions.banner.open()),
|
||||
).toEqual({ ...initialState, showSuccess: true });
|
||||
});
|
||||
});
|
||||
describe('actions.banner.close', () => {
|
||||
it('set showSuccess to false', () => {
|
||||
expect(
|
||||
grades(undefined, actions.banner.close()),
|
||||
).toEqual({ ...initialState, showSuccess: false });
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
prevPage: expectedPrev,
|
||||
nextPage: expectedNext,
|
||||
showSpinner: false,
|
||||
courseId,
|
||||
totalUsersCount: 4,
|
||||
filteredUsersCount: 2,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: GOT_GRADES,
|
||||
grades: gradesData,
|
||||
headings: headingsData,
|
||||
prev: expectedPrev,
|
||||
next: expectedNext,
|
||||
track: expectedTrack,
|
||||
cohort: expectedCohortId,
|
||||
showSpinner: true,
|
||||
courseId,
|
||||
totalUsersCount: 4,
|
||||
filteredUsersCount: 2,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
describe('actions.bulkHistory.received', () => {
|
||||
it('loads payload into bulkManagement.history', () => {
|
||||
const history = 'HIstory';
|
||||
expect(
|
||||
grades(testingState, actions.bulkHistory.received(history)),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
bulkManagement: { ...testingState.bulkManagement, history },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
describe('actions.csvUpload.started', () => {
|
||||
it(
|
||||
'sets showSpinner=true and removes errorMessages and uploadSuccess from bulkManagement',
|
||||
() => {
|
||||
const {
|
||||
errorMessages,
|
||||
uploadSuccess,
|
||||
...expectedBulkManagement
|
||||
} = testingState.bulkManagement;
|
||||
expect(
|
||||
grades(testingState, actions.csvUpload.started()),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
showSpinner: true,
|
||||
bulkManagement: expectedBulkManagement,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
describe('handling actions.csvUpload.finished', () => {
|
||||
it('sets showSpinner=false and sets bulkManagement.uploadSuccess=true', () => {
|
||||
expect(
|
||||
grades(testingState, actions.csvUpload.finished()),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
showSpinner: false,
|
||||
bulkManagement: { ...testingState.bulkManagement, uploadSuccess: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('handling actions.csvUpload.error', () => {
|
||||
it('loads errorMessage to bulkManagement from payload and sets showSpinner=false', () => {
|
||||
const errorMessage = 'This is a new error message';
|
||||
expect(
|
||||
grades(testingState, actions.csvUpload.error({
|
||||
errorMessage,
|
||||
uploadSuccess: false,
|
||||
})),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
showSpinner: false,
|
||||
bulkManagement: { errorMessage, ...testingState.bulkManagement },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates filter columns state success', () => {
|
||||
const expectedHeadings = headingsData;
|
||||
const expected = {
|
||||
...initialState,
|
||||
headings: expectedHeadings,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: FILTER_BY_ASSIGNMENT_TYPE,
|
||||
headings: expectedHeadings,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
describe('actions.doneViewingAssignment', () => {
|
||||
it('removes gradeOverride* and gradeOriginal* from existing state', () => {
|
||||
const {
|
||||
gradeOverrideHistoryResults,
|
||||
gradeOverrideCurrentEarnedAllOverride,
|
||||
gradeOverrideCurrentPossibleAllOverride,
|
||||
gradeOverrideCurrentEarnedGradedOverride,
|
||||
gradeOverrideCurrentPossibleGradedOverride,
|
||||
gradeOriginalEarnedAll,
|
||||
gradeOriginalPossibleAll,
|
||||
gradeOriginalEarnedGraded,
|
||||
gradeOriginalPossibleGraded,
|
||||
...expected
|
||||
} = testingState;
|
||||
expect(
|
||||
grades(testingState, actions.doneViewingAssignment()),
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates update_banner state success', () => {
|
||||
const expectedShowSuccess = true;
|
||||
const expected = {
|
||||
...initialState,
|
||||
showSuccess: expectedShowSuccess,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: OPEN_BANNER,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
describe('actions.fetching.started', () => {
|
||||
it('sets startedFetching and showSpinner to true', () => {
|
||||
expect(
|
||||
grades(testingState, actions.fetching.started()),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
startedFetching: true,
|
||||
showSpinner: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('actions.fetching.received', () => {
|
||||
it(
|
||||
'loads payload and sets finishedFetching:true, errorFetching:false, showSpinner:false',
|
||||
() => {
|
||||
const payload = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
headings: 'some Headings',
|
||||
prev: 'testPrevUrl',
|
||||
next: 'testNextUrl',
|
||||
track: 'verified',
|
||||
cohortId: 2,
|
||||
totalUsersCount: 4,
|
||||
filteredUsersCount: 2,
|
||||
assignmentType: 'Homework',
|
||||
grades: { somethingArbitrary: 'some data' },
|
||||
};
|
||||
expect(
|
||||
grades(testingState, actions.fetching.received(payload)),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
results: payload.grades,
|
||||
headings: payload.headings,
|
||||
prevPage: payload.prev,
|
||||
nextPage: payload.next,
|
||||
courseId: payload.courseId,
|
||||
totalUsersCount: payload.totalUsersCount,
|
||||
filteredUsersCount: payload.filteredUsersCount,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
showSpinner: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
describe('actions.fetching.error', () => {
|
||||
it('sets finishedFetching and errorFetching to true', () => {
|
||||
expect(
|
||||
grades(testingState, actions.fetching.error()),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates fetch grades failure state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: ERROR_FETCHING_GRADES,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
describe('actions.overrideHistory.received', () => {
|
||||
it('loads payload and clears overrideHistoryError', () => {
|
||||
const payload = {
|
||||
overrideHistory: true,
|
||||
currentEarnedAllOverride: false,
|
||||
currentPossibleAllOverride: 'red',
|
||||
currentEarnedGradedOverride: 'green',
|
||||
currentPossibleGradedOverride: 'blue',
|
||||
originalGradeEarnedAll: 'other',
|
||||
originalGradePossibleAll: 'sparrow',
|
||||
originalGradeEarnedGraded: 'crow',
|
||||
originalGradePossibleGraded: 'raven',
|
||||
};
|
||||
expect(
|
||||
grades(testingState, actions.overrideHistory.received(payload)),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
gradeOverrideHistoryResults: payload.overrideHistory,
|
||||
gradeOverrideCurrentEarnedAllOverride: payload.currentEarnedAllOverride,
|
||||
gradeOverrideCurrentPossibleAllOverride: payload.currentPossibleAllOverride,
|
||||
gradeOverrideCurrentEarnedGradedOverride: payload.currentEarnedGradedOverride,
|
||||
gradeOverrideCurrentPossibleGradedOverride: payload.currentPossibleGradedOverride,
|
||||
gradeOriginalEarnedAll: payload.originalGradeEarnedAll,
|
||||
gradeOriginalPossibleAll: payload.originalGradePossibleAll,
|
||||
gradeOriginalEarnedGraded: payload.originalGradeEarnedGraded,
|
||||
gradeOriginalPossibleGraded: payload.originalGradePossibleGraded,
|
||||
overrideHistoryError: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('actions.overrideHistory.error', () => {
|
||||
it(
|
||||
'sets finishedFetchingOverrideHistory=true and loads overrideHistoryError from payload',
|
||||
() => {
|
||||
const errorMessage = 'This is the error message';
|
||||
expect(
|
||||
grades(testingState, actions.overrideHistory.error(errorMessage)),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
finishedFetchingOverrideHistory: true,
|
||||
overrideHistoryError: errorMessage,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('updates fetch grade override history failure state', () => {
|
||||
const errorMessage = 'This is the error message';
|
||||
const expected = {
|
||||
...initialState,
|
||||
finishedFetchingOverrideHistory: true,
|
||||
overrideHistoryError: errorMessage,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
errorMessage,
|
||||
})).toEqual(expected);
|
||||
describe('handling actions.toggleGradeFormat', () => {
|
||||
it('updates grade format attribute', () => {
|
||||
const formatTypeData = 'percent';
|
||||
expect(
|
||||
grades(undefined, actions.toggleGradeFormat(formatTypeData)),
|
||||
).toEqual({ ...initialState, gradeFormat: formatTypeData });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handling filterActions.update.assignmentType', () => {
|
||||
it('loads assignmentType and headings from the payload', () => {
|
||||
const expectedSelectedAssignmentType = 'selected assignment type';
|
||||
expect(
|
||||
grades(testingState, filterActions.update.assignmentType({
|
||||
headings: headingsData,
|
||||
filterType: expectedSelectedAssignmentType,
|
||||
})),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
selectedAssignmentType: expectedSelectedAssignmentType,
|
||||
headings: headingsData,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES,
|
||||
} from '../constants/actionTypes/roles';
|
||||
import actions from '../actions/roles';
|
||||
|
||||
const initialState = {
|
||||
canUserViewGradebook: null,
|
||||
@@ -9,12 +6,12 @@ const initialState = {
|
||||
|
||||
const roles = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_ROLES:
|
||||
case actions.fetching.received.toString():
|
||||
return {
|
||||
...state,
|
||||
canUserViewGradebook: action.canUserViewGradebook,
|
||||
canUserViewGradebook: action.payload,
|
||||
};
|
||||
case ERROR_FETCHING_ROLES:
|
||||
case actions.fetching.error.toString():
|
||||
return {
|
||||
...state,
|
||||
canUserViewGradebook: false,
|
||||
@@ -24,4 +21,5 @@ const roles = (state = initialState, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState };
|
||||
export default roles;
|
||||
|
||||
@@ -1,47 +1,38 @@
|
||||
import roles from './roles';
|
||||
import {
|
||||
ERROR_FETCHING_ROLES,
|
||||
GOT_ROLES,
|
||||
} from '../constants/actionTypes/roles';
|
||||
import roles, { initialState } from './roles';
|
||||
import actions from '../actions/roles';
|
||||
|
||||
const initialState = {
|
||||
canUserViewGradebook: null,
|
||||
const testingState = {
|
||||
...initialState,
|
||||
arbitraryField: 'arbitrary',
|
||||
};
|
||||
|
||||
describe('tracks reducer', () => {
|
||||
describe('roles reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(roles(undefined, {})).toEqual(initialState);
|
||||
expect(
|
||||
roles(undefined, {}),
|
||||
).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('updates canUserViewGradebook to true', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
canUserViewGradebook: true,
|
||||
};
|
||||
expect(roles(undefined, {
|
||||
type: GOT_ROLES,
|
||||
canUserViewGradebook: true,
|
||||
})).toEqual(expected);
|
||||
describe('handling actions.received', () => {
|
||||
it('updates canUserViewGradebook to the received payload', () => {
|
||||
const expectedCanUserViewGradebook = true;
|
||||
expect(
|
||||
roles(testingState, actions.fetching.received(expectedCanUserViewGradebook)),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
canUserViewGradebook: expectedCanUserViewGradebook,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates canUserViewGradebook to false', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
canUserViewGradebook: false,
|
||||
};
|
||||
expect(roles(undefined, {
|
||||
type: GOT_ROLES,
|
||||
canUserViewGradebook: false,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates fetch roles failure state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
canUserViewGradebook: false,
|
||||
};
|
||||
expect(roles(undefined, {
|
||||
type: ERROR_FETCHING_ROLES,
|
||||
})).toEqual(expected);
|
||||
describe('handling actions.errorFetching', () => {
|
||||
it('sets canUserViewGradebook to false', () => {
|
||||
expect(
|
||||
roles(testingState, actions.fetching.error()),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
canUserViewGradebook: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
import actions from '../actions/tracks';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
@@ -12,19 +8,19 @@ const initialState = {
|
||||
|
||||
const tracks = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_TRACKS:
|
||||
return {
|
||||
...state,
|
||||
results: action.tracks,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
case STARTED_FETCHING_TRACKS:
|
||||
case actions.fetching.started.toString():
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
};
|
||||
case ERROR_FETCHING_TRACKS:
|
||||
case actions.fetching.received.toString():
|
||||
return {
|
||||
...state,
|
||||
results: action.payload,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
};
|
||||
case actions.fetching.error.toString():
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
@@ -35,4 +31,5 @@ const tracks = (state = initialState, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState };
|
||||
export default tracks;
|
||||
|
||||
@@ -1,76 +1,58 @@
|
||||
import tracks from './tracks';
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
import tracks, { initialState } from './tracks';
|
||||
import actions from '../actions/tracks';
|
||||
|
||||
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',
|
||||
}];
|
||||
{ someArbitraryField: 'arbitrary data' },
|
||||
{ anotherArbitraryField: 'more arbitrary data' },
|
||||
];
|
||||
|
||||
const testingState = {
|
||||
...initialState,
|
||||
results: tracksData,
|
||||
arbitraryField: 'arbitrary',
|
||||
};
|
||||
|
||||
describe('tracks reducer', () => {
|
||||
it('has initial state', () => {
|
||||
expect(tracks(undefined, {})).toEqual(initialState);
|
||||
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);
|
||||
describe('handling actions.fetching.started', () => {
|
||||
it('set start fetching to true. Preserve results if existed', () => {
|
||||
expect(
|
||||
tracks(testingState, actions.fetching.started()),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
startedFetching: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
describe('handling actions.fetching.received', () => {
|
||||
it('replace results then set finish fetching to true and error to false', () => {
|
||||
const newTracksData = [{ receivedData: 'new data' }];
|
||||
expect(
|
||||
tracks(testingState, actions.fetching.received(newTracksData)),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
results: newTracksData,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates fetch tracks failure state', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
};
|
||||
expect(tracks(undefined, {
|
||||
type: ERROR_FETCHING_TRACKS,
|
||||
})).toEqual(expected);
|
||||
describe('handling actions.fetching.error', () => {
|
||||
it('set finish fetch and error to true. Preserve results if existed.', () => {
|
||||
expect(
|
||||
tracks(testingState, actions.fetching.error()),
|
||||
).toEqual({
|
||||
...testingState,
|
||||
errorFetching: true,
|
||||
finishedFetching: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const selectors = {
|
||||
areGradesFrozen: ({ assignmentTypes }) => assignmentTypes.areGradesFrozen,
|
||||
allAssignmentTypes: ({ assignmentTypes }) => assignmentTypes.results,
|
||||
};
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export default selectors;
|
||||
const areGradesFrozen = ({ assignmentTypes }) => assignmentTypes.areGradesFrozen;
|
||||
const allAssignmentTypes = ({ assignmentTypes }) => assignmentTypes.results;
|
||||
|
||||
export default StrictDict({
|
||||
areGradesFrozen,
|
||||
allAssignmentTypes,
|
||||
});
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import selectors from './assignmentTypes';
|
||||
|
||||
describe('areGradesFrozen', () => {
|
||||
it('selects areGradesFrozen from state', () => {
|
||||
const testValue = 'THX 1138';
|
||||
const areGradesFrozen = selectors.areGradesFrozen({ assignmentTypes: { areGradesFrozen: testValue } });
|
||||
expect(areGradesFrozen).toEqual(testValue);
|
||||
describe('assignmentType selectors', () => {
|
||||
describe('areGradesFrozen', () => {
|
||||
it('selects areGradesFrozen from state', () => {
|
||||
const testValue = 'THX 1138';
|
||||
expect(
|
||||
selectors.areGradesFrozen({ assignmentTypes: { areGradesFrozen: testValue } }),
|
||||
).toEqual(testValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('allAssignmentTypes', () => {
|
||||
it('returns assignment types', () => {
|
||||
const testAssignmentTypes = ['assignment', 'labs'];
|
||||
const allAssignmentTypes = selectors.allAssignmentTypes({ assignmentTypes: { results: testAssignmentTypes } });
|
||||
expect(allAssignmentTypes).toEqual(testAssignmentTypes);
|
||||
describe('allAssignmentTypes', () => {
|
||||
it('returns assignment types', () => {
|
||||
const testAssignmentTypes = ['assignment', 'labs'];
|
||||
expect(
|
||||
selectors.allAssignmentTypes({ assignmentTypes: { results: testAssignmentTypes } }),
|
||||
).toEqual(testAssignmentTypes);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
const allCohorts = state => state.cohorts.results || [];
|
||||
import { StrictDict } from 'utils';
|
||||
/**
|
||||
* allCohorts(state)
|
||||
* returns top-level cohorts results data
|
||||
* @param {object} state - redux state
|
||||
* @return {object[]} - list of cohort objects from fetch results
|
||||
*/
|
||||
const allCohorts = (state) => state.cohorts.results || [];
|
||||
|
||||
/**
|
||||
* getCohortById(state, selectedCohortId)
|
||||
* returns cohort with given id
|
||||
* @param {object} state - redux state
|
||||
* @param {number} selectedCohortId - id of cohort to return
|
||||
* @return {object} cohort with given id.
|
||||
*/
|
||||
const getCohortById = (state, selectedCohortId) => {
|
||||
const cohort = allCohorts(state).find(coh => coh.id === selectedCohortId);
|
||||
const cohort = allCohorts(state).find(({ id }) => id === selectedCohortId);
|
||||
return cohort;
|
||||
};
|
||||
|
||||
/**
|
||||
* getCohortNameById(state, selectedCohortId)
|
||||
* @param {object} state - redux state
|
||||
* @param {number} selectedCohortId - id of cohort whose name is requested
|
||||
* @return {string} - name of cohort with the given id
|
||||
*/
|
||||
const getCohortNameById = (state, selectedCohortId) => (getCohortById(state, selectedCohortId) || {}).name;
|
||||
|
||||
const selectors = {
|
||||
export default StrictDict({
|
||||
getCohortById,
|
||||
getCohortNameById,
|
||||
allCohorts,
|
||||
};
|
||||
|
||||
export default selectors;
|
||||
});
|
||||
|
||||
@@ -1,12 +1,64 @@
|
||||
/* eslint-disable import/no-self-import */
|
||||
import { StrictDict } from 'utils';
|
||||
import * as module from './filters';
|
||||
import simpleSelectorFactory from '../utils';
|
||||
|
||||
const allFilters = (state) => state.filters || {};
|
||||
// Transformers
|
||||
/**
|
||||
* chooseRelevantAssignmentData(assignment)
|
||||
* formats the assignment api data for an assignment object for consumption
|
||||
* @param {object} assignment - assignment data to prepare
|
||||
* @return {object} - formatted data ({ label, subsectionLabel, type, id })
|
||||
*/
|
||||
export const chooseRelevantAssignmentData = ({
|
||||
label,
|
||||
subsection_name: subsectionLabel,
|
||||
category: type,
|
||||
module_id: id,
|
||||
}) => ({
|
||||
label, subsectionLabel, type, id,
|
||||
});
|
||||
|
||||
const getAssignmentsFromResultsSubstate = (results) => (
|
||||
/**
|
||||
* getAssignmentsFromResultsSubstate(results)
|
||||
* returns the section_breakdown of the first results entry
|
||||
* defaulting to an empty list.
|
||||
* @param {[object[]]} results - list of result entries from grades fetch
|
||||
* @return {object} - section_breakdown of first grade entry
|
||||
*/
|
||||
export const getAssignmentsFromResultsSubstate = (results) => (
|
||||
(results[0] || {}).section_breakdown || []
|
||||
);
|
||||
|
||||
const selectableAssignments = (state) => {
|
||||
/**
|
||||
* relevantAssignmentDataFromResults
|
||||
* returns assignment info from grades results for the assignment with the given id
|
||||
* @param {object} grades - grades fetch result
|
||||
* @param {string} id - selected assignment id from assignment filter
|
||||
* @return {object} assignment data with type, label, and subsectionLabel
|
||||
*/
|
||||
export const relevantAssignmentDataFromResults = (grades, id) => (
|
||||
module.getAssignmentsFromResultsSubstate(grades)
|
||||
.map(module.chooseRelevantAssignmentData)
|
||||
.find(assignment => assignment.id === id)
|
||||
);
|
||||
|
||||
// Selectors
|
||||
/**
|
||||
* allFilters(state)
|
||||
* returns the top-level filter state.
|
||||
* @param {object} state - redux state
|
||||
* @return {object} - filter substate from redux state
|
||||
*/
|
||||
export const allFilters = (state) => state.filters || {};
|
||||
|
||||
/**
|
||||
* selectableAssignments(state)
|
||||
* @param {object} state - redux state
|
||||
* @return {object[]} - list of selectable assignment objects, filtered if there is an
|
||||
* assignmentType filter selected.
|
||||
*/
|
||||
export const selectableAssignments = (state) => {
|
||||
const selectedAssignmentType = allFilters(state).assignmentType;
|
||||
const needToFilter = selectedAssignmentType && selectedAssignmentType !== 'All';
|
||||
const allAssignments = getAssignmentsFromResultsSubstate(state.grades.results);
|
||||
@@ -18,20 +70,16 @@ const selectableAssignments = (state) => {
|
||||
return allAssignments;
|
||||
};
|
||||
|
||||
const chooseRelevantAssignmentData = ({
|
||||
label,
|
||||
subsection_name: subsectionLabel,
|
||||
category,
|
||||
module_id: id,
|
||||
}) => ({
|
||||
label, subsectionLabel, category, id,
|
||||
});
|
||||
|
||||
const selectableAssignmentLabels = (state) => (
|
||||
/**
|
||||
* Returns the relevant assignment data for all selectable assignments
|
||||
* @param {object} state - redux state
|
||||
* @return {object[]} - object of assignment data entries [({ label, subsectionLabel, type, id })]
|
||||
*/
|
||||
export const selectableAssignmentLabels = (state) => (
|
||||
selectableAssignments(state).map(chooseRelevantAssignmentData)
|
||||
);
|
||||
|
||||
const simpleSelectors = simpleSelectorFactory(
|
||||
export const simpleSelectors = simpleSelectorFactory(
|
||||
({ filters }) => filters,
|
||||
[
|
||||
'assignment',
|
||||
@@ -45,11 +93,24 @@ const simpleSelectors = simpleSelectorFactory(
|
||||
'includeCourseRoleMembers',
|
||||
],
|
||||
);
|
||||
const selectedAssignmentId = (state) => (simpleSelectors.assignment(state) || {}).id;
|
||||
const selectedAssignmentLabel = (state) => (simpleSelectors.assignment(state) || {}).label;
|
||||
/**
|
||||
* Returns the id of the selected assignment filter
|
||||
* @param {object} state - redux state
|
||||
* @return {string} - assignment id
|
||||
*/
|
||||
export const selectedAssignmentId = (state) => (simpleSelectors.assignment(state) || {}).id;
|
||||
|
||||
const selectors = {
|
||||
/**
|
||||
* selectedAssignmentLabel(state)
|
||||
* Returns the label of the selected assignment filter
|
||||
* @param {object} state - redux state
|
||||
* @return {string} - assignment label
|
||||
*/
|
||||
export const selectedAssignmentLabel = (state) => (simpleSelectors.assignment(state) || {}).label;
|
||||
|
||||
export default StrictDict({
|
||||
...simpleSelectors,
|
||||
relevantAssignmentDataFromResults,
|
||||
selectedAssignmentId,
|
||||
selectedAssignmentLabel,
|
||||
selectableAssignmentLabels,
|
||||
@@ -57,6 +118,4 @@ const selectors = {
|
||||
allFilters,
|
||||
chooseRelevantAssignmentData,
|
||||
getAssignmentsFromResultsSubstate,
|
||||
};
|
||||
|
||||
export default selectors;
|
||||
});
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import selectors from './filters';
|
||||
// import * in order to mock in-file references
|
||||
import * as selectors from './filters';
|
||||
// import default export in order to test simpleSelectors not exported individually
|
||||
import exportedSelectors from './filters';
|
||||
|
||||
const selectedAssignmentInfo = {
|
||||
category: 'Homework',
|
||||
type: 'Homework',
|
||||
id: 'block-v1:edX+type@sequential+block@abcde',
|
||||
label: 'HW 01',
|
||||
subsectionLabel: 'Example Week 1: Getting Started',
|
||||
@@ -63,75 +66,124 @@ const testState = {
|
||||
grades: gradesData,
|
||||
};
|
||||
|
||||
describe('allFilters', () => {
|
||||
it('selects all filters from state', () => {
|
||||
const allFilters = selectors.allFilters(testState);
|
||||
expect(allFilters).toEqual(filters);
|
||||
describe('filters selectors', () => {
|
||||
describe('chooseRelevantAssignmentData', () => {
|
||||
it('maps label, subsection, category, and ID from assignment data', () => {
|
||||
const assignmentData = selectors.chooseRelevantAssignmentData(sectionBreakdowns[0]);
|
||||
expect(assignmentData).toEqual(selectedAssignmentInfo);
|
||||
});
|
||||
});
|
||||
it('returns an empty object when no filters are in state', () => {
|
||||
const allFilters = selectors.allFilters({});
|
||||
expect(allFilters).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectedAssignmentId', () => {
|
||||
it('gets filtered assignment ID when available', () => {
|
||||
const assignmentId = selectors.selectedAssignmentId(testState);
|
||||
expect(assignmentId).toEqual(filters.assignment.id);
|
||||
describe('getAssignmentsFromResultsSubstate', () => {
|
||||
it('gets section breakdowns from state', () => {
|
||||
const assignments = selectors.getAssignmentsFromResultsSubstate(gradesData.results);
|
||||
expect(assignments).toEqual(sectionBreakdowns);
|
||||
});
|
||||
it('returns an empty array when results are not supplied', () => {
|
||||
const assignments = selectors.getAssignmentsFromResultsSubstate([]);
|
||||
expect(assignments).toEqual([]);
|
||||
});
|
||||
it('returns an empty array when section breakdowns are not supplied', () => {
|
||||
const assignments = selectors.getAssignmentsFromResultsSubstate([{}]);
|
||||
expect(assignments).toEqual([]);
|
||||
});
|
||||
});
|
||||
it('returns undefined when assignment ID unavailable', () => {
|
||||
const assignmentId = selectors.selectedAssignmentId({ filters: { assignment: undefined } });
|
||||
expect(assignmentId).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectedAssignmentLabel', () => {
|
||||
it('gets filtered assignment label when available', () => {
|
||||
const assignmentLabel = selectors.selectedAssignmentLabel(testState);
|
||||
expect(assignmentLabel).toEqual(filters.assignment.label);
|
||||
});
|
||||
it('returns undefined when assignment label is unavailable', () => {
|
||||
const assignmentLabel = selectors.selectedAssignmentLabel({ filters: { assignment: undefined } });
|
||||
expect(assignmentLabel).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
describe('relevantAssignmentDataFromResults', () => {
|
||||
it('grabs relevant assignment data from the grades substate with matching id', () => {
|
||||
const ids = ['some', 'fake', 'ids'];
|
||||
const grades = { gradeIds: ids };
|
||||
|
||||
describe('selectableAssignmentLabels', () => {
|
||||
it('gets assignment data for sections matching selected type filters', () => {
|
||||
const selectableAssignmentLabels = selectors.selectableAssignmentLabels(testState);
|
||||
expect(selectableAssignmentLabels).toEqual([filters.assignment]);
|
||||
const getFromSubstate = selectors.getAssignmentsFromResultsSubstate;
|
||||
selectors.getAssignmentsFromResultsSubstate = jest.fn(
|
||||
({ gradeIds }) => gradeIds,
|
||||
);
|
||||
const mapper = selectors.chooseRelevantAssignmentData;
|
||||
selectors.chooseRelevantAssignmentData = jest.fn((id) => ({ id }));
|
||||
expect(selectors.relevantAssignmentDataFromResults(grades, ids[2])).toEqual(
|
||||
{ id: ids[2] },
|
||||
);
|
||||
selectors.getAssignmentsFromResultsSubstate = getFromSubstate;
|
||||
selectors.chooseRelevantAssignmentData = mapper;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectableAssignments', () => {
|
||||
it('returns all graded assignments when no assignment type filtering is applied', () => {
|
||||
const selectableAssignments = selectors.selectableAssignments({ grades: gradesData, filters: noFilters });
|
||||
expect(selectableAssignments).toEqual(sectionBreakdowns);
|
||||
describe('allFilters', () => {
|
||||
it('selects all filters from state', () => {
|
||||
const allFilters = selectors.allFilters(testState);
|
||||
expect(allFilters).toEqual(filters);
|
||||
});
|
||||
it('returns an empty object when no filters are in state', () => {
|
||||
const allFilters = selectors.allFilters({});
|
||||
expect(allFilters).toEqual({});
|
||||
});
|
||||
});
|
||||
it('gets assignments of the selected category when assignment type filtering is applied', () => {
|
||||
const selectableAssignments = selectors.selectableAssignments(testState);
|
||||
expect(selectableAssignments).toEqual([sectionBreakdowns[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chooseRelevantAssignmentData', () => {
|
||||
it('maps label, subsection, category, and ID from assignment data', () => {
|
||||
const assignmentData = selectors.chooseRelevantAssignmentData(sectionBreakdowns[0]);
|
||||
expect(assignmentData).toEqual(selectedAssignmentInfo);
|
||||
describe('selectedAssignmentId', () => {
|
||||
it('gets filtered assignment ID when available', () => {
|
||||
const assignmentId = selectors.selectedAssignmentId(testState);
|
||||
expect(assignmentId).toEqual(filters.assignment.id);
|
||||
});
|
||||
it('returns undefined when assignment ID unavailable', () => {
|
||||
const assignmentId = selectors.selectedAssignmentId({ filters: { assignment: undefined } });
|
||||
expect(assignmentId).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAssignmentsFromResultsSubstate', () => {
|
||||
it('gets section breakdowns from state', () => {
|
||||
const assignments = selectors.getAssignmentsFromResultsSubstate(gradesData.results);
|
||||
expect(assignments).toEqual(sectionBreakdowns);
|
||||
describe('selectedAssignmentLabel', () => {
|
||||
it('gets filtered assignment label when available', () => {
|
||||
const assignmentLabel = selectors.selectedAssignmentLabel(testState);
|
||||
expect(assignmentLabel).toEqual(filters.assignment.label);
|
||||
});
|
||||
it('returns undefined when assignment label is unavailable', () => {
|
||||
const assignmentLabel = selectors.selectedAssignmentLabel({ filters: { assignment: undefined } });
|
||||
expect(assignmentLabel).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
it('returns an empty array when results are not supplied', () => {
|
||||
const assignments = selectors.getAssignmentsFromResultsSubstate([]);
|
||||
expect(assignments).toEqual([]);
|
||||
|
||||
describe('selectableAssignmentLabels', () => {
|
||||
it('gets assignment data for sections matching selected type filters', () => {
|
||||
const selectableAssignmentLabels = selectors.selectableAssignmentLabels(testState);
|
||||
expect(selectableAssignmentLabels).toEqual([filters.assignment]);
|
||||
});
|
||||
});
|
||||
it('returns an empty array when section breakdowns are not supplied', () => {
|
||||
const assignments = selectors.getAssignmentsFromResultsSubstate([{}]);
|
||||
expect(assignments).toEqual([]);
|
||||
|
||||
describe('selectableAssignments', () => {
|
||||
it('returns all graded assignments when no assignment type filtering is applied', () => {
|
||||
const selectableAssignments = selectors.selectableAssignments({ grades: gradesData, filters: noFilters });
|
||||
expect(selectableAssignments).toEqual(sectionBreakdowns);
|
||||
});
|
||||
it('gets assignments of the selected category when assignment type filtering is applied', () => {
|
||||
const selectableAssignments = selectors.selectableAssignments(testState);
|
||||
expect(selectableAssignments).toEqual([sectionBreakdowns[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('simpleSelectors', () => {
|
||||
const testVal = 'some Test Value';
|
||||
const testSimpleSelector = (key) => {
|
||||
test(key, () => {
|
||||
expect(exportedSelectors[key]({ filters: { [key]: testVal } })).toEqual(testVal);
|
||||
});
|
||||
};
|
||||
testSimpleSelector('assignment');
|
||||
testSimpleSelector('assignmentGradeMax');
|
||||
testSimpleSelector('assignmentGradeMin');
|
||||
testSimpleSelector('assignmentType');
|
||||
testSimpleSelector('cohort');
|
||||
testSimpleSelector('courseGradeMax');
|
||||
testSimpleSelector('courseGradeMin');
|
||||
testSimpleSelector('track');
|
||||
testSimpleSelector('includeCourseRoleMembers');
|
||||
test('selectedAssignmentId', () => {
|
||||
expect(
|
||||
selectors.selectedAssignmentId({ filters: { assignment: { id: testVal } } }),
|
||||
).toEqual(testVal);
|
||||
});
|
||||
test('selectedAssignmentLabel', () => {
|
||||
expect(
|
||||
selectors.selectedAssignmentLabel({ filters: { assignment: { label: testVal } } }),
|
||||
).toEqual(testVal);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,42 +1,89 @@
|
||||
/* eslint-disable import/no-self-import */
|
||||
import { StrictDict } from 'utils';
|
||||
import { formatDateForDisplay } from '../actions/utils';
|
||||
import simpleSelectorFactory from '../utils';
|
||||
import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../constants/grades';
|
||||
import * as module from './grades';
|
||||
|
||||
const getRowsProcessed = (data) => {
|
||||
const {
|
||||
processed_rows: processed,
|
||||
saved_rows: saved,
|
||||
total_rows: total,
|
||||
} = data;
|
||||
return {
|
||||
total,
|
||||
successfullyProcessed: saved,
|
||||
failed: processed - saved,
|
||||
skipped: total - processed,
|
||||
};
|
||||
};
|
||||
|
||||
const transformHistoryEntry = ({
|
||||
modified,
|
||||
original_filename: originalFilename,
|
||||
data,
|
||||
...rest
|
||||
export const getRowsProcessed = ({
|
||||
processed_rows: processed,
|
||||
saved_rows: saved,
|
||||
total_rows: total,
|
||||
}) => ({
|
||||
timeUploaded: formatDateForDisplay(new Date(modified)),
|
||||
originalFilename,
|
||||
summaryOfRowsProcessed: getRowsProcessed(data),
|
||||
...rest,
|
||||
total,
|
||||
successfullyProcessed: saved,
|
||||
failed: processed - saved,
|
||||
skipped: total - processed,
|
||||
});
|
||||
|
||||
const bulkManagementHistory = ({ grades: { bulkManagement } }) => (
|
||||
(bulkManagement.history || [])
|
||||
/**
|
||||
* formatGradeOverrideForDisplay(historyArray)
|
||||
* returns the grade override history results in display format.
|
||||
* @param {object[]} historyArray - array of gradeOverrideHistoryResults
|
||||
* @return {object[]} - display-formatted history results ({ date, grader, reason, adjustedGrade })
|
||||
*/
|
||||
export const formatGradeOverrideForDisplay = historyArray => historyArray.map(item => ({
|
||||
date: formatDateForDisplay(new Date(item.history_date)),
|
||||
grader: item.history_user,
|
||||
reason: item.override_reason,
|
||||
adjustedGrade: item.earned_graded_override,
|
||||
}));
|
||||
|
||||
export const minGrade = '0';
|
||||
export const maxGrade = '100';
|
||||
|
||||
/**
|
||||
* formatMaxCourseGrade(percentGrade)
|
||||
* Takes a percent grade and returns it unless it is equal to the max grade
|
||||
* @param {string} percentGrade - grade percentage
|
||||
* @return {string} percent grade or null
|
||||
*/
|
||||
export const formatMaxCourseGrade = (percentGrade) => (
|
||||
(percentGrade === maxGrade) ? null : percentGrade
|
||||
);
|
||||
/**
|
||||
* formatMinCourseGrade(percentGrade)
|
||||
* Takes a percent grade and returns it unless it is equal to the min grade
|
||||
* @param {string} percentGrade - grade percentage
|
||||
* @return {string} percent grade or null
|
||||
*/
|
||||
export const formatMinCourseGrade = (percentGrade) => (
|
||||
(percentGrade === minGrade) ? null : percentGrade
|
||||
);
|
||||
|
||||
const bulkManagementHistoryEntries = (state) => (
|
||||
bulkManagementHistory(state).map(transformHistoryEntry)
|
||||
/**
|
||||
* formatMaxAssignmentGrade(percentGrade, options)
|
||||
* Takes a percent grade and returns it unless it is equal to the max grade or
|
||||
* the assignment id is set.
|
||||
* @param {string} percentGrade - grade percentage
|
||||
* @param {object} options - options object ({ assignmentId });
|
||||
* @return {string} percent grade or null
|
||||
*/
|
||||
export const formatMaxAssignmentGrade = (percentGrade, options) => (
|
||||
(percentGrade === maxGrade || !options.assignmentId) ? null : percentGrade
|
||||
);
|
||||
|
||||
const headingMapper = (category, label = 'All') => {
|
||||
/**
|
||||
* formatMinAssignmentGrade(percentGrade, options)
|
||||
* Takes a percent grade and returns it unless it is equal to the min grade or
|
||||
* the assignment id is set.
|
||||
* @param {string} percentGrade - grade percentage
|
||||
* @param {object} options - options object ({ assignmentId });
|
||||
* @return {string} percent grade or null
|
||||
*/
|
||||
export const formatMinAssignmentGrade = (percentGrade, options) => (
|
||||
(percentGrade === minGrade || !options.assignmentId) ? null : percentGrade
|
||||
);
|
||||
|
||||
/**
|
||||
* headingMapper(category, label='All')
|
||||
* Takes category and label filters and returns a method that will take a section breakdown
|
||||
* and return the appropriate table headings.
|
||||
* @param {string} category - assignment filter type
|
||||
* @param {string} label - assignment filter label
|
||||
* @return {string[]} - list of table headers
|
||||
*/
|
||||
export const headingMapper = (category, label = 'All') => {
|
||||
const filters = {
|
||||
all: section => section.label,
|
||||
byCategory: section => section.label && section.category === category,
|
||||
@@ -45,66 +92,116 @@ const headingMapper = (category, label = 'All') => {
|
||||
|
||||
let filter;
|
||||
if (label === 'All') {
|
||||
filter = category === 'All' ? 'all' : 'byCategory';
|
||||
filter = category === 'All' ? filters.all : filters.byCategory;
|
||||
} else {
|
||||
filter = 'byLabel';
|
||||
filter = filters.byLabel;
|
||||
}
|
||||
|
||||
return (entry) => {
|
||||
if (entry) {
|
||||
const results = [USERNAME_HEADING, EMAIL_HEADING];
|
||||
|
||||
const assignmentHeadings = entry
|
||||
.filter(filters[filter])
|
||||
.map(s => s.label);
|
||||
|
||||
const totals = [TOTAL_COURSE_GRADE_HEADING];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
return [
|
||||
USERNAME_HEADING,
|
||||
EMAIL_HEADING,
|
||||
...entry.filter(filter).map(s => s.label),
|
||||
TOTAL_COURSE_GRADE_HEADING,
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
};
|
||||
|
||||
const getExampleSectionBreakdown = ({ grades }) => (
|
||||
/**
|
||||
* transformHistoryEntry(rawEntry)
|
||||
* Takes a raw bulkManagementHistory entry and formats it for consumption
|
||||
* @param {object} rawEntry - raw history entry to transform
|
||||
* @return {object} - transformed history entry
|
||||
* ({ timeUploaded, originalFilename, summaryOfRowsProcessed, ... })
|
||||
*/
|
||||
export const transformHistoryEntry = ({
|
||||
modified,
|
||||
original_filename: originalFilename,
|
||||
data,
|
||||
...rest
|
||||
}) => ({
|
||||
timeUploaded: formatDateForDisplay(new Date(modified)),
|
||||
originalFilename,
|
||||
summaryOfRowsProcessed: module.getRowsProcessed(data),
|
||||
...rest,
|
||||
});
|
||||
|
||||
// Selectors
|
||||
/**
|
||||
* allGrades(state)
|
||||
* returns the top-level redux grades state.
|
||||
* @param {object} state - redux state
|
||||
* @return {object} - redux grades state
|
||||
*/
|
||||
export const allGrades = ({ grades: { results } }) => results;
|
||||
|
||||
/**
|
||||
* bulkImportError(state)
|
||||
* returns the stringified bulkManagement import error messages.
|
||||
* @param {object} state - redux state
|
||||
* @return {string} - bulk import error messages joined into a display form
|
||||
* (or empty string if there are none)
|
||||
*/
|
||||
export const bulkImportError = ({ grades: { bulkManagement } }) => (
|
||||
(!!bulkManagement && bulkManagement.errorMessages)
|
||||
? `Errors while processing: ${bulkManagement.errorMessages.join(', ')}`
|
||||
: ''
|
||||
);
|
||||
|
||||
/**
|
||||
* bulkManagementHistory(state)
|
||||
* returns the bulkManagement history entries from the grades state
|
||||
* @param {object} state - redux state
|
||||
* @return {object[]} - list of bulkManagement history entries
|
||||
*/
|
||||
export const bulkManagementHistory = ({ grades: { bulkManagement } }) => (
|
||||
(bulkManagement.history || [])
|
||||
);
|
||||
|
||||
/**
|
||||
* bulkManagementHistoryEntries(state)
|
||||
* returns transformed history entries from bulkManagement grades data.
|
||||
* @param {object} state - redux state
|
||||
* @return {object[]} - list of transformed bulkManagement history entries
|
||||
*/
|
||||
export const bulkManagementHistoryEntries = (state) => (
|
||||
module.bulkManagementHistory(state).map(module.transformHistoryEntry)
|
||||
);
|
||||
|
||||
/**
|
||||
* getExampleSectionBreakdown(state)
|
||||
* returns section breakdown of first grades result.
|
||||
* @param {object} state - redux state
|
||||
* @return {object[]} - section breakdown of first grades result.
|
||||
*/
|
||||
export const getExampleSectionBreakdown = ({ grades }) => (
|
||||
(grades.results[0] || {}).section_breakdown || []
|
||||
);
|
||||
|
||||
const composeFilters = (...predicates) => (percentGrade, options = {}) => predicates.reduce((accum, predicate) => {
|
||||
if (predicate(percentGrade, options)) {
|
||||
return null;
|
||||
}
|
||||
return accum;
|
||||
}, percentGrade);
|
||||
/**
|
||||
* gradeOverrides(state)
|
||||
* returns the gradeOverride history results
|
||||
* @param {object} state - redux state
|
||||
* @return {object[]} - grade override history result entries
|
||||
*/
|
||||
export const gradeOverrides = ({ grades }) => grades.gradeOverrideHistoryResults;
|
||||
|
||||
const percentGradeIsMax = percentGrade => (
|
||||
percentGrade === '100'
|
||||
);
|
||||
|
||||
const percentGradeIsMin = percentGrade => (
|
||||
percentGrade === '0'
|
||||
);
|
||||
|
||||
const assignmentIdIsDefined = (percentGrade, { assignmentId }) => (
|
||||
!assignmentId
|
||||
);
|
||||
|
||||
const formatMaxCourseGrade = composeFilters(percentGradeIsMax);
|
||||
const formatMinCourseGrade = composeFilters(percentGradeIsMin);
|
||||
|
||||
const formatMaxAssignmentGrade = composeFilters(
|
||||
percentGradeIsMax,
|
||||
assignmentIdIsDefined,
|
||||
);
|
||||
|
||||
const formatMinAssignmentGrade = composeFilters(
|
||||
percentGradeIsMin,
|
||||
assignmentIdIsDefined,
|
||||
/**
|
||||
* uploadSuccess(state)
|
||||
* @param {object} state - redux state
|
||||
* @return {bool} - is bulkManagement.uploadSuccess true?
|
||||
*/
|
||||
export const uploadSuccess = ({ grades: { bulkManagement } }) => (
|
||||
!!bulkManagement && bulkManagement.uploadSuccess
|
||||
);
|
||||
|
||||
const simpleSelectors = simpleSelectorFactory(
|
||||
({ grades }) => grades,
|
||||
[
|
||||
'courseId',
|
||||
'filteredUsersCount',
|
||||
'totalUsersCount',
|
||||
'gradeFormat',
|
||||
@@ -117,30 +214,19 @@ const simpleSelectors = simpleSelectorFactory(
|
||||
],
|
||||
);
|
||||
|
||||
const allGrades = ({ grades: { results } }) => results;
|
||||
const uploadSuccess = ({ grades: { bulkManagement } }) => (!!bulkManagement && bulkManagement.uploadSuccess);
|
||||
|
||||
const bulkImportError = ({ grades: { bulkManagement } }) => (
|
||||
(!!bulkManagement && bulkManagement.errorMessages)
|
||||
? `Errors while processing: ${bulkManagement.errorMessages.join(', ')}`
|
||||
: ''
|
||||
);
|
||||
const gradeOverrides = ({ grades }) => grades.gradeOverrideHistoryResults;
|
||||
|
||||
const selectors = {
|
||||
export default StrictDict({
|
||||
bulkImportError,
|
||||
formatGradeOverrideForDisplay,
|
||||
formatMinAssignmentGrade,
|
||||
formatMaxAssignmentGrade,
|
||||
formatMaxCourseGrade,
|
||||
formatMinCourseGrade,
|
||||
getExampleSectionBreakdown,
|
||||
headingMapper,
|
||||
|
||||
...simpleSelectors,
|
||||
allGrades,
|
||||
uploadSuccess,
|
||||
bulkManagementHistoryEntries,
|
||||
getExampleSectionBreakdown,
|
||||
gradeOverrides,
|
||||
};
|
||||
|
||||
export default selectors;
|
||||
uploadSuccess,
|
||||
});
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import selectors from './grades';
|
||||
import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../constants/grades';
|
||||
import { formatDateForDisplay } from '../actions/utils';
|
||||
import * as selectors from './grades';
|
||||
import exportedSelectors from './grades';
|
||||
|
||||
const genericHistoryRow = {
|
||||
id: 5,
|
||||
class_name: 'bulk_grades.api.GradeCSVProcessor',
|
||||
unique_id: 'course-v1:google+goog101+2018_spring',
|
||||
operation: 'commit',
|
||||
user: 'edx',
|
||||
modified: '2019-07-16T20:25:46.700802Z',
|
||||
original_filename: '',
|
||||
data: {
|
||||
total_rows: 5,
|
||||
processed_rows: 3,
|
||||
saved_rows: 3,
|
||||
},
|
||||
};
|
||||
const { minGrade, maxGrade } = selectors;
|
||||
|
||||
const genericResultsRows = [
|
||||
{
|
||||
@@ -48,254 +38,260 @@ const genericResultsRows = [
|
||||
},
|
||||
];
|
||||
|
||||
describe('bulkImportError', () => {
|
||||
it('returns an empty string when bulkManagement not run', () => {
|
||||
const result = selectors.bulkImportError({ grades: { bulkManagement: null } });
|
||||
expect(result).toEqual('');
|
||||
});
|
||||
|
||||
it('returns an empty string when bulkManagement runs without error', () => {
|
||||
const result = selectors.bulkImportError({ grades: { bulkManagement: { uploadSuccess: true } } });
|
||||
expect(result).toEqual('');
|
||||
});
|
||||
|
||||
it('returns error string when bulkManagement encounters an error', () => {
|
||||
const errorMessages = ['error1', 'also error2'];
|
||||
const expectedErrorString = `Errors while processing: ${errorMessages[0]}, ${errorMessages[1]}`;
|
||||
const result = selectors.bulkImportError({ grades: { bulkManagement: { errorMessages } } });
|
||||
expect(result).toEqual(expectedErrorString);
|
||||
});
|
||||
});
|
||||
|
||||
describe('grade formatters', () => {
|
||||
const selectedAssignment = { assignmentId: 'block-v1:edX+type@sequential+block@abcde' };
|
||||
|
||||
describe('formatMinAssignmentGrade', () => {
|
||||
const defaultGrade = '0';
|
||||
const modifiedGrade = '1';
|
||||
|
||||
it('passes numbers through when grade is not default (0) and assignment is supplied', () => {
|
||||
const formattedMinAssignmentGrade = selectors.formatMinAssignmentGrade(modifiedGrade, selectedAssignment);
|
||||
expect(formattedMinAssignmentGrade).toEqual(modifiedGrade);
|
||||
});
|
||||
it('ignores grade when unmodified from default (0)', () => {
|
||||
const formattedMinAssignmentGrade = selectors.formatMinAssignmentGrade(defaultGrade, selectedAssignment);
|
||||
expect(formattedMinAssignmentGrade).toEqual(null);
|
||||
});
|
||||
it('ignores grade when an assignment is not supplied', () => {
|
||||
const formattedMinAssignmentGrade = selectors.formatMinAssignmentGrade(modifiedGrade, {});
|
||||
expect(formattedMinAssignmentGrade).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatMaxAssignmentGrade', () => {
|
||||
const defaultGrade = '100';
|
||||
const modifiedGrade = '99';
|
||||
|
||||
it('passes numbers through when grade is not default (100) and assignment is supplied', () => {
|
||||
const formattedMaxAssignmentGrade = selectors.formatMaxAssignmentGrade(modifiedGrade, selectedAssignment);
|
||||
expect(formattedMaxAssignmentGrade).toEqual(modifiedGrade);
|
||||
});
|
||||
it('ignores grade when unmodified from default (100)', () => {
|
||||
const formattedMaxAssignmentGrade = selectors.formatMaxAssignmentGrade(defaultGrade, selectedAssignment);
|
||||
expect(formattedMaxAssignmentGrade).toEqual(null);
|
||||
});
|
||||
it('ignores grade when an assignment is not supplied', () => {
|
||||
const formattedMaxAssignmentGrade = selectors.formatMaxAssignmentGrade(modifiedGrade, {});
|
||||
expect(formattedMaxAssignmentGrade).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatMinCourseGrade', () => {
|
||||
const defaultGrade = '0';
|
||||
const modifiedGrade = '37';
|
||||
|
||||
it('passes numbers through when grade is not default (0) and assignment is supplied', () => {
|
||||
const formattedMinGrade = selectors.formatMinCourseGrade(modifiedGrade, selectedAssignment);
|
||||
expect(formattedMinGrade).toEqual(modifiedGrade);
|
||||
});
|
||||
it('ignores grade when unmodified from default (0)', () => {
|
||||
const formattedMinGrade = selectors.formatMinCourseGrade(defaultGrade, selectedAssignment);
|
||||
expect(formattedMinGrade).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatMaxCourseGrade', () => {
|
||||
const defaultGrade = '100';
|
||||
const modifiedGrade = '42';
|
||||
|
||||
it('passes numbers through when grade is not default (100) and assignment is supplied', () => {
|
||||
const formattedMaxGrade = selectors.formatMaxCourseGrade(modifiedGrade, selectedAssignment);
|
||||
expect(formattedMaxGrade).toEqual(modifiedGrade);
|
||||
});
|
||||
it('ignores unmodified grades', () => {
|
||||
const formattedMaxGrade = selectors.formatMaxCourseGrade(defaultGrade, selectedAssignment);
|
||||
expect(formattedMaxGrade).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExampleSectionBreakdown', () => {
|
||||
const gradesData = {
|
||||
next: null,
|
||||
previous: null,
|
||||
results: [
|
||||
{
|
||||
section_breakdown: [
|
||||
{
|
||||
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)',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('returns an empty array when results are unavailable', () => {
|
||||
const result = selectors.getExampleSectionBreakdown({ grades: { results: [{}] } });
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns an empty array when breakdowns are unavailable', () => {
|
||||
const result = selectors.getExampleSectionBreakdown({ grades: { results: [{ foo: 'bar' }] } });
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('gets section breakdown when available', () => {
|
||||
const result = selectors.getExampleSectionBreakdown({ grades: gradesData });
|
||||
expect(result).toEqual([{
|
||||
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)',
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('headingMapper', () => {
|
||||
const expectedHeaders = (subsectionLabels) => (['Username', 'Email', ...subsectionLabels, 'Total Grade (%)']);
|
||||
|
||||
it('creates headers for all assignments when no filtering is applied', () => {
|
||||
const allSubsectionLabels = ['HW 01', 'HW 02', 'Lab 01'];
|
||||
const headingMapper = selectors.headingMapper('All');
|
||||
const headers = headingMapper(genericResultsRows);
|
||||
expect(headers).toEqual(expectedHeaders(allSubsectionLabels));
|
||||
});
|
||||
it('creates headers for only matching assignment types when type filter is applied', () => {
|
||||
const homeworkHeaders = ['HW 01', 'HW 02'];
|
||||
const headingMapper = selectors.headingMapper('Homework');
|
||||
const headers = headingMapper(genericResultsRows);
|
||||
expect(headers).toEqual(expectedHeaders(homeworkHeaders));
|
||||
});
|
||||
it('creates headers for only matching assignment when label filter is applied', () => {
|
||||
const homeworkHeader = ['HW 02'];
|
||||
const headingMapper = selectors.headingMapper('Homework', 'HW 02');
|
||||
const headers = headingMapper(genericResultsRows);
|
||||
expect(headers).toEqual(expectedHeaders(homeworkHeader));
|
||||
});
|
||||
it('returns an empty array when no entries are passed', () => {
|
||||
const headingMapper = selectors.headingMapper('All');
|
||||
const headers = headingMapper(undefined);
|
||||
expect(headers).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('simpleSelectors', () => {
|
||||
const simpleSelectorState = {
|
||||
grades: {
|
||||
filteredUsersCount: 9000,
|
||||
totalUsersCount: 9001,
|
||||
gradeFormat: 'percent',
|
||||
showSpinner: false,
|
||||
gradeOverrideCurrentEarnedGradedOverride: null,
|
||||
gradeOverrideHistoryError: null,
|
||||
gradeOriginalEarnedGraded: null,
|
||||
gradeOriginalPossibleGraded: null,
|
||||
showSuccess: false,
|
||||
},
|
||||
};
|
||||
|
||||
it('selects simple data by name from grades state', () => {
|
||||
const expectedFilteredUsers = 9000;
|
||||
const expectedTotalUsers = 9001;
|
||||
const expectedGradeFormat = 'percent';
|
||||
|
||||
// the selector factory is already tested, this just exercises some of these mappings
|
||||
expect(selectors.filteredUsersCount(simpleSelectorState)).toEqual(expectedFilteredUsers);
|
||||
expect(selectors.totalUsersCount(simpleSelectorState)).toEqual(expectedTotalUsers);
|
||||
expect(selectors.gradeFormat(simpleSelectorState)).toEqual(expectedGradeFormat);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadSuccess', () => {
|
||||
it('shows an upload success when bulk management data returned and completed successfully', () => {
|
||||
const uploadSuccess = selectors.uploadSuccess({ grades: { bulkManagement: { uploadSuccess: true } } });
|
||||
expect(uploadSuccess).toEqual(true);
|
||||
});
|
||||
it('returns false when bulk management data not returned', () => {
|
||||
const uploadSuccess = selectors.uploadSuccess({ grades: {} });
|
||||
expect(uploadSuccess).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkManagementHistoryEntries', () => {
|
||||
it('handles history being as-yet unloaded', () => {
|
||||
const result = selectors.bulkManagementHistoryEntries({ grades: { bulkManagement: {} } });
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('formats dates for us', () => {
|
||||
const result = selectors.bulkManagementHistoryEntries({
|
||||
grades: {
|
||||
bulkManagement: {
|
||||
history: [
|
||||
genericHistoryRow,
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const [{ timeUploaded }] = result;
|
||||
expect(timeUploaded).not.toMatch(/Z$/);
|
||||
expect(timeUploaded).toContain(' at ');
|
||||
});
|
||||
|
||||
const exerciseGetRowsProcessed = (input, expectation) => {
|
||||
const result = selectors.bulkManagementHistoryEntries({
|
||||
grades: {
|
||||
bulkManagement: {
|
||||
history: [
|
||||
{ ...genericHistoryRow, data: input },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const [{ summaryOfRowsProcessed }] = result;
|
||||
expect(summaryOfRowsProcessed).toEqual(expect.objectContaining(expectation));
|
||||
};
|
||||
|
||||
it('calculates skipped rows', () => {
|
||||
exerciseGetRowsProcessed({
|
||||
total_rows: 100,
|
||||
processed_rows: 10,
|
||||
describe('grades selectors', () => {
|
||||
// Transformers
|
||||
describe('getRowsProcessed', () => {
|
||||
const data = {
|
||||
processed_rows: 20,
|
||||
saved_rows: 10,
|
||||
}, {
|
||||
skipped: 90,
|
||||
total_rows: 50,
|
||||
};
|
||||
expect(selectors.getRowsProcessed(data)).toEqual({
|
||||
total: data.total_rows,
|
||||
successfullyProcessed: data.saved_rows,
|
||||
failed: data.processed_rows - data.saved_rows,
|
||||
skipped: data.total_rows - data.processed_rows,
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates failures', () => {
|
||||
exerciseGetRowsProcessed({
|
||||
total_rows: 10,
|
||||
processed_rows: 100,
|
||||
saved_rows: 10,
|
||||
}, {
|
||||
failed: 90,
|
||||
describe('grade formatters', () => {
|
||||
const selectedAssignment = { assignmentId: 'block-v1:edX+type@sequential+block@abcde' };
|
||||
|
||||
describe('formatMinAssignmentGrade', () => {
|
||||
const modifiedGrade = '1';
|
||||
const selector = selectors.formatMinAssignmentGrade;
|
||||
it('passes grade through when not min (0) and assignment is supplied', () => {
|
||||
expect(selector(modifiedGrade, selectedAssignment)).toEqual(modifiedGrade);
|
||||
});
|
||||
it('returns null for min grade', () => {
|
||||
expect(selector(minGrade, selectedAssignment)).toEqual(null);
|
||||
});
|
||||
it('returns null when assignment is not supplied', () => {
|
||||
expect(selector(modifiedGrade, {})).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatMaxAssignmentGrade', () => {
|
||||
const modifiedGrade = '99';
|
||||
const selector = selectors.formatMaxAssignmentGrade;
|
||||
it('passes grade through when not max (100) and assignment is supplied', () => {
|
||||
expect(selector(modifiedGrade, selectedAssignment)).toEqual(modifiedGrade);
|
||||
});
|
||||
it('returns null for max grade', () => {
|
||||
expect(selector(maxGrade, selectedAssignment)).toEqual(null);
|
||||
});
|
||||
it('returns null when assignment is not supplied', () => {
|
||||
expect(selector(modifiedGrade, {})).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatMinCourseGrade', () => {
|
||||
const modifiedGrade = '37';
|
||||
const selector = selectors.formatMinCourseGrade;
|
||||
it('passes grades through when not min (0) and assignment is supplied', () => {
|
||||
expect(selector(modifiedGrade, selectedAssignment)).toEqual(modifiedGrade);
|
||||
});
|
||||
it('returns null for min grade', () => {
|
||||
expect(selector(minGrade, selectedAssignment)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatMaxCourseGrade', () => {
|
||||
const modifiedGrade = '42';
|
||||
const selector = selectors.formatMaxCourseGrade;
|
||||
it('passes grades through when not max and assignment is supplied', () => {
|
||||
expect(selector(modifiedGrade, selectedAssignment)).toEqual(modifiedGrade);
|
||||
});
|
||||
it('returns null for max grade', () => {
|
||||
expect(selector(maxGrade, selectedAssignment)).toEqual(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('headingMapper', () => {
|
||||
const expectedHeaders = (subsectionLabels) => ([
|
||||
USERNAME_HEADING,
|
||||
EMAIL_HEADING,
|
||||
...subsectionLabels,
|
||||
TOTAL_COURSE_GRADE_HEADING,
|
||||
]);
|
||||
|
||||
const rows = genericResultsRows;
|
||||
const selector = selectors.headingMapper;
|
||||
it('creates headers for all assignments when no filtering is applied', () => {
|
||||
expect(selector('All')(genericResultsRows)).toEqual(
|
||||
expectedHeaders([rows[0].label, rows[1].label, rows[2].label]),
|
||||
);
|
||||
});
|
||||
it('creates headers for only matching assignment types when type filter is applied', () => {
|
||||
expect(
|
||||
selector('Homework')(genericResultsRows),
|
||||
).toEqual(
|
||||
expectedHeaders([rows[0].label, rows[1].label]),
|
||||
);
|
||||
});
|
||||
it('creates headers for only matching assignment when label filter is applied', () => {
|
||||
expect(selector('Homework', rows[1].label)(rows)).toEqual(
|
||||
expectedHeaders([rows[1].label]),
|
||||
);
|
||||
});
|
||||
it('returns an empty array when no entries are passed', () => {
|
||||
expect(selector('all')(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformHistoryEntry', () => {
|
||||
let getRowsProcessed;
|
||||
let output;
|
||||
const rowsProcessed = ['some', 'fake', 'rows'];
|
||||
const rawEntry = {
|
||||
modified: 'Jan 10 2021',
|
||||
original_filename: 'fileName',
|
||||
data: { some: 'data' },
|
||||
also: 'some',
|
||||
other: 'fields',
|
||||
};
|
||||
beforeEach(() => {
|
||||
getRowsProcessed = selectors.getRowsProcessed;
|
||||
selectors.getRowsProcessed = jest.fn(data => ({ data, rowsProcessed }));
|
||||
output = selectors.transformHistoryEntry(rawEntry);
|
||||
});
|
||||
afterEach(() => {
|
||||
selectors.getRowsProcessed = getRowsProcessed;
|
||||
});
|
||||
it('transforms modified into timeUploaded', () => {
|
||||
expect(output.timeUploaded).toEqual(formatDateForDisplay(new Date(rawEntry.modified)));
|
||||
});
|
||||
it('forwards filename', () => {
|
||||
expect(output.originalFilename).toEqual(rawEntry.original_filename);
|
||||
});
|
||||
it('summarizes processed rows', () => {
|
||||
expect(output.summaryOfRowsProcessed).toEqual(selectors.getRowsProcessed(rawEntry.data));
|
||||
});
|
||||
});
|
||||
|
||||
// Selectors
|
||||
describe('allGrades', () => {
|
||||
it('returns the grades results from redux state', () => {
|
||||
const results = ['some', 'fake', 'results'];
|
||||
expect(selectors.allGrades({ grades: { results } })).toEqual(results);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkImportError', () => {
|
||||
it('returns an empty string when bulkManagement not run', () => {
|
||||
expect(
|
||||
selectors.bulkImportError({ grades: { bulkManagement: null } }),
|
||||
).toEqual('');
|
||||
});
|
||||
|
||||
it('returns an empty string when bulkManagement runs without error', () => {
|
||||
expect(
|
||||
selectors.bulkImportError({ grades: { bulkManagement: { uploadSuccess: true } } }),
|
||||
).toEqual('');
|
||||
});
|
||||
|
||||
it('returns error string when bulkManagement encounters an error', () => {
|
||||
const errorMessages = ['error1', 'also error2'];
|
||||
expect(
|
||||
selectors.bulkImportError({ grades: { bulkManagement: { errorMessages } } }),
|
||||
).toEqual(
|
||||
`Errors while processing: ${errorMessages[0]}, ${errorMessages[1]}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkManagementHistory', () => {
|
||||
const selector = selectors.bulkManagementHistory;
|
||||
it('returns history entries from grades.bulkManagement in redux store', () => {
|
||||
const history = ['a', 'few', 'history', 'entries'];
|
||||
expect(
|
||||
selector({ grades: { bulkManagement: { history } } }),
|
||||
).toEqual(history);
|
||||
});
|
||||
it('returns an empty list if not set', () => {
|
||||
expect(
|
||||
selector({ grades: { bulkManagement: {} } }),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkManagementHistoryEntries', () => {
|
||||
let bulkManagementHistory;
|
||||
let transformHistoryEntry;
|
||||
const listFn = (state) => state.entries;
|
||||
const mapFn = (entry) => ([entry]);
|
||||
const entries = ['some', 'entries', 'for', 'testing'];
|
||||
beforeEach(() => {
|
||||
bulkManagementHistory = selectors.bulkManagementHistory;
|
||||
transformHistoryEntry = selectors.transformHistoryEntry;
|
||||
selectors.bulkManagementHistory = jest.fn(listFn);
|
||||
selectors.transformHistoryEntry = jest.fn(mapFn);
|
||||
});
|
||||
afterEach(() => {
|
||||
selectors.bulkManagementHistory = bulkManagementHistory;
|
||||
selectors.transformHistoryEntry = transformHistoryEntry;
|
||||
});
|
||||
it('returns history entries mapped to transformer', () => {
|
||||
expect(
|
||||
selectors.bulkManagementHistoryEntries({ entries }),
|
||||
).toEqual(entries.map(mapFn));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExampleSectionBreakdown', () => {
|
||||
const selector = selectors.getExampleSectionBreakdown;
|
||||
it('returns an empty array when results are unavailable', () => {
|
||||
expect(selector({ grades: { results: [] } })).toEqual([]);
|
||||
});
|
||||
it('returns an empty array when breakdowns are unavailable', () => {
|
||||
expect(selector({ grades: { results: [{ foo: 'bar' }] } })).toEqual([]);
|
||||
});
|
||||
it('gets section breakdown when available', () => {
|
||||
const sectionBreakdown = { fake: 'section', breakdown: 'data' };
|
||||
expect(
|
||||
selector({ grades: { results: [{ section_breakdown: sectionBreakdown }] } }),
|
||||
).toEqual(sectionBreakdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gradeOverrides', () => {
|
||||
it('returns grades.gradeOverrideHistoryResults from redux state', () => {
|
||||
const testVal = 'Temp Test VALUE';
|
||||
expect(
|
||||
selectors.gradeOverrides({ grades: { gradeOverrideHistoryResults: testVal } }),
|
||||
).toEqual(testVal);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadSuccess', () => {
|
||||
const selector = selectors.uploadSuccess;
|
||||
it('shows upload success when bulkManagement data returned/completed successfully', () => {
|
||||
expect(selector({ grades: { bulkManagement: { uploadSuccess: true } } })).toEqual(true);
|
||||
});
|
||||
it('returns false when bulk management data not returned', () => {
|
||||
expect(selector({ grades: {} })).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('simpleSelectors', () => {
|
||||
const testVal = 'some TEST value';
|
||||
const testSimpleSelector = (key) => {
|
||||
test(key, () => {
|
||||
expect(
|
||||
exportedSelectors[key]({ grades: { [key]: testVal } }),
|
||||
).toEqual(testVal);
|
||||
});
|
||||
};
|
||||
testSimpleSelector('courseId');
|
||||
testSimpleSelector('filteredUsersCount');
|
||||
testSimpleSelector('totalUsersCount');
|
||||
testSimpleSelector('gradeFormat');
|
||||
testSimpleSelector('showSpinner');
|
||||
testSimpleSelector('gradeOverrideCurrentEarnedGradedOverride');
|
||||
testSimpleSelector('gradeOverrideHistoryError');
|
||||
testSimpleSelector('gradeOriginalEarnedGraded');
|
||||
testSimpleSelector('gradeOriginalPossibleGraded');
|
||||
testSimpleSelector('showSuccess');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/* eslint-disable import/no-named-as-default-member, import/no-self-import */
|
||||
import { StrictDict } from 'utils';
|
||||
import LmsApiService from 'data/services/LmsApiService';
|
||||
|
||||
import * as module from '.';
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
import cohorts from './cohorts';
|
||||
import filters from './filters';
|
||||
@@ -8,7 +11,13 @@ import roles from './roles';
|
||||
import special from './special';
|
||||
import tracks from './tracks';
|
||||
|
||||
const lmsApiServiceArgs = (state) => ({
|
||||
/**
|
||||
* lmsApiServiceArgs(state)
|
||||
* Returns common lms api service request args.
|
||||
* @param {object} state - redux state
|
||||
* @return {object} lms api query params object
|
||||
*/
|
||||
export const lmsApiServiceArgs = (state) => ({
|
||||
cohort: cohorts.getCohortNameById(state, filters.cohort(state)),
|
||||
assignment: filters.selectedAssignmentId(state),
|
||||
assignmentType: filters.assignmentType(state),
|
||||
@@ -24,36 +33,71 @@ const lmsApiServiceArgs = (state) => ({
|
||||
courseGradeMax: grades.formatMaxCourseGrade(filters.courseGradeMax(state)),
|
||||
});
|
||||
|
||||
const gradeExportUrl = (state, { courseId }) => (
|
||||
/**
|
||||
* gradeExportUrl(state, options)
|
||||
* Returns the output of getGradeExportCsvUrl, applying the current includeCourseRoleMembers
|
||||
* filter.
|
||||
* @param {object} state - redux state
|
||||
* @param {object} options - options object of the form ({ courseId })
|
||||
* @return {string} - generated grade export url
|
||||
*/
|
||||
export const gradeExportUrl = (state, { courseId }) => (
|
||||
LmsApiService.getGradeExportCsvUrl(courseId, {
|
||||
...lmsApiServiceArgs(state),
|
||||
...module.lmsApiServiceArgs(state),
|
||||
excludeCourseRoles: filters.includeCourseRoleMembers(state) ? '' : 'all',
|
||||
})
|
||||
);
|
||||
|
||||
const interventionExportUrl = (state, { courseId }) => (
|
||||
/**
|
||||
* interventionExportUrl(state, options)
|
||||
* Returns the output of getInterventionExportUrl.
|
||||
* @param {object} state - redux state
|
||||
* @param {object} options - options object of the form ({ courseId })
|
||||
* @return {string} - generated intervention export url
|
||||
*/
|
||||
export const interventionExportUrl = (state, { courseId }) => (
|
||||
LmsApiService.getInterventionExportCsvUrl(
|
||||
courseId,
|
||||
lmsApiServiceArgs(state),
|
||||
module.lmsApiServiceArgs(state),
|
||||
)
|
||||
);
|
||||
|
||||
const showBulkManagement = (state, { courseId }) => (
|
||||
special.hasSpecialBulkManagementAccess(courseId)
|
||||
|| (tracks.stateHasMastersTrack(state) && state.config.bulkManagementAvailable)
|
||||
);
|
||||
|
||||
const shouldShowSpinner = (state) => {
|
||||
const canView = roles.canUserViewGradebook(state);
|
||||
return canView && grades.showSpinner(state);
|
||||
};
|
||||
|
||||
const getHeadings = (state) => grades.headingMapper(
|
||||
/**
|
||||
* getHeadings(state)
|
||||
* Returns the table headings given the current assignmentType and assignmentLabel filters.
|
||||
* @param {object} state - redux state
|
||||
* @return {string[]} - array of table headings
|
||||
*/
|
||||
export const getHeadings = (state) => grades.headingMapper(
|
||||
filters.assignmentType(state) || 'All',
|
||||
filters.selectedAssignmentLabel(state) || 'All',
|
||||
)(grades.getExampleSectionBreakdown(state));
|
||||
|
||||
export default {
|
||||
/**
|
||||
* showBulkManagement(state, options)
|
||||
* Returns true iff the user has special access or bulk management is configured to be available
|
||||
* and the course has a masters track.
|
||||
* @param {object} state - redux state
|
||||
* @param {object} options - options object of the form ({ courseId })
|
||||
* @return {bool} - should show bulk management controls?
|
||||
*/
|
||||
export const showBulkManagement = (state, { courseId }) => (
|
||||
special.hasSpecialBulkManagementAccess(courseId)
|
||||
|| (tracks.stateHasMastersTrack(state) && state.config.bulkManagementAvailable)
|
||||
);
|
||||
|
||||
/**
|
||||
* shouldShowSpinner(state)
|
||||
* Returns true iff the user can view the gradebook and grades.showSpinner is true.
|
||||
* @param {object} state - redux state
|
||||
* @return {bool} - should show spinner?
|
||||
*/
|
||||
export const shouldShowSpinner = (state) => (
|
||||
roles.canUserViewGradebook(state)
|
||||
&& grades.showSpinner(state)
|
||||
);
|
||||
|
||||
export default StrictDict({
|
||||
root: {
|
||||
getHeadings,
|
||||
gradeExportUrl,
|
||||
@@ -68,4 +112,4 @@ export default {
|
||||
roles,
|
||||
special,
|
||||
tracks,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,156 +1,217 @@
|
||||
/* eslint-disable import/no-named-as-default-member */
|
||||
import selectors from '.';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import * as moduleSelectors from '.';
|
||||
|
||||
describe('root', () => {
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getGradeExportCsvUrl: jest.fn(
|
||||
(...args) => ({ getGradeExportCsvUrl: { args } }),
|
||||
),
|
||||
getInterventionExportCsvUrl: jest.fn(
|
||||
(...args) => ({ getInterventionExportCsvUrl: { args } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockFn = (key) => jest.fn((state) => ({ [key]: state }));
|
||||
const mockMetaFn = (key) => jest.fn((...args) => ({ [key]: { args } }));
|
||||
const testState = { a: 'test', state: 'of', random: 'data' };
|
||||
|
||||
describe('root selectors', () => {
|
||||
const testCourseId = 'OxfordX+Time+Travel';
|
||||
|
||||
const mockAssignmentId = 'block-v1:edX+Term+type@sequential+block@1';
|
||||
const mockAssignmentType = 'Homework';
|
||||
const mockAssignmentLabel = 'HW 42';
|
||||
const mockCohort = 'test cohort';
|
||||
|
||||
const baseApiArgs = {
|
||||
assignment: mockAssignmentId,
|
||||
assignmentGradeMax: '99',
|
||||
assignmentGradeMin: '1',
|
||||
assignmentType: mockAssignmentType,
|
||||
cohort: mockCohort,
|
||||
courseGradeMax: '98',
|
||||
courseGradeMin: '2',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
selectors.cohorts.getCohortNameById = jest.fn(() => mockCohort);
|
||||
selectors.filters.assignmentType = jest.fn(() => mockAssignmentType);
|
||||
selectors.filters.includeCourseRoleMembers = jest.fn();
|
||||
selectors.filters.selectedAssignmentId = jest.fn(() => mockAssignmentId);
|
||||
selectors.grades.formatMaxAssignmentGrade = jest.fn(() => '99');
|
||||
selectors.grades.formatMinAssignmentGrade = jest.fn(() => '1');
|
||||
selectors.grades.formatMaxCourseGrade = jest.fn(() => '98');
|
||||
selectors.grades.formatMinCourseGrade = jest.fn(() => '2');
|
||||
|
||||
// Internal functions, intentionally left blank
|
||||
selectors.filters.assignmentGradeMax = jest.fn();
|
||||
selectors.filters.assignmentGradeMin = jest.fn();
|
||||
selectors.filters.cohort = jest.fn();
|
||||
selectors.filters.courseGradeMin = jest.fn();
|
||||
selectors.filters.courseGradeMax = jest.fn();
|
||||
});
|
||||
|
||||
describe('getHeadings', () => {
|
||||
const mockHeadingMapper = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
// Note: the mock setup for this is gross which I'd argue speaks to the need for refactoring.
|
||||
mockHeadingMapper.mockReturnValue(() => (() => []));
|
||||
|
||||
selectors.grades.headingMapper = mockHeadingMapper;
|
||||
selectors.filters.selectedAssignmentLabel = jest.fn();
|
||||
selectors.grades.getExampleSectionBreakdown = jest.fn();
|
||||
});
|
||||
|
||||
it('uses all assignment types for creating headings when no type/assignment filters are supplied', () => {
|
||||
selectors.filters.assignmentType.mockReturnValue(undefined);
|
||||
selectors.filters.selectedAssignmentLabel.mockReturnValue(undefined);
|
||||
selectors.root.getHeadings({});
|
||||
expect(mockHeadingMapper).toHaveBeenCalledWith('All', 'All');
|
||||
});
|
||||
|
||||
it('filters headings by assignment type when type filter is applied', () => {
|
||||
selectors.filters.assignmentType.mockReturnValue(mockAssignmentType);
|
||||
selectors.filters.selectedAssignmentLabel.mockReturnValue(undefined);
|
||||
selectors.root.getHeadings({});
|
||||
expect(mockHeadingMapper).toHaveBeenCalledWith(mockAssignmentType, 'All');
|
||||
});
|
||||
|
||||
it('filters headings by assignment when a type and assignment filter are applied', () => {
|
||||
selectors.filters.assignmentType.mockReturnValue(mockAssignmentType);
|
||||
selectors.filters.selectedAssignmentLabel.mockReturnValue(mockAssignmentLabel);
|
||||
selectors.root.getHeadings({});
|
||||
expect(mockHeadingMapper).toHaveBeenCalledWith(mockAssignmentType, mockAssignmentLabel);
|
||||
describe('lmsApiServiceArgs', () => {
|
||||
it('gathers formatted data from state for various lms api service calls', () => {
|
||||
selectors.cohorts.getCohortNameById = mockFn('getCohortNameById');
|
||||
selectors.filters.selectedAssignmentId = mockFn('selectedAssignmentId');
|
||||
selectors.filters.assignmentType = mockFn('assignmentType');
|
||||
selectors.filters.cohort = mockFn('cohort');
|
||||
selectors.filters.assignmentGradeMax = mockFn('assignmentGradeMax');
|
||||
selectors.filters.assignmentGradeMin = mockFn('assignmentGradeMin');
|
||||
selectors.filters.courseGradeMax = mockFn('courseGradeMax');
|
||||
selectors.filters.courseGradeMin = mockFn('courseGradeMin');
|
||||
selectors.grades.formatMaxAssignmentGrade = mockMetaFn('formatMaxAssignmentGrade');
|
||||
selectors.grades.formatMinAssignmentGrade = mockMetaFn('formatMinAssignmentGrade');
|
||||
selectors.grades.formatMinCourseGrade = mockFn('formatMinCourseGrade');
|
||||
selectors.grades.formatMaxCourseGrade = mockFn('formatMaxCourseGrade');
|
||||
const assignmentId = { selectedAssignmentId: testState };
|
||||
expect(moduleSelectors.lmsApiServiceArgs(testState)).toEqual({
|
||||
cohort: { getCohortNameById: testState },
|
||||
assignment: assignmentId,
|
||||
assignmentType: { assignmentType: testState },
|
||||
assignmentGradeMin: {
|
||||
formatMinAssignmentGrade: {
|
||||
args: [{ assignmentGradeMin: testState }, { assignmentId }],
|
||||
},
|
||||
},
|
||||
assignmentGradeMax: {
|
||||
formatMaxAssignmentGrade: {
|
||||
args: [{ assignmentGradeMax: testState }, { assignmentId }],
|
||||
},
|
||||
},
|
||||
courseGradeMin: {
|
||||
formatMinCourseGrade: { courseGradeMin: testState },
|
||||
},
|
||||
courseGradeMax: {
|
||||
formatMaxCourseGrade: { courseGradeMax: testState },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('gradeExportUrl', () => {
|
||||
it('calls the API service with the right args, excluding all course roles', () => {
|
||||
const testState = {};
|
||||
const mockGetExportUrl = jest.fn();
|
||||
const expectedApiArgs = { ...baseApiArgs, excludeCourseRoles: 'all' };
|
||||
|
||||
LmsApiService.getGradeExportCsvUrl = mockGetExportUrl;
|
||||
|
||||
selectors.root.gradeExportUrl(testState, { courseId: testCourseId });
|
||||
expect(mockGetExportUrl).toHaveBeenCalledWith(testCourseId, expectedApiArgs);
|
||||
const selector = moduleSelectors.gradeExportUrl;
|
||||
let lmsApiServiceArgs;
|
||||
beforeEach(() => {
|
||||
selectors.filters.includeCourseRoleMembers = jest.fn();
|
||||
lmsApiServiceArgs = moduleSelectors.lmsApiServiceArgs;
|
||||
moduleSelectors.lmsApiServiceArgs = jest.fn(state => ({ lmsArgs: state }));
|
||||
});
|
||||
it('calls the API service with the right args, including course roles when the option is selected', () => {
|
||||
const testState = {};
|
||||
const mockGetExportUrl = jest.fn();
|
||||
const expectedApiArgs = { ...baseApiArgs, excludeCourseRoles: '' };
|
||||
selectors.filters.includeCourseRoleMembers.mockReturnValue(true);
|
||||
|
||||
LmsApiService.getGradeExportCsvUrl = mockGetExportUrl;
|
||||
|
||||
selectors.root.gradeExportUrl(testState, { courseId: testCourseId });
|
||||
expect(mockGetExportUrl).toHaveBeenCalledWith(testCourseId, expectedApiArgs);
|
||||
afterEach(() => {
|
||||
moduleSelectors.lmsApiServiceArgs = lmsApiServiceArgs;
|
||||
});
|
||||
describe('without includeCourseRoleMembers filter', () => {
|
||||
it('calls the API service with the right args, excluding all course roles', () => {
|
||||
selectors.filters.includeCourseRoleMembers.mockReturnValue(undefined);
|
||||
expect(selector(testState, { courseId: testCourseId })).toEqual({
|
||||
getGradeExportCsvUrl: {
|
||||
args: [testCourseId, { lmsArgs: testState, excludeCourseRoles: 'all' }],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('with includeCourseRoleMembers filter', () => {
|
||||
it('calls the API service with the right args, including course roles', () => {
|
||||
selectors.filters.includeCourseRoleMembers.mockReturnValue(true);
|
||||
expect(selector(testState, { courseId: testCourseId })).toEqual({
|
||||
getGradeExportCsvUrl: {
|
||||
args: [testCourseId, { lmsArgs: testState, excludeCourseRoles: '' }],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('interventionExportUrl', () => {
|
||||
it('calls the API service with the right args', () => {
|
||||
const testState = {};
|
||||
const mockGetExportUrl = jest.fn();
|
||||
const { lmsApiServiceArgs } = moduleSelectors;
|
||||
selectors.filters.includeCourseRoleMembers = jest.fn();
|
||||
moduleSelectors.lmsApiServiceArgs = jest.fn(state => ({ lmsArgs: state }));
|
||||
expect(
|
||||
moduleSelectors.interventionExportUrl(testState, { courseId: testCourseId }),
|
||||
).toEqual({
|
||||
getInterventionExportCsvUrl: {
|
||||
args: [testCourseId, { lmsArgs: testState }],
|
||||
},
|
||||
});
|
||||
moduleSelectors.lmsApiServiceArgs = lmsApiServiceArgs;
|
||||
});
|
||||
});
|
||||
|
||||
LmsApiService.getInterventionExportCsvUrl = mockGetExportUrl;
|
||||
|
||||
selectors.root.interventionExportUrl(testState, { courseId: testCourseId });
|
||||
expect(mockGetExportUrl).toHaveBeenCalledWith(testCourseId, baseApiArgs);
|
||||
describe('getHeadings', () => {
|
||||
const selector = moduleSelectors.getHeadings;
|
||||
beforeEach(() => {
|
||||
selectors.grades.headingMapper = jest.fn(
|
||||
(type, label) => (breakdown) => ({ headingMapper: { type, label, breakdown } }),
|
||||
);
|
||||
selectors.filters.assignmentType = jest.fn();
|
||||
selectors.filters.selectedAssignmentLabel = jest.fn();
|
||||
selectors.grades.getExampleSectionBreakdown = mockFn('getExampleSectionBreakdown');
|
||||
});
|
||||
describe('no assignmentType or label selected', () => {
|
||||
it('maps selected filters into getExampleSectionBreakdown', () => {
|
||||
selectors.filters.assignmentType.mockReturnValue(undefined);
|
||||
selectors.filters.selectedAssignmentLabel.mockReturnValue(undefined);
|
||||
expect(selector(testState)).toEqual({
|
||||
headingMapper: {
|
||||
type: 'All',
|
||||
label: 'All',
|
||||
breakdown: { getExampleSectionBreakdown: testState },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('assignmentType and label selected', () => {
|
||||
it('maps selected filters into getExampleSectionBreakdown', () => {
|
||||
selectors.filters.assignmentType.mockReturnValue(mockAssignmentType);
|
||||
selectors.filters.selectedAssignmentLabel.mockReturnValue(mockAssignmentLabel);
|
||||
expect(selector(testState)).toEqual({
|
||||
headingMapper: {
|
||||
type: mockAssignmentType,
|
||||
label: mockAssignmentLabel,
|
||||
breakdown: { getExampleSectionBreakdown: testState },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('showBulkManagement', () => {
|
||||
let state = {};
|
||||
const mockCourseInfo = { courseId: 'foo' };
|
||||
|
||||
beforeEach(() => {
|
||||
const templateState = { config: { bulkManagementAvailable: true } };
|
||||
state = { ...templateState };
|
||||
|
||||
selectors.special.hasSpecialBulkManagementAccess = jest.fn(() => (false));
|
||||
selectors.tracks.stateHasMastersTrack = jest.fn(() => (false));
|
||||
const mockAccess = (val) => {
|
||||
selectors.special.hasSpecialBulkManagementAccess = jest.fn(() => val);
|
||||
};
|
||||
const mockHasMastersTrack = (val) => {
|
||||
selectors.tracks.stateHasMastersTrack = jest.fn(() => val);
|
||||
};
|
||||
const selector = moduleSelectors.showBulkManagement;
|
||||
const mkState = (bulkManagementAvailable) => ({ config: { bulkManagementAvailable } });
|
||||
describe('user has special bulk management access', () => {
|
||||
it('returns true', () => {
|
||||
mockAccess(true);
|
||||
mockHasMastersTrack(false);
|
||||
expect(selector(mkState(true), { courseId: testCourseId })).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show bulk management when the course does not have a masters track', () => {
|
||||
expect(selectors.root.showBulkManagement(state, mockCourseInfo)).toEqual(false);
|
||||
});
|
||||
it('shows bulk management when the course has a masters track', () => {
|
||||
selectors.tracks.stateHasMastersTrack = jest.fn(() => (true));
|
||||
expect(selectors.root.showBulkManagement(state, mockCourseInfo)).toEqual(true);
|
||||
});
|
||||
it('shows bulk management when a course is configured for special access, regardless of other settings', () => {
|
||||
selectors.special.hasSpecialBulkManagementAccess = jest.fn(() => (true));
|
||||
expect(selectors.root.showBulkManagement(state, mockCourseInfo)).toEqual(true);
|
||||
});
|
||||
it('does not show bulk management when bulk management is not available', () => {
|
||||
selectors.tracks.stateHasMastersTrack = jest.fn(() => (true));
|
||||
state.config.bulkManagementAvailable = false;
|
||||
expect(selectors.root.showBulkManagement(state, mockCourseInfo)).toEqual(false);
|
||||
describe('user does not have special access', () => {
|
||||
beforeEach(() => {
|
||||
mockAccess(false);
|
||||
});
|
||||
describe('course has a masters track, but bulkManagement not available', () => {
|
||||
it('returns false', () => {
|
||||
mockHasMastersTrack(true);
|
||||
expect(selector(mkState(false), { courseId: testCourseId })).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('course does not have a masters track, but bulkManagement available', () => {
|
||||
it('returns false', () => {
|
||||
mockHasMastersTrack(false);
|
||||
expect(selector(mkState(true), { courseId: testCourseId })).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('course has a masters track, and bulkManagement is available', () => {
|
||||
it('returns false', () => {
|
||||
mockHasMastersTrack(true);
|
||||
expect(selector(mkState(true), { courseId: testCourseId })).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldShowSpinner', () => {
|
||||
it('does not show the spinner if the user cannot view Gradebook', () => {
|
||||
selectors.roles.canUserViewGradebook = jest.fn(() => (false));
|
||||
expect(selectors.root.shouldShowSpinner()).toEqual(false);
|
||||
const selector = moduleSelectors.shouldShowSpinner;
|
||||
const testSelector = (canView, showSpinner, expected) => {
|
||||
selectors.roles.canUserViewGradebook = jest.fn(() => canView);
|
||||
selectors.grades.showSpinner = jest.fn(() => showSpinner);
|
||||
expect(selector(testState)).toEqual(expected);
|
||||
};
|
||||
describe('user can view gradebook, but showSpinner is false', () => {
|
||||
it('returns false', () => {
|
||||
testSelector(true, false, false);
|
||||
});
|
||||
});
|
||||
it('shows the spinner when a grades task is processing', () => {
|
||||
selectors.roles.canUserViewGradebook = jest.fn(() => (true));
|
||||
selectors.grades.showSpinner = jest.fn(() => (true));
|
||||
expect(selectors.root.shouldShowSpinner()).toEqual(true);
|
||||
describe('user cannot view gradebook, but showSpinner is true', () => {
|
||||
it('returns false', () => {
|
||||
testSelector(false, true, false);
|
||||
});
|
||||
});
|
||||
it('stops showing the spinner when a grades task is not processing', () => {
|
||||
selectors.roles.canUserViewGradebook = jest.fn(() => (true));
|
||||
selectors.grades.showSpinner = jest.fn(() => (false));
|
||||
expect(selectors.root.shouldShowSpinner()).toEqual(false);
|
||||
describe('user can view gradebook, and showSpinner is true', () => {
|
||||
it('returns true', () => {
|
||||
testSelector(true, true, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const selectors = {
|
||||
canUserViewGradebook: ({ roles }) => roles.canUserViewGradebook,
|
||||
};
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export default selectors;
|
||||
const canUserViewGradebook = ({ roles }) => !!roles.canUserViewGradebook;
|
||||
|
||||
export default StrictDict({
|
||||
canUserViewGradebook,
|
||||
});
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import selectors from './roles';
|
||||
|
||||
describe('canUserViewGradebook', () => {
|
||||
it('returns true if the user has the canUserViewGradebook role', () => {
|
||||
const canUserViewGradebook = selectors.canUserViewGradebook({
|
||||
roles: {
|
||||
canUserViewGradebook: true,
|
||||
canUserDoTheMonsterMash: false,
|
||||
},
|
||||
describe('roles selectors', () => {
|
||||
describe('canUserViewGradebook', () => {
|
||||
it('returns true if the user has the canUserViewGradebook role', () => {
|
||||
const canUserViewGradebook = selectors.canUserViewGradebook({
|
||||
roles: {
|
||||
canUserViewGradebook: true,
|
||||
canUserDoTheMonsterMash: false,
|
||||
},
|
||||
});
|
||||
expect(canUserViewGradebook).toBeTruthy();
|
||||
});
|
||||
expect(canUserViewGradebook).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the user does not have the canUserViewGradebook role', () => {
|
||||
const canUserViewGradebook = selectors.canUserViewGradebook({
|
||||
roles: {
|
||||
canUserViewGradebook: false,
|
||||
canUserDoTheMonsterMash: true,
|
||||
},
|
||||
it('returns false if the user does not have the canUserViewGradebook role', () => {
|
||||
const canUserViewGradebook = selectors.canUserViewGradebook({
|
||||
roles: {
|
||||
canUserViewGradebook: false,
|
||||
canUserDoTheMonsterMash: true,
|
||||
},
|
||||
});
|
||||
expect(canUserViewGradebook).toBeFalsy();
|
||||
});
|
||||
expect(canUserViewGradebook).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
// Certain course runs may be expressly allowed to view the
|
||||
// bulk management tools, bypassing the other checks.
|
||||
// Note that this does not affect whether or not the backend
|
||||
// LMS API will permit usage of the tool.
|
||||
|
||||
const selectors = {
|
||||
hasSpecialBulkManagementAccess: (courseId) => {
|
||||
const specialIdList = process.env.BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS || '';
|
||||
return specialIdList.split(',').includes(courseId);
|
||||
},
|
||||
/**
|
||||
* hasSpecialBulkManagementAccess(courseId)
|
||||
* Returns true iff the bulk management special access course ids env variable includes
|
||||
* the linked course id.
|
||||
* @param {string} courseId - linked course id
|
||||
* @param {bool} - course has special bulk management access?
|
||||
*/
|
||||
const hasSpecialBulkManagementAccess = (courseId) => {
|
||||
const specialIdList = process.env.BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS || '';
|
||||
return specialIdList.split(',').includes(courseId);
|
||||
};
|
||||
|
||||
export default selectors;
|
||||
export default StrictDict({
|
||||
hasSpecialBulkManagementAccess,
|
||||
});
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
const compose = (...fns) => {
|
||||
const [firstFunc, ...rest] = fns.reverse();
|
||||
return (...args) => rest.reduce((accum, fn) => fn(accum), firstFunc(...args));
|
||||
};
|
||||
/* eslint-disable import/no-self-import */
|
||||
import { StrictDict } from 'utils';
|
||||
import * as module from './tracks';
|
||||
|
||||
const allTracks = state => state.tracks.results || [];
|
||||
const trackIsMasters = track => track.slug === 'masters';
|
||||
const hasMastersTrack = tracks => tracks.some(trackIsMasters);
|
||||
const stateHasMastersTrack = compose(hasMastersTrack, allTracks);
|
||||
export const mastersKey = 'masters';
|
||||
|
||||
const selectors = {
|
||||
/**
|
||||
* hasMastersTrack(tracks)
|
||||
* returns true if at least one track in the list is masters track
|
||||
* @param {object[]} tracks - list of track objects
|
||||
* @return {bool} - are any of the tracks a masters track?
|
||||
*/
|
||||
export const hasMastersTrack = tracks => tracks.some(({ slug }) => slug === mastersKey);
|
||||
|
||||
// Selectors
|
||||
/**
|
||||
* allTracks(state)
|
||||
* returns all tracks resuls from top-level redux state
|
||||
* @param {object} state - redux state
|
||||
* @return {object[]} - list of track result entries
|
||||
*/
|
||||
export const allTracks = state => state.tracks.results || [];
|
||||
|
||||
/**
|
||||
* stateHasMastersTrack(state)
|
||||
* returns true if the state has a masters track entry.
|
||||
* @param {object} state - redux state
|
||||
* @return {bool} - does the state have a masters track entry?
|
||||
*/
|
||||
export const stateHasMastersTrack = (state) => module.hasMastersTrack(module.allTracks(state));
|
||||
|
||||
export default StrictDict({
|
||||
allTracks,
|
||||
hasMastersTrack,
|
||||
stateHasMastersTrack,
|
||||
trackIsMasters,
|
||||
};
|
||||
|
||||
export default selectors;
|
||||
});
|
||||
|
||||
@@ -1,88 +1,49 @@
|
||||
import selectors from './tracks';
|
||||
import * as selectors from './tracks';
|
||||
|
||||
const nonMastersTrack = {
|
||||
slug: 'honor',
|
||||
name: 'Honor Code Certificate',
|
||||
min_price: 0,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: null,
|
||||
description: null,
|
||||
sku: null,
|
||||
bulk_sku: null,
|
||||
};
|
||||
const tracksWithMasters = [{ slug: selectors.mastersKey }, { slug: 'other track' }];
|
||||
const tracksWithoutMasters = [{ slug: 'fake track' }, { slug: 'other track' }];
|
||||
|
||||
const mastersTrack = {
|
||||
slug: 'masters',
|
||||
name: 'Masters track',
|
||||
min_price: 0,
|
||||
suggested_prices: 'a lot',
|
||||
currency: 'usd',
|
||||
expiration_datetime: null,
|
||||
description: null,
|
||||
sku: null,
|
||||
bulk_sku: null,
|
||||
};
|
||||
describe('tracks selectors', () => {
|
||||
// Transformers
|
||||
describe('hasMastersTrack', () => {
|
||||
const selector = selectors.hasMastersTrack;
|
||||
it('returns true if a masters track is present', () => {
|
||||
expect(selector(tracksWithMasters)).toEqual(true);
|
||||
});
|
||||
|
||||
const exampleTracksWithoutMasters = [nonMastersTrack];
|
||||
const exampleTracksWithMasters = [nonMastersTrack, mastersTrack];
|
||||
|
||||
describe('allTracks', () => {
|
||||
it('returns an empty array if no tracks found', () => {
|
||||
const allTracks = selectors.allTracks({ tracks: {} });
|
||||
expect(allTracks).toEqual([]);
|
||||
it('returns false if a masters track is not present', () => {
|
||||
expect(selector(tracksWithoutMasters)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns tracks if included in result', () => {
|
||||
const allTracks = selectors.allTracks({ tracks: { results: exampleTracksWithoutMasters } });
|
||||
expect(allTracks).toEqual([
|
||||
{
|
||||
slug: 'honor',
|
||||
name: 'Honor Code Certificate',
|
||||
min_price: 0,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: null,
|
||||
description: null,
|
||||
sku: null,
|
||||
bulk_sku: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasMastersTrack', () => {
|
||||
it('returns true if a masters track is present', () => {
|
||||
const hasMastersTrack = selectors.hasMastersTrack(exampleTracksWithMasters);
|
||||
expect(hasMastersTrack).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if a masters track is not present', () => {
|
||||
const hasMastersTrack = selectors.hasMastersTrack(exampleTracksWithoutMasters);
|
||||
expect(hasMastersTrack).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stateHasMastersTrack', () => {
|
||||
it('returns true if a masters track is present', () => {
|
||||
const stateHasMastersTrack = selectors.stateHasMastersTrack({ tracks: { results: exampleTracksWithMasters } });
|
||||
expect(stateHasMastersTrack).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if a masters track is not present', () => {
|
||||
const stateHasMastersTrack = selectors.stateHasMastersTrack({ tracks: { results: exampleTracksWithoutMasters } });
|
||||
expect(stateHasMastersTrack).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackIsMasters', () => {
|
||||
it('returns true if track is a masters track', () => {
|
||||
const trackIsMasters = selectors.trackIsMasters(mastersTrack);
|
||||
expect(trackIsMasters).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true if track is not a masters track', () => {
|
||||
const trackIsMasters = selectors.trackIsMasters(nonMastersTrack);
|
||||
expect(trackIsMasters).toBeFalsy();
|
||||
// Selectors
|
||||
describe('allTracks', () => {
|
||||
const selector = selectors.allTracks;
|
||||
it('returns an empty array if no tracks found', () => {
|
||||
expect(selector({ tracks: {} })).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns tracks if included in result', () => {
|
||||
const results = [{ some: 'example' }, { track: 'results' }];
|
||||
expect(selector({ tracks: { results } })).toEqual(results);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stateHasMastersTrack', () => {
|
||||
it('returns hasMastersTracks called with allTracks as input', () => {
|
||||
const testState = { some: 'fake', state: 'values' };
|
||||
const mocks = {
|
||||
allTracks: selectors.allTracks,
|
||||
hasMastersTrack: selectors.hasMastersTrack,
|
||||
};
|
||||
selectors.allTracks = jest.fn(state => ({ allTracks: state }));
|
||||
selectors.hasMastersTrack = jest.fn((tracks) => ({ hasMastersTrack: tracks }));
|
||||
|
||||
expect(selectors.stateHasMastersTrack(testState)).toEqual({
|
||||
hasMastersTrack: { allTracks: testState },
|
||||
});
|
||||
selectors.allTracks = mocks.allTracks;
|
||||
selectors.hasMastersTrack = mocks.hasMastersTrack;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,88 +4,87 @@ import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProductio
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { createMiddleware } from 'redux-beacon';
|
||||
import Segment, { trackEvent, trackPageView } from '@redux-beacon/segment';
|
||||
import { GOT_ROLES } from './constants/actionTypes/roles';
|
||||
import {
|
||||
GOT_GRADES, GRADE_UPDATE_SUCCESS, GRADE_UPDATE_FAILURE, UPLOAD_OVERRIDE,
|
||||
UPLOAD_OVERRIDE_ERROR, BULK_GRADE_REPORT_DOWNLOADED, INTERVENTION_REPORT_DOWNLOADED,
|
||||
} from './constants/actionTypes/grades';
|
||||
import { UPDATE_COURSE_GRADE_LIMITS } from './constants/actionTypes/filters';
|
||||
|
||||
import actions from './actions';
|
||||
import reducers from './reducers';
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
const trackingCategory = 'gradebook';
|
||||
|
||||
const eventsMap = {
|
||||
[GOT_ROLES]: trackPageView(action => ({
|
||||
[actions.roles.fetching.received.toString()]: trackPageView(({ payload }) => ({
|
||||
category: trackingCategory,
|
||||
page: action.courseId,
|
||||
page: payload.courseId,
|
||||
})),
|
||||
[GOT_GRADES]: trackEvent(action => ({
|
||||
[actions.grades.fetching.received.toString()]: trackEvent(({ payload }) => ({
|
||||
name: 'edx.gradebook.grades.displayed',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: action.courseId,
|
||||
track: action.track,
|
||||
cohort: action.cohort,
|
||||
assignmentType: action.assignmentType,
|
||||
prev: action.prev,
|
||||
next: action.next,
|
||||
label: payload.courseId,
|
||||
track: payload.track,
|
||||
cohort: payload.cohort,
|
||||
assignmentType: payload.assignmentType,
|
||||
prev: payload.prev,
|
||||
next: payload.next,
|
||||
},
|
||||
})),
|
||||
[GRADE_UPDATE_SUCCESS]: trackEvent(action => ({
|
||||
[actions.grades.update.success.toString()]: trackEvent(({ payload }) => ({
|
||||
name: 'edx.gradebook.grades.grade_override.succeeded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: action.courseId,
|
||||
updatedGrades: action.payload.responseData,
|
||||
label: payload.courseId,
|
||||
updatedGrades: payload.responseData,
|
||||
},
|
||||
})),
|
||||
[GRADE_UPDATE_FAILURE]: trackEvent(action => ({
|
||||
[actions.grades.update.failure.toString()]: trackEvent(({ payload }) => ({
|
||||
name: 'edx.gradebook.grades.grade_override.failed',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: action.courseId,
|
||||
error: action.payload.error,
|
||||
label: payload.courseId,
|
||||
error: payload.error,
|
||||
},
|
||||
})),
|
||||
[UPLOAD_OVERRIDE]: trackEvent(action => ({
|
||||
[actions.grades.uploadOverride.success.toString()]: trackEvent(({ payload }) => ({
|
||||
name: 'edx.gradebook.grades.upload.grades_overrides.succeeded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: action.courseId,
|
||||
label: payload.courseId,
|
||||
},
|
||||
})),
|
||||
[UPLOAD_OVERRIDE_ERROR]: trackEvent(action => ({
|
||||
[actions.grades.uploadOverride.failure.toString()]: trackEvent(({ payload }) => ({
|
||||
name: 'edx.gradebook.grades.upload.grades_overrides.failed',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: action.courseId,
|
||||
error: action.payload.error,
|
||||
label: payload.courseId,
|
||||
error: payload.error,
|
||||
},
|
||||
})),
|
||||
[UPDATE_COURSE_GRADE_LIMITS]: trackEvent(action => ({
|
||||
[actions.filters.update.courseGradeLimits]: trackEvent(({ payload }) => ({
|
||||
name: 'edx.gradebook.grades.filter_applied',
|
||||
label: action.courseId,
|
||||
label: payload.courseId,
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: action.courseId,
|
||||
},
|
||||
})),
|
||||
[BULK_GRADE_REPORT_DOWNLOADED]: trackEvent(action => ({
|
||||
name: 'edx.gradebook.reports.grade_export.downloaded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: action.courseId,
|
||||
},
|
||||
})),
|
||||
[INTERVENTION_REPORT_DOWNLOADED]: trackEvent(action => ({
|
||||
name: 'edx.gradebook.reports.intervention.downloaded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: action.courseId,
|
||||
label: payload.courseId,
|
||||
},
|
||||
})),
|
||||
[actions.grades.downloadReport.bulkGrades.toString()]: trackEvent(
|
||||
({ payload }) => ({
|
||||
name: 'edx.gradebook.reports.grade_export.downloaded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: payload.courseId,
|
||||
},
|
||||
}),
|
||||
),
|
||||
[actions.grades.downloadReport.intervention.toString()]: trackEvent(
|
||||
({ payload }) => ({
|
||||
name: 'edx.gradebook.reports.intervention.downloaded',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
label: payload.courseId,
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
const segmentMiddleware = createMiddleware(eventsMap, Segment());
|
||||
|
||||
26
src/data/thunkActions/assignmentTypes.js
Normal file
26
src/data/thunkActions/assignmentTypes.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { StrictDict } from 'utils';
|
||||
import actions from '../actions';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const { fetching, gotGradesFrozen } = actions.assignmentTypes;
|
||||
const { gotBulkManagementConfig } = actions.config;
|
||||
|
||||
export const fetchAssignmentTypes = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(fetching.started());
|
||||
return LmsApiService.fetchAssignmentTypes(courseId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(fetching.received(Object.keys(data.assignment_types)));
|
||||
dispatch(gotGradesFrozen(data.grades_frozen));
|
||||
dispatch(gotBulkManagementConfig(data.can_see_bulk_management));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(fetching.error());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default StrictDict({ fetchAssignmentTypes });
|
||||
55
src/data/thunkActions/assignmentTypes.test.js
Normal file
55
src/data/thunkActions/assignmentTypes.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
import actions from '../actions';
|
||||
import * as thunkActions from './assignmentTypes';
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
fetchAssignmentTypes: jest.fn(),
|
||||
}));
|
||||
|
||||
const responseData = {
|
||||
assignment_types: {
|
||||
some: 'types',
|
||||
other: 'TYpeS',
|
||||
},
|
||||
grades_frozen: 'bOOl',
|
||||
can_see_bulk_management: 'BooL',
|
||||
};
|
||||
|
||||
describe('assignmentType thunkActions', () => {
|
||||
describe('fetchAssignmentTypes', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchAssignmentTypes,
|
||||
thunkActions.fetchAssignmentTypes,
|
||||
[courseId],
|
||||
);
|
||||
describe('actions dispatched on valid response', () => {
|
||||
const actionNames = [
|
||||
'fetching.started',
|
||||
'fetching.received with data.assignment_types',
|
||||
'gotGradesFrozen with data.grades_frozen',
|
||||
'config.gotBulkManagement with data.can_see_bulk_management',
|
||||
];
|
||||
test(actionNames.join(', '), () => testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
[
|
||||
actions.assignmentTypes.fetching.started(),
|
||||
actions.assignmentTypes.fetching.received(Object.keys(responseData.assignment_types)),
|
||||
actions.assignmentTypes.gotGradesFrozen(responseData.grades_frozen),
|
||||
actions.config.gotBulkManagementConfig(responseData.can_see_bulk_management),
|
||||
],
|
||||
));
|
||||
});
|
||||
describe('actions dispatched on api error', () => {
|
||||
test('fetching.started, fetching.error', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[
|
||||
actions.assignmentTypes.fetching.started(),
|
||||
actions.assignmentTypes.fetching.error(),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
21
src/data/thunkActions/cohorts.js
Normal file
21
src/data/thunkActions/cohorts.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { StrictDict } from 'utils';
|
||||
import cohorts from '../actions/cohorts';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
export const fetchCohorts = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(cohorts.fetching.started());
|
||||
return LmsApiService.fetchCohorts(courseId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(cohorts.fetching.received(data.cohorts));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(cohorts.fetching.error());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default StrictDict({ fetchCohorts });
|
||||
45
src/data/thunkActions/cohorts.test.js
Normal file
45
src/data/thunkActions/cohorts.test.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
import actions from '../actions';
|
||||
import * as thunkActions from './cohorts';
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
fetchCohorts: jest.fn(),
|
||||
}));
|
||||
|
||||
const responseData = {
|
||||
cohorts: {
|
||||
some: 'COHorts',
|
||||
other: 'cohORT$',
|
||||
},
|
||||
};
|
||||
|
||||
describe('cohorts thunkActions', () => {
|
||||
describe('fetchCohorts', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchCohorts,
|
||||
thunkActions.fetchCohorts,
|
||||
[courseId],
|
||||
);
|
||||
describe('actions dispatched on valid response', () => {
|
||||
test('fetching.started, fetching.received', () => testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
[
|
||||
actions.cohorts.fetching.started(),
|
||||
actions.cohorts.fetching.received(responseData.cohorts),
|
||||
],
|
||||
));
|
||||
});
|
||||
describe('actions dispatched on api error', () => {
|
||||
test('fetching.started, fetching.error', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[
|
||||
actions.cohorts.fetching.started(),
|
||||
actions.cohorts.fetching.error(),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
18
src/data/thunkActions/filters.js
Normal file
18
src/data/thunkActions/filters.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { StrictDict } from 'utils';
|
||||
import filters from '../actions/filters';
|
||||
import selectors from '../selectors';
|
||||
|
||||
import { fetchGrades } from './grades';
|
||||
|
||||
export const updateIncludeCourseRoleMembers = (includeCourseRoleMembers) => (dispatch, getState) => {
|
||||
dispatch(filters.update.includeCourseRoleMembers(includeCourseRoleMembers));
|
||||
const state = getState();
|
||||
const { cohort, track, assignmentType } = selectors.filters.allFilters(state);
|
||||
const courseId = selectors.grades.courseId(state);
|
||||
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
updateIncludeCourseRoleMembers,
|
||||
});
|
||||
47
src/data/thunkActions/filters.test.js
Normal file
47
src/data/thunkActions/filters.test.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import selectors from '../selectors';
|
||||
import actions from '../actions';
|
||||
import { fetchGrades } from './grades';
|
||||
import { updateIncludeCourseRoleMembers } from './filters';
|
||||
|
||||
jest.mock('./grades', () => ({
|
||||
fetchGrades: jest.fn((...args) => ({ type: 'fetchGrades', args })),
|
||||
}));
|
||||
|
||||
jest.mock('../selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: { courseId: jest.fn() },
|
||||
filters: { allFilters: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
describe('filters thunkActions', () => {
|
||||
describe('updateIncludeCourseRoleMembers', () => {
|
||||
const getState = () => ({});
|
||||
const testVal = 'Hawaii';
|
||||
const filters = {
|
||||
cohort: 'COHort',
|
||||
track: 'TRacK',
|
||||
assignmentType: 'Prague',
|
||||
};
|
||||
const courseId = 'Some Course ID';
|
||||
let dispatch;
|
||||
beforeEach(() => {
|
||||
dispatch = jest.fn();
|
||||
selectors.filters.allFilters.mockReturnValue(filters);
|
||||
selectors.grades.courseId.mockReturnValue(courseId);
|
||||
updateIncludeCourseRoleMembers(testVal)(dispatch, getState);
|
||||
});
|
||||
it('dispatches filters.update.includeCoruseRoleMembers with passed value', () => {
|
||||
expect(dispatch.mock.calls[0][0]).toEqual(actions.filters.update.includeCourseRoleMembers(testVal));
|
||||
});
|
||||
it('dispatches fetchGrades with courseId, cohort, track, and assignmentType', () => {
|
||||
expect(dispatch.mock.calls[1][0]).toEqual(fetchGrades(
|
||||
courseId,
|
||||
filters.cohort,
|
||||
filters.track,
|
||||
filters.assignmentType,
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
226
src/data/thunkActions/grades.js
Normal file
226
src/data/thunkActions/grades.js
Normal file
@@ -0,0 +1,226 @@
|
||||
/* eslint-disable import/no-self-import */
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import grades from '../actions/grades';
|
||||
import { sortAlphaAsc } from '../actions/utils';
|
||||
|
||||
import selectors from '../selectors';
|
||||
|
||||
import GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG from '../constants/errors';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import * as module from './grades';
|
||||
|
||||
const {
|
||||
formatMaxAssignmentGrade,
|
||||
formatMinAssignmentGrade,
|
||||
formatMaxCourseGrade,
|
||||
formatMinCourseGrade,
|
||||
formatGradeOverrideForDisplay,
|
||||
} = selectors.grades;
|
||||
|
||||
export const defaultAssignmentFilter = 'All';
|
||||
|
||||
export const fetchBulkUpgradeHistory = courseId => (
|
||||
// todo add loading effect
|
||||
dispatch => LmsApiService.fetchGradeBulkOperationHistory(courseId).then(
|
||||
(response) => { dispatch(grades.bulkHistory.received(response)); },
|
||||
).catch(() => dispatch(grades.bulkHistory.error()))
|
||||
);
|
||||
|
||||
export const fetchGrades = (
|
||||
courseId,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
options = {},
|
||||
) => (
|
||||
(dispatch, getState) => {
|
||||
dispatch(grades.fetching.started());
|
||||
const {
|
||||
assignment,
|
||||
assignmentGradeMax: assignmentMax,
|
||||
assignmentGradeMin: assignmentMin,
|
||||
courseGradeMin,
|
||||
courseGradeMax,
|
||||
includeCourseRoleMembers,
|
||||
} = selectors.filters.allFilters(getState());
|
||||
const { id: assignmentId } = assignment || {};
|
||||
const assignmentGradeMax = formatMaxAssignmentGrade(assignmentMax, { assignmentId });
|
||||
const assignmentGradeMin = formatMinAssignmentGrade(assignmentMin, { assignmentId });
|
||||
const courseGradeMaxFormatted = formatMaxCourseGrade(courseGradeMax);
|
||||
const courseGradeMinFormatted = formatMinCourseGrade(courseGradeMin);
|
||||
return LmsApiService.fetchGradebookData(
|
||||
courseId,
|
||||
options.searchText || null,
|
||||
cohort,
|
||||
track,
|
||||
{
|
||||
assignment: assignmentId,
|
||||
assignmentGradeMax,
|
||||
assignmentGradeMin,
|
||||
courseGradeMax: courseGradeMaxFormatted,
|
||||
courseGradeMin: courseGradeMinFormatted,
|
||||
includeCourseRoleMembers,
|
||||
},
|
||||
)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(grades.fetching.received({
|
||||
grades: data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
prev: data.previous,
|
||||
next: data.next,
|
||||
courseId,
|
||||
totalUsersCount: data.total_users_count,
|
||||
filteredUsersCount: data.filtered_users_count,
|
||||
}));
|
||||
dispatch(grades.fetching.finished());
|
||||
if (options.showSuccess) {
|
||||
dispatch(grades.banner.open());
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(grades.fetching.error());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchGradeOverrideHistory = (subsectionId, userId) => (
|
||||
dispatch => LmsApiService.fetchGradeOverrideHistory(subsectionId, userId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
dispatch(grades.overrideHistory.received({
|
||||
overrideHistory: formatGradeOverrideForDisplay(data.history),
|
||||
currentEarnedAllOverride: data.override ? data.override.earned_all_override : null,
|
||||
currentPossibleAllOverride: data.override ? data.override.possible_all_override : null,
|
||||
currentEarnedGradedOverride: data.override ? data.override.earned_graded_override : null,
|
||||
currentPossibleGradedOverride: data.override
|
||||
? data.override.possible_graded_override : null,
|
||||
originalGradeEarnedAll: data.original_grade ? data.original_grade.earned_all : null,
|
||||
originalGradePossibleAll: data.original_grade ? data.original_grade.possible_all : null,
|
||||
originalGradeEarnedGraded: data.original_grade
|
||||
? data.original_grade.earned_graded : null,
|
||||
originalGradePossibleGraded: data.original_grade
|
||||
? data.original_grade.possible_graded : null,
|
||||
}));
|
||||
} else {
|
||||
dispatch(grades.overrideHistory.error(data.error_message));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(grades.overrideHistory.error(GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG));
|
||||
})
|
||||
);
|
||||
|
||||
export const fetchMatchingUserGrades = (
|
||||
courseId,
|
||||
searchText,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
showSuccess,
|
||||
options = {},
|
||||
) => {
|
||||
const newOptions = { ...options, searchText, showSuccess };
|
||||
return module.fetchGrades(courseId, cohort, track, assignmentType, newOptions);
|
||||
};
|
||||
|
||||
export const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
|
||||
(dispatch) => {
|
||||
dispatch(grades.fetching.started());
|
||||
return getAuthenticatedHttpClient().get(endpoint)
|
||||
.then(({ data }) => data)
|
||||
.then((data) => {
|
||||
dispatch(grades.fetching.received({
|
||||
grades: data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
prev: data.previous,
|
||||
next: data.next,
|
||||
courseId,
|
||||
totalUsersCount: data.total_users_count,
|
||||
filteredUsersCount: data.filtered_users_count,
|
||||
}));
|
||||
dispatch(grades.fetching.finished());
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(grades.fetching.error());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const submitFileUploadFormData = (courseId, formData) => (
|
||||
(dispatch) => {
|
||||
dispatch(grades.csvUpload.started());
|
||||
return LmsApiService.uploadGradeCsv(courseId, formData).then(() => {
|
||||
dispatch(grades.csvUpload.finished());
|
||||
dispatch(grades.uploadOverride.success(courseId));
|
||||
}).catch((err) => {
|
||||
dispatch(grades.uploadOverride.failure(courseId, err));
|
||||
if (err.status === 200 && err.data.error_messages.length) {
|
||||
const { error_messages: errorMessages, saved, total } = err.data;
|
||||
return dispatch(grades.csvUpload.error({ errorMessages, saved, total }));
|
||||
}
|
||||
return dispatch(grades.csvUpload.error({ errorMessages: ['Unknown error.'] }));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const updateGrades = (courseId, updateData, searchText, cohort, track) => (
|
||||
(dispatch) => {
|
||||
dispatch(grades.update.request());
|
||||
return LmsApiService.updateGradebookData(courseId, updateData)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(grades.update.success({ courseId, data }));
|
||||
dispatch(module.fetchMatchingUserGrades(
|
||||
courseId,
|
||||
searchText,
|
||||
cohort,
|
||||
track,
|
||||
defaultAssignmentFilter,
|
||||
true,
|
||||
{ searchText },
|
||||
));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(grades.update.failure({ courseId, error }));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const updateGradesIfAssignmentGradeFiltersSet = (
|
||||
courseId,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
) => (dispatch, getState) => {
|
||||
const assignmentGradeMin = selectors.filters.assignmentGradeMin(getState());
|
||||
const assignmentGradeMax = selectors.filters.assignmentGradeMax(getState());
|
||||
const hasAssignmentGradeFiltersSet = assignmentGradeMax || assignmentGradeMin;
|
||||
if (hasAssignmentGradeFiltersSet) {
|
||||
dispatch(module.fetchGrades(
|
||||
courseId,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
fetchBulkUpgradeHistory,
|
||||
fetchGrades,
|
||||
fetchGradeOverrideHistory,
|
||||
fetchMatchingUserGrades,
|
||||
fetchPrevNextGrades,
|
||||
submitFileUploadFormData,
|
||||
updateGrades,
|
||||
updateGradesIfAssignmentGradeFiltersSet,
|
||||
});
|
||||
774
src/data/thunkActions/grades.test.js
Normal file
774
src/data/thunkActions/grades.test.js
Normal file
@@ -0,0 +1,774 @@
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import * as thunkActions from './grades';
|
||||
import actions from '../actions';
|
||||
import GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG from '../constants/errors';
|
||||
import { sortAlphaAsc } from '../actions/utils';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import selectors from '../selectors';
|
||||
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const sections = [
|
||||
{
|
||||
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)',
|
||||
},
|
||||
];
|
||||
|
||||
const gradesResults = [
|
||||
{
|
||||
course_id: courseId,
|
||||
section_breakdown: [...sections],
|
||||
email: 'user1@example.com',
|
||||
username: 'user1',
|
||||
user_id: 1,
|
||||
percent: 0.5,
|
||||
letter_grade: null,
|
||||
},
|
||||
{
|
||||
course_id: courseId,
|
||||
section_breakdown: [...sections],
|
||||
email: 'user22@example.com',
|
||||
username: 'user22',
|
||||
user_id: 22,
|
||||
percent: 0,
|
||||
letter_grade: null,
|
||||
},
|
||||
];
|
||||
|
||||
const allFilters = {
|
||||
assignment: { id: 'assigNment_ID' },
|
||||
assignmentGradeMax: 89,
|
||||
assignmentGradeMin: 12,
|
||||
courseGradeMax: 92,
|
||||
courseGradeMin: 2,
|
||||
includeCourseRoleMembers: true,
|
||||
};
|
||||
|
||||
const testVal = 'A Test VALue';
|
||||
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
fetchGradebookData: jest.fn(),
|
||||
fetchGradeBulkOperationHistory: jest.fn(),
|
||||
fetchGradeOverrideHistory: jest.fn(),
|
||||
fetchPrevNextGrades: jest.fn(),
|
||||
updateGradebookData: jest.fn(),
|
||||
updateGrades: jest.fn(),
|
||||
uploadGradeCsv: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
}));
|
||||
jest.mock('../selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
formatGradeOverrideForDisplay: (override) => ({
|
||||
formatGradeOverrideForDisplay: { override },
|
||||
}),
|
||||
formatMaxAssignmentGrade: (grade, { assignmentId }) => ({
|
||||
formatMaxAssignmentGrade: { grade, assignmentId },
|
||||
}),
|
||||
formatMinAssignmentGrade: (grade, { assignmentId }) => ({
|
||||
formatMinAssignmentGrade: { grade, assignmentId },
|
||||
}),
|
||||
formatMinCourseGrade: (grade) => ({
|
||||
formatMinCourseGrade: { grade },
|
||||
}),
|
||||
formatMaxCourseGrade: (grade) => ({
|
||||
formatMaxCourseGrade: { grade },
|
||||
}),
|
||||
},
|
||||
filters: {
|
||||
allFilters: jest.fn(),
|
||||
assignmentGradeMin: jest.fn(),
|
||||
assignmentGradeMax: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
selectors.filters.allFilters.mockReturnValue(allFilters);
|
||||
|
||||
describe('grades thunkActions', () => {
|
||||
beforeEach(() => {
|
||||
LmsApiService.fetchGradebookData.mockClear();
|
||||
LmsApiService.fetchGradeBulkOperationHistory.mockClear();
|
||||
LmsApiService.fetchGradeOverrideHistory.mockClear();
|
||||
LmsApiService.fetchPrevNextGrades.mockClear();
|
||||
LmsApiService.updateGrades.mockClear();
|
||||
});
|
||||
describe('fetchBulkUpgradeHistory', () => {
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchGradeBulkOperationHistory,
|
||||
thunkActions.fetchBulkUpgradeHistory,
|
||||
[courseId],
|
||||
() => expect(LmsApiService.fetchGradeBulkOperationHistory).toHaveBeenCalledWith(courseId),
|
||||
);
|
||||
describe('valid response', () => {
|
||||
it('dispatches bulkHistory.received with response', () => {
|
||||
testFetch((resolve) => resolve(testVal), [actions.grades.bulkHistory.received(testVal)]);
|
||||
});
|
||||
});
|
||||
describe('api failure', () => {
|
||||
it('dispatches bulkHistory.error', () => {
|
||||
testFetch((resolve, reject) => reject(), [actions.grades.bulkHistory.error()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchGrades', () => {
|
||||
const responseData = {
|
||||
previous: 'PREvGrade',
|
||||
next: 'nextGRADe',
|
||||
results: gradesResults,
|
||||
total_users_count: 10,
|
||||
filtered_users_count: 8,
|
||||
};
|
||||
const expected = {
|
||||
grades: gradesResults.sort(sortAlphaAsc),
|
||||
cohort: 1,
|
||||
track: 'verified',
|
||||
assignmentType: 'Exam',
|
||||
prev: responseData.previous,
|
||||
next: responseData.next,
|
||||
courseId,
|
||||
totalUsersCount: responseData.total_users_count,
|
||||
filteredUsersCount: responseData.filtered_users_count,
|
||||
};
|
||||
const apiArgs = (options) => {
|
||||
const {
|
||||
assignment,
|
||||
assignmentGradeMax, assignmentGradeMin,
|
||||
courseGradeMax, courseGradeMin,
|
||||
includeCourseRoleMembers,
|
||||
} = selectors.filters.allFilters();
|
||||
const assignmentId = (assignment || {}).id;
|
||||
return [
|
||||
courseId,
|
||||
options.searchText || null,
|
||||
expected.cohort,
|
||||
expected.track,
|
||||
{
|
||||
assignment: assignmentId,
|
||||
assignmentGradeMax: selectors.grades.formatMaxAssignmentGrade(
|
||||
assignmentGradeMax, { assignmentId },
|
||||
),
|
||||
assignmentGradeMin: selectors.grades.formatMinAssignmentGrade(
|
||||
assignmentGradeMin, { assignmentId },
|
||||
),
|
||||
courseGradeMax: selectors.grades.formatMaxCourseGrade(
|
||||
courseGradeMax,
|
||||
),
|
||||
courseGradeMin: selectors.grades.formatMinCourseGrade(
|
||||
courseGradeMin,
|
||||
),
|
||||
includeCourseRoleMembers,
|
||||
},
|
||||
];
|
||||
};
|
||||
const testFetchWithOptions = (options = false) => createTestFetcher(
|
||||
LmsApiService.fetchGradebookData,
|
||||
thunkActions.fetchGrades,
|
||||
[
|
||||
courseId,
|
||||
expected.cohort,
|
||||
expected.track,
|
||||
expected.assignmentType,
|
||||
options,
|
||||
],
|
||||
() => {
|
||||
expect(
|
||||
LmsApiService.fetchGradebookData,
|
||||
).toHaveBeenCalledWith(...apiArgs(options));
|
||||
},
|
||||
);
|
||||
const testFetch = testFetchWithOptions();
|
||||
|
||||
describe('after valid response', () => {
|
||||
const successActions = [
|
||||
actions.grades.fetching.started(),
|
||||
actions.grades.fetching.received({ ...expected }),
|
||||
actions.grades.fetching.finished(),
|
||||
];
|
||||
it('dispatches success', () => testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
[...successActions],
|
||||
));
|
||||
it('passes the correct args to the fetchGradebookData query', () => {
|
||||
const assignmentId = allFilters.assignment.id;
|
||||
return testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
[...successActions],
|
||||
() => {
|
||||
const {
|
||||
formatMaxAssignmentGrade,
|
||||
formatMinAssignmentGrade,
|
||||
formatMaxCourseGrade,
|
||||
formatMinCourseGrade,
|
||||
} = selectors.grades;
|
||||
expect(LmsApiService.fetchGradebookData).toHaveBeenCalledWith(
|
||||
courseId,
|
||||
null,
|
||||
expected.cohort,
|
||||
expected.track,
|
||||
{
|
||||
assignment: allFilters.assignment.id,
|
||||
assignmentGradeMax: formatMaxAssignmentGrade(
|
||||
allFilters.assignmentGradeMax,
|
||||
{ assignmentId },
|
||||
),
|
||||
assignmentGradeMin: formatMinAssignmentGrade(
|
||||
allFilters.assignmentGradeMin,
|
||||
{ assignmentId },
|
||||
),
|
||||
courseGradeMax: formatMaxCourseGrade(allFilters.courseGradeMax),
|
||||
courseGradeMin: formatMinCourseGrade(allFilters.courseGradeMin),
|
||||
includeCourseRoleMembers: allFilters.includeCourseRoleMembers,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
describe('no assignment selected', () => {
|
||||
beforeAll(() => {
|
||||
selectors.filters.allFilters.mockReturnValue({
|
||||
...allFilters,
|
||||
assignment: undefined,
|
||||
});
|
||||
});
|
||||
afterAll(() => {
|
||||
selectors.filters.allFilters.mockReturnValue(allFilters);
|
||||
});
|
||||
it('passes the correct args to the fetchGradebookData query', () => testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
undefined,
|
||||
() => {
|
||||
const {
|
||||
formatMaxAssignmentGrade,
|
||||
formatMinAssignmentGrade,
|
||||
formatMaxCourseGrade,
|
||||
formatMinCourseGrade,
|
||||
} = selectors.grades;
|
||||
expect(LmsApiService.fetchGradebookData).toHaveBeenCalledWith(
|
||||
courseId,
|
||||
null,
|
||||
expected.cohort,
|
||||
expected.track,
|
||||
{
|
||||
assignmentGradeMax: formatMaxAssignmentGrade(allFilters.assignmentGradeMax, {}),
|
||||
assignmentGradeMin: formatMinAssignmentGrade(allFilters.assignmentGradeMin, {}),
|
||||
courseGradeMax: formatMaxCourseGrade(allFilters.courseGradeMax),
|
||||
courseGradeMin: formatMinCourseGrade(allFilters.courseGradeMin),
|
||||
includeCourseRoleMembers: allFilters.includeCourseRoleMembers,
|
||||
},
|
||||
);
|
||||
},
|
||||
));
|
||||
});
|
||||
describe('options', () => {
|
||||
const resolveFn = (resolve) => resolve({ data: responseData });
|
||||
describe('showSuccess', () => {
|
||||
it('dispatches success and opens banner if true', () => (
|
||||
testFetchWithOptions({ showSuccess: true })(
|
||||
resolveFn,
|
||||
[...successActions, actions.grades.banner.open()],
|
||||
)
|
||||
));
|
||||
it('does not open banner if not true', () => testFetchWithOptions({})(
|
||||
resolveFn, [...successActions],
|
||||
));
|
||||
});
|
||||
describe('searchText', () => {
|
||||
it('passes searchText to api call if included', () => {
|
||||
const options = { searchText: 'Search Text' };
|
||||
return testFetchWithOptions(options)(
|
||||
resolveFn,
|
||||
[...successActions],
|
||||
() => {
|
||||
expect(
|
||||
LmsApiService.fetchGradebookData.mock.calls[0][1],
|
||||
).toEqual(options.searchText);
|
||||
},
|
||||
);
|
||||
});
|
||||
it('passes null to api call for searchText if not included', () => {
|
||||
const options = {};
|
||||
return testFetchWithOptions(options)(
|
||||
resolveFn,
|
||||
[...successActions],
|
||||
() => {
|
||||
expect(
|
||||
LmsApiService.fetchGradebookData.mock.calls[0][1],
|
||||
).toEqual(null);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('empty response', () => {
|
||||
it('dispatches success on empty response', () => testFetch(
|
||||
(resolve) => resolve({ data: { ...responseData, results: [] } }),
|
||||
[
|
||||
actions.grades.fetching.started(),
|
||||
actions.grades.fetching.received({ ...expected, grades: [] }),
|
||||
actions.grades.fetching.finished(),
|
||||
],
|
||||
));
|
||||
});
|
||||
describe('after invalid response', () => {
|
||||
it('dispatches error', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[
|
||||
actions.grades.fetching.started(),
|
||||
actions.grades.fetching.error(),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchGradeOverrideHistory', () => {
|
||||
const subsectionId = 'subsectionId-11111';
|
||||
const userId = 'user-id-11111';
|
||||
|
||||
const originalGrade = {
|
||||
earned_all: 1.0,
|
||||
possible_all: 12.0,
|
||||
earned_graded: 3.0,
|
||||
possible_graded: 8.0,
|
||||
};
|
||||
|
||||
const override = {
|
||||
earned_all_override: 13.0,
|
||||
possible_all_override: 13.0,
|
||||
earned_graded_override: 10.0,
|
||||
possible_graded_override: 10.0,
|
||||
};
|
||||
const history = { some: 'history' };
|
||||
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchGradeOverrideHistory,
|
||||
thunkActions.fetchGradeOverrideHistory,
|
||||
[subsectionId, userId],
|
||||
() => {
|
||||
expect(
|
||||
LmsApiService.fetchGradeOverrideHistory,
|
||||
).toHaveBeenCalledWith(subsectionId, userId);
|
||||
},
|
||||
);
|
||||
|
||||
describe('valid data', () => {
|
||||
describe('if data.success', () => {
|
||||
describe('override', () => {
|
||||
it('loads override values and sets original to null', () => {
|
||||
const resolveFn = (resolve) => {
|
||||
resolve({ data: { success: true, history, override } });
|
||||
};
|
||||
return testFetch(resolveFn, [
|
||||
actions.grades.overrideHistory.received({
|
||||
overrideHistory: selectors.grades.formatGradeOverrideForDisplay(history),
|
||||
currentEarnedAllOverride: override.earned_all_override,
|
||||
currentPossibleAllOverride: override.possible_all_override,
|
||||
currentEarnedGradedOverride: override.earned_graded_override,
|
||||
currentPossibleGradedOverride: override.possible_graded_override,
|
||||
originalGradeEarnedAll: null,
|
||||
originalGradePossibleAll: null,
|
||||
originalGradeEarnedGraded: null,
|
||||
originalGradePossibleGraded: null,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('original', () => {
|
||||
it('loads original values and sets overrides to null', () => {
|
||||
const resolveFn = (resolve) => {
|
||||
resolve({ data: { success: true, history, original_grade: originalGrade } });
|
||||
};
|
||||
return testFetch(resolveFn, [
|
||||
actions.grades.overrideHistory.received({
|
||||
overrideHistory: selectors.grades.formatGradeOverrideForDisplay(history),
|
||||
currentEarnedAllOverride: null,
|
||||
currentPossibleAllOverride: null,
|
||||
currentEarnedGradedOverride: null,
|
||||
currentPossibleGradedOverride: null,
|
||||
originalGradeEarnedAll: originalGrade.earned_all,
|
||||
originalGradePossibleAll: originalGrade.possible_all,
|
||||
originalGradeEarnedGraded: originalGrade.earned_graded,
|
||||
originalGradePossibleGraded: originalGrade.possible_graded,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('no override or original', () => {
|
||||
it('loads null for current and override fields', () => {
|
||||
const resolveFn = (resolve) => {
|
||||
resolve({ data: { success: true, history } });
|
||||
};
|
||||
return testFetch(resolveFn, [
|
||||
actions.grades.overrideHistory.received({
|
||||
overrideHistory: selectors.grades.formatGradeOverrideForDisplay(history),
|
||||
currentEarnedAllOverride: null,
|
||||
currentPossibleAllOverride: null,
|
||||
currentEarnedGradedOverride: null,
|
||||
currentPossibleGradedOverride: null,
|
||||
originalGradeEarnedAll: null,
|
||||
originalGradePossibleAll: null,
|
||||
originalGradeEarnedGraded: null,
|
||||
originalGradePossibleGraded: null,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('if not data.success', () => {
|
||||
it('dispatchs error with error_message', () => {
|
||||
const errorMessage = 'oh Noooooooo!';
|
||||
const resolveFn = (resolve) => {
|
||||
resolve({ data: { success: false, error_message: errorMessage } });
|
||||
};
|
||||
return testFetch(resolveFn, [
|
||||
actions.grades.overrideHistory.error(errorMessage),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('api failure', () => {
|
||||
it('sends error action with default error message', () => {
|
||||
const resolveFn = (resolve, reject) => reject();
|
||||
return testFetch(resolveFn, [
|
||||
actions.grades.overrideHistory.error(
|
||||
GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG,
|
||||
),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchGrades inheritors', () => {
|
||||
let fetchGrades;
|
||||
const fetchGradesAction = { type: 'fetchGrades' };
|
||||
beforeEach(() => {
|
||||
fetchGrades = thunkActions.fetchGrades;
|
||||
thunkActions.fetchGrades = jest.fn().mockImplementation((...args) => ({
|
||||
...fetchGradesAction,
|
||||
args,
|
||||
}));
|
||||
});
|
||||
afterEach(() => {
|
||||
thunkActions.fetchGrades = fetchGrades;
|
||||
});
|
||||
describe('fetchMatchingUserGrades', () => {
|
||||
it('calls with added searchText and showSuccess options', () => {
|
||||
const store = mockStore({});
|
||||
const args = {
|
||||
searchText: 'Some SearcH',
|
||||
cohort: 'coHOrt',
|
||||
track: 'TRAck',
|
||||
assignmentType: 'aType',
|
||||
showSuccess: true,
|
||||
options: { some: 'options' },
|
||||
};
|
||||
store.dispatch(thunkActions.fetchMatchingUserGrades(
|
||||
courseId,
|
||||
args.searchText,
|
||||
args.cohort,
|
||||
args.track,
|
||||
args.assignmentType,
|
||||
args.showSuccess,
|
||||
args.options,
|
||||
));
|
||||
expect(store.getActions()).toEqual([
|
||||
{
|
||||
...fetchGradesAction,
|
||||
args: [
|
||||
courseId,
|
||||
args.cohort,
|
||||
args.track,
|
||||
args.assignmentType,
|
||||
{ searchText: args.searchText, showSuccess: args.showSuccess, ...args.options },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('updateGrades', () => {
|
||||
const args = {
|
||||
updateData: { some: 'data' },
|
||||
searchText: 'SEARch TErm',
|
||||
cohort: 'COhoRT',
|
||||
track: 'trACk',
|
||||
};
|
||||
const gradebookData = { data: { OTher: 'DATA' } };
|
||||
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.updateGradebookData,
|
||||
thunkActions.updateGrades,
|
||||
[courseId, args.updateData, args.searchText, args.cohort, args.track],
|
||||
() => expect(
|
||||
LmsApiService.updateGradebookData,
|
||||
).toHaveBeenCalledWith(courseId, args.updateData),
|
||||
);
|
||||
let fetchMatchingUserGrades;
|
||||
const fetchMatchingUserGradesAction = { type: 'fetchMatchingUserGrades' };
|
||||
beforeEach(() => {
|
||||
fetchMatchingUserGrades = jest.spyOn(
|
||||
thunkActions,
|
||||
'fetchMatchingUserGrades',
|
||||
).mockImplementation((...actionArgs) => ({
|
||||
...fetchMatchingUserGradesAction,
|
||||
args: actionArgs,
|
||||
}));
|
||||
});
|
||||
afterEach(() => {
|
||||
fetchMatchingUserGrades.mockRestore();
|
||||
});
|
||||
describe('valid response', () => {
|
||||
it('sends success event, and fetches matching user grades', () => testFetch(
|
||||
(resolve) => resolve(gradebookData),
|
||||
[
|
||||
actions.grades.update.request(),
|
||||
actions.grades.update.success({ courseId, data: gradebookData.data }),
|
||||
{
|
||||
...fetchMatchingUserGradesAction,
|
||||
args: [
|
||||
courseId,
|
||||
args.searchText,
|
||||
args.cohort,
|
||||
args.track,
|
||||
thunkActions.defaultAssignmentFilter,
|
||||
true,
|
||||
{ searchText: args.searchText },
|
||||
],
|
||||
},
|
||||
],
|
||||
));
|
||||
});
|
||||
describe('error response', () => {
|
||||
it('sends failure event', () => {
|
||||
const error = 'Some ERRor';
|
||||
return testFetch(
|
||||
(resolve, reject) => reject(error),
|
||||
[
|
||||
actions.grades.update.request(),
|
||||
actions.grades.update.failure({ courseId, error }),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateGradesIfAssignmentGradeFiltersSet', () => {
|
||||
const args = {
|
||||
cohort: 'coHOrt',
|
||||
track: 'trAck',
|
||||
assignmentType: 'bananas',
|
||||
};
|
||||
let assignmentGradeMin;
|
||||
let assignmentGradeMax;
|
||||
const mockFilters = (minValue, maxValue) => {
|
||||
assignmentGradeMax = jest.spyOn(
|
||||
selectors.filters, 'assignmentGradeMax',
|
||||
).mockReturnValue(maxValue);
|
||||
assignmentGradeMin = jest.spyOn(
|
||||
selectors.filters, 'assignmentGradeMin',
|
||||
).mockReturnValue(minValue);
|
||||
};
|
||||
const callUpdate = (expectedActions) => {
|
||||
const store = mockStore({});
|
||||
store.dispatch(thunkActions.updateGradesIfAssignmentGradeFiltersSet(
|
||||
courseId,
|
||||
args.cohort,
|
||||
args.track,
|
||||
args.assignmentType,
|
||||
));
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
};
|
||||
afterEach(() => {
|
||||
assignmentGradeMin.mockRestore();
|
||||
assignmentGradeMax.mockRestore();
|
||||
});
|
||||
describe('if neither assignment grade filter is set', () => {
|
||||
mockFilters(undefined, undefined);
|
||||
it('does not call', () => callUpdate([]));
|
||||
});
|
||||
describe('if either assignment grade filter is set', () => {
|
||||
const assertFetchGradesCalled = () => callUpdate([
|
||||
{
|
||||
...fetchGradesAction,
|
||||
args: [
|
||||
courseId,
|
||||
args.cohort,
|
||||
args.track,
|
||||
args.assignmentType,
|
||||
],
|
||||
},
|
||||
]);
|
||||
it('calls if min is set', () => {
|
||||
mockFilters(21, undefined);
|
||||
return assertFetchGradesCalled();
|
||||
});
|
||||
it('calls if max is set', () => {
|
||||
mockFilters(undefined, 92);
|
||||
return assertFetchGradesCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchPrevNextGrades', () => {
|
||||
const args = {
|
||||
endpoint: 'someEndpoint',
|
||||
cohort: 'COhoRT',
|
||||
track: 'TracK',
|
||||
assignmentType: '23',
|
||||
};
|
||||
const response = {
|
||||
results: gradesResults,
|
||||
previous: 'Prev',
|
||||
next: 'NEXT',
|
||||
total_users_count: 23,
|
||||
filtered_users_count: 12,
|
||||
};
|
||||
const { fetching } = actions.grades;
|
||||
let getClient;
|
||||
|
||||
const mockClient = (resolveFn) => {
|
||||
getClient = jest.spyOn(
|
||||
auth,
|
||||
'getAuthenticatedHttpClient',
|
||||
).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(new Promise(resolveFn)),
|
||||
});
|
||||
};
|
||||
|
||||
const testFetch = (
|
||||
resolveFn,
|
||||
expectedActions,
|
||||
) => {
|
||||
const store = mockStore({});
|
||||
mockClient(resolveFn);
|
||||
return store.dispatch(thunkActions.fetchPrevNextGrades(
|
||||
args.endpoint,
|
||||
courseId,
|
||||
args.cohort,
|
||||
args.track,
|
||||
args.assignmentType,
|
||||
)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
getClient.mockRestore();
|
||||
});
|
||||
|
||||
describe('valid data', () => {
|
||||
it('sends finished action and loads results', () => testFetch(
|
||||
(resolve) => resolve({ data: response }),
|
||||
[
|
||||
fetching.started(),
|
||||
fetching.received({
|
||||
grades: gradesResults.sort(sortAlphaAsc),
|
||||
prev: response.previous,
|
||||
next: response.next,
|
||||
totalUsersCount: response.total_users_count,
|
||||
filteredUsersCount: response.filtered_users_count,
|
||||
cohort: args.cohort,
|
||||
track: args.track,
|
||||
assignmentType: args.assignmentType,
|
||||
courseId,
|
||||
}),
|
||||
fetching.finished(),
|
||||
],
|
||||
));
|
||||
});
|
||||
describe('error response', () => {
|
||||
it('sends error action', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[fetching.started(), fetching.error()],
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitFileUploadFormData', () => {
|
||||
const formData = { form: 'data' };
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.uploadGradeCsv,
|
||||
thunkActions.submitFileUploadFormData,
|
||||
[courseId, formData],
|
||||
() => expect(LmsApiService.uploadGradeCsv).toHaveBeenCalledWith(courseId, formData),
|
||||
);
|
||||
const { csvUpload, uploadOverride } = actions.grades;
|
||||
describe('valid data', () => {
|
||||
it('sends csvUpload finished and uploadOverride success actions', () => {
|
||||
testFetch(
|
||||
(resolve) => resolve(),
|
||||
[
|
||||
csvUpload.started(),
|
||||
csvUpload.finished(),
|
||||
uploadOverride.success(courseId),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('error response', () => {
|
||||
describe('non-200 error', () => {
|
||||
it('sends uploadOverride failure w/ raw error and csvUploadError with default', () => {
|
||||
const error = { some: 'error' };
|
||||
testFetch((resolve, reject) => reject(error), [
|
||||
csvUpload.started(),
|
||||
uploadOverride.failure(courseId, error),
|
||||
csvUpload.error({ errorMessages: ['Unknown error.'] }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('200 error with no messages', () => {
|
||||
it('sends uploadOverride failure w/ raw error and csvUploadError with default', () => {
|
||||
const error = { status: 200, data: { error_messages: [] }, some: 'error' };
|
||||
testFetch((resolve, reject) => reject(error), [
|
||||
csvUpload.started(),
|
||||
uploadOverride.failure(courseId, error),
|
||||
csvUpload.error({ errorMessages: ['Unknown error.'] }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('200 error with messages', () => {
|
||||
it('sends uploadOverride failure w/ raw error and csvUploadError w loaded error', () => {
|
||||
const error = {
|
||||
status: 200,
|
||||
data: {
|
||||
error_messages: ['some', 'errors'], saved: 21, total: 32,
|
||||
},
|
||||
};
|
||||
testFetch((resolve, reject) => reject(error), [
|
||||
csvUpload.started(),
|
||||
uploadOverride.failure(courseId, error),
|
||||
csvUpload.error({
|
||||
errorMessages: error.data.error_messages,
|
||||
saved: error.data.saved,
|
||||
total: error.data.total,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
16
src/data/thunkActions/index.js
Normal file
16
src/data/thunkActions/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
import cohorts from './cohorts';
|
||||
import filters from './filters';
|
||||
import grades from './grades';
|
||||
import roles from './roles';
|
||||
import tracks from './tracks';
|
||||
|
||||
export default StrictDict({
|
||||
assignmentTypes,
|
||||
cohorts,
|
||||
filters,
|
||||
grades,
|
||||
roles,
|
||||
tracks,
|
||||
});
|
||||
43
src/data/thunkActions/roles.js
Normal file
43
src/data/thunkActions/roles.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { StrictDict } from 'utils';
|
||||
import roles from '../actions/roles';
|
||||
import selectors from '../selectors';
|
||||
|
||||
import { fetchCohorts } from './cohorts';
|
||||
import {
|
||||
fetchGrades,
|
||||
} from './grades';
|
||||
import { fetchTracks } from './tracks';
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
export const allowedRoles = ['staff', 'instructor', 'support'];
|
||||
|
||||
export const fetchRoles = courseId => (
|
||||
(dispatch, getState) => LmsApiService.fetchUserRoles(courseId)
|
||||
.then(response => response.data)
|
||||
.then((response) => {
|
||||
const isAllowedRole = (role) => (
|
||||
(role.course_id === courseId) && allowedRoles.includes(role.role)
|
||||
);
|
||||
const canUserViewGradebook = (response.is_staff || (response.roles.some(isAllowedRole)));
|
||||
|
||||
dispatch(roles.fetching.received({ canUserViewGradebook, courseId }));
|
||||
|
||||
const { cohort, track, assignmentType } = selectors.filters.allFilters(getState());
|
||||
if (canUserViewGradebook) {
|
||||
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
||||
dispatch(fetchTracks(courseId));
|
||||
dispatch(fetchCohorts(courseId));
|
||||
dispatch(fetchAssignmentTypes(courseId));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(roles.fetching.error());
|
||||
}));
|
||||
|
||||
export default StrictDict({
|
||||
allowedRoles,
|
||||
fetchRoles,
|
||||
});
|
||||
112
src/data/thunkActions/roles.test.js
Normal file
112
src/data/thunkActions/roles.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import actions from '../actions';
|
||||
import selectors from '../selectors';
|
||||
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
import { fetchCohorts } from './cohorts';
|
||||
import { fetchGrades } from './grades';
|
||||
import { fetchTracks } from './tracks';
|
||||
|
||||
import { allowedRoles, fetchRoles } from './roles';
|
||||
|
||||
jest.mock('../selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
filters: {
|
||||
allFilters: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
fetchUserRoles: jest.fn(),
|
||||
}));
|
||||
jest.mock('./assignmentTypes', () => ({
|
||||
fetchAssignmentTypes: jest.fn((...args) => ({ type: 'fetchAssignmentTypes', args })),
|
||||
}));
|
||||
jest.mock('./cohorts', () => ({
|
||||
fetchCohorts: jest.fn((...args) => ({ type: 'fetchCohorts', args })),
|
||||
}));
|
||||
jest.mock('./grades', () => ({
|
||||
fetchGrades: jest.fn((...args) => ({ type: 'fetchGrades', args })),
|
||||
}));
|
||||
jest.mock('./tracks', () => ({
|
||||
fetchTracks: jest.fn((...args) => ({ type: 'fetchTracks', args })),
|
||||
}));
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const allowedRole = { course_id: courseId, role: allowedRoles[0] };
|
||||
const responseData = {
|
||||
roles: [
|
||||
{ course_id: 'fakeCourseId', role: 'fakeROLE' },
|
||||
{ couse_id: 'anotherId', role: 'STuff' },
|
||||
],
|
||||
is_staff: false,
|
||||
};
|
||||
|
||||
describe('roles thunkActions', () => {
|
||||
const filters = {
|
||||
cohort: 'COHort',
|
||||
track: 'traCK',
|
||||
assignmentType: 23,
|
||||
};
|
||||
beforeAll(() => {
|
||||
selectors.filters.allFilters.mockReturnValue(filters);
|
||||
});
|
||||
describe('fetchRoles', () => {
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchUserRoles,
|
||||
fetchRoles,
|
||||
[courseId],
|
||||
);
|
||||
describe('valid response', () => {
|
||||
describe('cannot view gradebook (not is_staff, and no allowed roles)', () => {
|
||||
it('dispatches received with canUserViewGradeBook=false and the courseId', () => (
|
||||
testFetch((resolve) => resolve({ data: responseData }), [
|
||||
actions.roles.fetching.received({
|
||||
canUserViewGradebook: false,
|
||||
courseId,
|
||||
}),
|
||||
])
|
||||
));
|
||||
});
|
||||
describe('canUserViewGradebook (is_staff or some role is allowed)', () => {
|
||||
const testCanUserViewGradebookOutput = (resolveData) => {
|
||||
const resolveFn = (resolve) => resolve({ data: resolveData });
|
||||
const expectedActions = [
|
||||
'received with canUserViewGradebook=false and the courseId',
|
||||
'fetchGrades thunkAction with courseId and filters(cohort, track, and assignmentType)',
|
||||
'fetchTracks thunkAction with courseId',
|
||||
'fetchCohorts thunkAction with courseId',
|
||||
'fetchAssignmentTypes thunkAction with courseId',
|
||||
];
|
||||
it(`dispatches the appropriate actions: [\n ${expectedActions.join('\n ')}\n]`, () => testFetch(
|
||||
resolveFn,
|
||||
[
|
||||
actions.roles.fetching.received({ canUserViewGradebook: true, courseId }),
|
||||
fetchGrades(courseId, filters.cohort, filters.track, filters.assignmentType),
|
||||
fetchTracks(courseId),
|
||||
fetchCohorts(courseId),
|
||||
fetchAssignmentTypes(courseId),
|
||||
],
|
||||
));
|
||||
};
|
||||
describe('is_staff', () => testCanUserViewGradebookOutput({
|
||||
...responseData,
|
||||
is_staff: true,
|
||||
}));
|
||||
describe('has allowed role', () => testCanUserViewGradebookOutput({
|
||||
...responseData,
|
||||
roles: [...responseData.roles, allowedRole],
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe('actions dispatched on api error', () => {
|
||||
test('errorFetching', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[actions.roles.fetching.error()],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
55
src/data/thunkActions/testUtils.js
Normal file
55
src/data/thunkActions/testUtils.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
/** createTestFetcher(mockedMethod, thunkAction, args, onDispatch)
|
||||
* Creates a testFetch method, which will test a given thunkAction of the form:
|
||||
* ```
|
||||
* const <thunkAction> = (<args>) => (dispatch, getState) => {
|
||||
* ...
|
||||
* return <mockedMethod>.then().catch();
|
||||
* ```
|
||||
* The returned function will take a promise handler function, a list of expected actions
|
||||
* to have been dispatched (objects only), and an optional verifyFn method to be called after
|
||||
* the fetch has been completed.
|
||||
*
|
||||
* @param {fn} mockedMethod - already-mocked api method being exercised by the thunkAction.
|
||||
* @param {fn} thunkAction - thunkAction to call/test
|
||||
* @param {array} args - array of args to dispatch the thunkAction with
|
||||
* @param {[fn]} onDispatch - optional function to be called after dispatch
|
||||
*
|
||||
* @return {fn} testFetch method
|
||||
* @param {fn} resolveFn - promise handler of the form (resolve, reject) => {}.
|
||||
* should return a call to resolve or reject with response data.
|
||||
* @param {object[]} expectedActions - array of action objects expected to have been dispatched
|
||||
* will be verified after the thunkAction resolves
|
||||
* @param {[fn]} verifyFn - optional function to be called after dispatch
|
||||
*/
|
||||
export const createTestFetcher = (
|
||||
mockedMethod,
|
||||
thunkAction,
|
||||
args,
|
||||
onDispatch,
|
||||
) => (
|
||||
resolveFn,
|
||||
expectedActions,
|
||||
verifyFn,
|
||||
) => {
|
||||
const store = mockStore({});
|
||||
mockedMethod.mockReturnValue(new Promise(resolve => {
|
||||
resolve(new Promise(resolveFn));
|
||||
}));
|
||||
return store.dispatch(thunkAction(...args)).then(() => {
|
||||
if (onDispatch) { onDispatch(); }
|
||||
if (verifyFn) { verifyFn(); }
|
||||
if (expectedActions !== undefined) {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
createTestFetcher,
|
||||
};
|
||||
30
src/data/thunkActions/tracks.js
Normal file
30
src/data/thunkActions/tracks.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { StrictDict } from 'utils';
|
||||
import tracks from '../actions/tracks';
|
||||
|
||||
import selectors from '../selectors';
|
||||
|
||||
import { fetchBulkUpgradeHistory } from './grades';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
export const fetchTracks = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(tracks.fetching.started());
|
||||
return LmsApiService.fetchTracks(courseId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(tracks.fetching.received(data.course_modes));
|
||||
if (selectors.tracks.hasMastersTrack(data.course_modes)) {
|
||||
dispatch(fetchBulkUpgradeHistory(courseId));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(tracks.fetching.error());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default StrictDict({
|
||||
fetchTracks,
|
||||
});
|
||||
85
src/data/thunkActions/tracks.test.js
Normal file
85
src/data/thunkActions/tracks.test.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import actions from '../actions';
|
||||
import selectors from '../selectors';
|
||||
|
||||
import { fetchBulkUpgradeHistory } from './grades';
|
||||
import { fetchTracks } from './tracks';
|
||||
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
fetchTracks: jest.fn(),
|
||||
}));
|
||||
jest.mock('../selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
tracks: { hasMastersTrack: jest.fn(() => false) },
|
||||
},
|
||||
}));
|
||||
jest.mock('./grades', () => ({
|
||||
fetchBulkUpgradeHistory: jest.fn((...args) => ({ type: 'fetchBulkUpgradeHistory', args })),
|
||||
}));
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const responseData = {
|
||||
couse_modes: ['some', 'course', 'modes'],
|
||||
};
|
||||
|
||||
describe('tracjs thunkActions', () => {
|
||||
describe('fetchTracks', () => {
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchTracks,
|
||||
fetchTracks,
|
||||
[courseId],
|
||||
);
|
||||
describe('valid response', () => {
|
||||
describe('if not hasMastersTrack(data.course_modes)', () => {
|
||||
describe('dispatched actions', () => {
|
||||
beforeEach(() => {
|
||||
selectors.tracks.hasMastersTrack.mockReturnValue(false);
|
||||
});
|
||||
const expectedActions = [
|
||||
'tracks.fetching.started',
|
||||
'tracks.fetching.received with course_modes',
|
||||
];
|
||||
it(`dispatches [${expectedActions.join(', ')}]`, () => testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
[
|
||||
actions.tracks.fetching.started(),
|
||||
actions.tracks.fetching.received(responseData.course_modes),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
describe('if hasMastersTrack(data.course_modes)', () => {
|
||||
describe('dispatched actions', () => {
|
||||
beforeEach(() => {
|
||||
selectors.tracks.hasMastersTrack.mockReturnValue(true);
|
||||
});
|
||||
const expectedActions = [
|
||||
'fetching.started',
|
||||
'fetching.received with course_modes',
|
||||
'fetchBulkUpgradeHistory thunkAction with courseId',
|
||||
];
|
||||
test(`[${expectedActions.join(', ')}]`, () => testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
[
|
||||
actions.tracks.fetching.started(),
|
||||
actions.tracks.fetching.received(responseData.course_modes),
|
||||
fetchBulkUpgradeHistory(courseId),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('actions dispatched on api error', () => {
|
||||
test('errorFetching', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[
|
||||
actions.tracks.fetching.started(),
|
||||
actions.tracks.fetching.error(),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
39
src/utils/StrictDict.js
Normal file
39
src/utils/StrictDict.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/* eslint-disable no-console */
|
||||
import util from 'util';
|
||||
|
||||
const staticReturnOptions = [
|
||||
'dict',
|
||||
'inspect',
|
||||
Symbol.toStringTag,
|
||||
util.inspect.custom,
|
||||
Symbol.for('nodejs.util.inspect.custom'),
|
||||
];
|
||||
|
||||
const strictGet = (target, name) => {
|
||||
if (name === Symbol.toStringTag) {
|
||||
return target;
|
||||
}
|
||||
if (name === 'length') {
|
||||
return target.length;
|
||||
}
|
||||
if (staticReturnOptions.indexOf(name) >= 0) {
|
||||
return target;
|
||||
}
|
||||
if (name === Symbol.iterator) {
|
||||
return { ...target };
|
||||
}
|
||||
|
||||
if (name in target || name === '_reactFragment') {
|
||||
return target[name];
|
||||
}
|
||||
|
||||
console.log(name.toString());
|
||||
console.error({ target, name });
|
||||
const e = Error(`invalid property "${name.toString()}"`);
|
||||
console.error(e.stack);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const StrictDict = (dict) => new Proxy(dict, { get: strictGet });
|
||||
|
||||
export default StrictDict;
|
||||
2
src/utils/index.js
Normal file
2
src/utils/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as StrictDict } from './StrictDict';
|
||||
Reference in New Issue
Block a user