Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec81eb47d9 | ||
|
|
cd2a5ae903 | ||
|
|
7a02330e9e | ||
|
|
a929194a29 | ||
|
|
febf4d99c6 | ||
|
|
83ed8ab875 | ||
|
|
6563f54590 | ||
|
|
e1fe31dc94 | ||
|
|
8754263584 | ||
|
|
c660bd8d15 | ||
|
|
df32123f34 | ||
|
|
e81db01be2 | ||
|
|
05c3468d93 | ||
|
|
1a88343be9 | ||
|
|
6f752f3a18 | ||
|
|
7bbc9a84dc | ||
|
|
85cf3e35e4 | ||
|
|
85fa6bca72 | ||
|
|
231685e78d | ||
|
|
a4dc135129 | ||
|
|
2c890e53f8 | ||
|
|
33556fd749 | ||
|
|
8a62e8b710 |
@@ -3,6 +3,7 @@
|
||||
const Merge = require('webpack-merge');
|
||||
const commonConfig = require('./webpack.common.config.js');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
@@ -91,5 +92,20 @@ module.exports = Merge.smart(commonConfig, {
|
||||
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
|
||||
template: path.resolve(__dirname, '../public/index.html'),
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production',
|
||||
BASE_URL: null,
|
||||
LMS_BASE_URL: null,
|
||||
LOGIN_URL: null,
|
||||
LOGOUT_URL: null,
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: null,
|
||||
DATA_API_BASE_URL: null,
|
||||
SEGMENT_KEY: null,
|
||||
FEATURE_FLAGS: {},
|
||||
ACCESS_TOKEN_COOKIE_NAME: null,
|
||||
CSRF_COOKIE_NAME: 'csrftoken',
|
||||
NEW_RELIC_APP_ID: null,
|
||||
NEW_RELIC_LICENSE_KEY: null,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
78
package-lock.json
generated
78
package-lock.json
generated
@@ -2988,14 +2988,14 @@
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.6.1.tgz",
|
||||
"integrity": "sha512-OtawJThYlvRgm9BXK+xTL7BIlDx8vv21j+fbQDjRRUyok6y7NyjlweGorielTahLZHYIdKUoK2Dp9ByVWuMqxw==",
|
||||
"version": "16.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz",
|
||||
"integrity": "sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.11.0"
|
||||
"scheduler": "^0.11.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
@@ -3055,6 +3055,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz",
|
||||
"integrity": "sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
|
||||
@@ -3066,9 +3075,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/paragon": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-3.7.0.tgz",
|
||||
"integrity": "sha512-BFzPA03CjzSMEwz4utSu5Nzh9tz5kShrWHuBNZvTtwD529ObYK52C7occ9Eid2jVHtAkbwkBNQdLzz6KscCniw==",
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-3.7.1.tgz",
|
||||
"integrity": "sha512-HmcWcCL6ocz0yoTajeSEt62a/CPM+Jz/Ch+RzxZhvUrlced6u7bSrjM0CdK7rlz4sZKI5oPQzZe199x1KHXk6Q==",
|
||||
"requires": {
|
||||
"@edx/edx-bootstrap": "^1.0.0",
|
||||
"@sambego/storybook-styles": "^1.0.0",
|
||||
@@ -3103,14 +3112,14 @@
|
||||
"integrity": "sha512-gulJE5dGFo6Q61V/whS6VM4WIyrlydXfCgkE+Gxe5hjrJ8rXLLZlALq7zq2RPhOc45PSwQpJkrTnc2KgD6cvmA=="
|
||||
},
|
||||
"react": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.6.1.tgz",
|
||||
"integrity": "sha512-OtawJThYlvRgm9BXK+xTL7BIlDx8vv21j+fbQDjRRUyok6y7NyjlweGorielTahLZHYIdKUoK2Dp9ByVWuMqxw==",
|
||||
"version": "16.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz",
|
||||
"integrity": "sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.11.0"
|
||||
"scheduler": "^0.11.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
@@ -3123,6 +3132,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz",
|
||||
"integrity": "sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7254,8 +7272,7 @@
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
||||
"dev": true
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "3.3.0",
|
||||
@@ -13485,6 +13502,16 @@
|
||||
"cast-array": "~1.0.0",
|
||||
"object-filter": "~1.0.2",
|
||||
"query-string": "~2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"query-string": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "http://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
|
||||
"integrity": "sha1-fbBmZCCAS6qSrp8miWKFWnYUPfs=",
|
||||
"requires": {
|
||||
"strict-uri-encode": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
@@ -19187,11 +19214,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"query-string": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
|
||||
"integrity": "sha1-fbBmZCCAS6qSrp8miWKFWnYUPfs=",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.2.0.tgz",
|
||||
"integrity": "sha512-5wupExkIt8RYL4h/FE+WTg3JHk62e6fFPWtAZA9J5IWK1PfTfKkMS93HBUHcFpeYi9KsY5pFbh+ldvEyaz5MyA==",
|
||||
"requires": {
|
||||
"strict-uri-encode": "^1.0.0"
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"strict-uri-encode": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"strict-uri-encode": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
|
||||
}
|
||||
}
|
||||
},
|
||||
"querystring": {
|
||||
@@ -20479,15 +20514,6 @@
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
|
||||
"dev": true
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.0.tgz",
|
||||
"integrity": "sha512-MAYbBfmiEHxF0W+c4CxMpEqMYK+rYF584VP/qMKSiHM6lTkBKKYOJaDiSILpJHla6hBOsVd6GucPL46o2Uq3sg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz",
|
||||
|
||||
@@ -25,14 +25,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/edx-bootstrap": "^0.4.3",
|
||||
"@edx/frontend-auth": "^1.1.0",
|
||||
"@edx/paragon": "^3.7.0",
|
||||
"@edx/frontend-auth": "1.1.0",
|
||||
"@edx/paragon": "^3.7.1",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"classnames": "^2.2.5",
|
||||
"email-prop-type": "^1.1.5",
|
||||
"font-awesome": "^4.7.0",
|
||||
"history": "^4.7.2",
|
||||
"prop-types": "^15.5.10",
|
||||
"query-string": "^6.2.0",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-redux": "^5.0.7",
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
.back-link{
|
||||
float:right;
|
||||
}
|
||||
.student-filters{
|
||||
display: flex;
|
||||
.label{
|
||||
padding-top: 30px;
|
||||
}
|
||||
.form-group{
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.gbook {
|
||||
overflow-x: scroll;
|
||||
|
||||
|
||||
@@ -1,214 +1,200 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import emailPropType from 'email-prop-type';
|
||||
import { SearchField, Table, Modal } from '@edx/paragon';
|
||||
|
||||
import { Button, InputSelect, Modal, SearchField, StatusAlert, Table } from '@edx/paragon';
|
||||
import queryString from 'query-string';
|
||||
import { configuration } from '../../config';
|
||||
|
||||
export default class Gradebook extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
grades: [], // this.mapUserEnteriesPercent(this.props.grades).sort(this.sortAlphaDesc),
|
||||
grades: [], // this.mapUserEntriesPercent(this.props.grades).sort(this.sortAlphaDesc),
|
||||
headings: [], // this.mapHeadings(this.props.grades[0]),
|
||||
filterValue: '',
|
||||
modalContent: (<h1>Hello, World!</h1>),
|
||||
modalOpen: false,
|
||||
modalModel: [{}],
|
||||
updateVal: 0,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.getUserGrades(this.props.match.params.courseId);
|
||||
const urlQuery = queryString.parse(this.props.location.search);
|
||||
this.props.getUserGrades(
|
||||
this.props.match.params.courseId,
|
||||
urlQuery.cohort,
|
||||
urlQuery.track,
|
||||
);
|
||||
this.props.getTracks(this.props.match.params.courseId);
|
||||
this.props.getCohorts(this.props.match.params.courseId);
|
||||
}
|
||||
|
||||
sortAlphaDesc = (gradeRowA, gradeRowB) => {
|
||||
const a = gradeRowA.username.toUpperCase();
|
||||
const b = gradeRowB.username.toUpperCase();
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||
const a = gradeRowA.username.toUpperCase();
|
||||
const b = gradeRowB.username.toUpperCase();
|
||||
if (a < b) {
|
||||
return 1;
|
||||
}
|
||||
if (a > b) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
sortNumerically = (colKey, direction) => {
|
||||
function sortNumAsc(gradeRowA, gradeRowB) {
|
||||
if (gradeRowA[colKey] < gradeRowB[colKey]) {
|
||||
return -1;
|
||||
}
|
||||
if (gradeRowA[colKey] > gradeRowB[colKey]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortNumDesc(gradeRowA, gradeRowB) {
|
||||
if (gradeRowA[colKey] < gradeRowB[colKey]) {
|
||||
return 1;
|
||||
}
|
||||
if (gradeRowA[colKey] > gradeRowB[colKey]) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.setState({ grades: [...this.state.grades].sort(direction === 'desc' ? sortNumDesc : sortNumAsc) });
|
||||
setNewModalState = (userEntry, subsection) => {
|
||||
this.setState({
|
||||
modalModel: [{
|
||||
username: userEntry.username,
|
||||
currentGrade: `${subsection.score_earned}/${subsection.score_possible}`,
|
||||
adjustedGrade: (
|
||||
<span>
|
||||
<input
|
||||
style={{ width: '25px' }}
|
||||
type="text"
|
||||
onChange={event => this.setState({ updateVal: event.target.value })}
|
||||
/> / {subsection.score_possible}
|
||||
</span>
|
||||
),
|
||||
assignmentName: `${subsection.subsection_name}`,
|
||||
}],
|
||||
modalOpen: true,
|
||||
updateModuleId: subsection.module_id,
|
||||
updateUserId: userEntry.user_id,
|
||||
});
|
||||
}
|
||||
|
||||
mapHeadings = (entry) => {
|
||||
if (entry) {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => {
|
||||
this.setState({
|
||||
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc),
|
||||
});
|
||||
handleAdjustedGradeClick = () => {
|
||||
this.props.updateGrades(this.props.match.params.courseId, [
|
||||
{
|
||||
user_id: this.state.updateUserId,
|
||||
usage_id: this.state.updateModuleId,
|
||||
grade: {
|
||||
earned_graded_override: this.state.updateVal,
|
||||
},
|
||||
}];
|
||||
},
|
||||
]);
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label)
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { this.sortNumerically(s.label, direction); },
|
||||
}));
|
||||
this.setState({
|
||||
modalModel: [{}],
|
||||
modalOpen: false,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
});
|
||||
}
|
||||
|
||||
const totals = [{
|
||||
label: 'Total',
|
||||
key: 'total',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { this.sortNumerically('total', direction); },
|
||||
}];
|
||||
updateQueryParams = (queryKey, queryValue) => {
|
||||
const parsed = queryString.parse(this.props.location.search);
|
||||
parsed[queryKey] = queryValue;
|
||||
return `?${queryString.stringify(parsed)}`;
|
||||
};
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
mapCohortsEntries = (entries) => {
|
||||
const mapped = entries.map(entry => ({
|
||||
id: entry.id,
|
||||
label: entry.name,
|
||||
}));
|
||||
mapped.unshift({ id: 0, label: 'Cohort-All' });
|
||||
return mapped;
|
||||
};
|
||||
|
||||
mapTracksEntries = (entries) => {
|
||||
const mapped = entries.map(entry => ({
|
||||
id: entry.slug,
|
||||
label: entry.name,
|
||||
}));
|
||||
mapped.unshift({ label: 'Track-All' });
|
||||
return mapped;
|
||||
};
|
||||
|
||||
updateTracks = (event) => {
|
||||
const selectedTrackItem = this.props.tracks.find(x => x.name === event);
|
||||
let selectedTrackSlug = null;
|
||||
if (selectedTrackItem) {
|
||||
selectedTrackSlug = selectedTrackItem.slug;
|
||||
}
|
||||
return [];
|
||||
this.props.getUserGrades(
|
||||
this.props.match.params.courseId,
|
||||
this.props.selectedCohort,
|
||||
selectedTrackSlug,
|
||||
);
|
||||
const updatedQueryStrings = this.updateQueryParams('track', selectedTrackSlug);
|
||||
this.props.history.push(updatedQueryStrings);
|
||||
};
|
||||
|
||||
mapHeadingsHw = (entry) => {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => {
|
||||
this.setState({
|
||||
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc),
|
||||
});
|
||||
},
|
||||
}];
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label && section.category == 'Homework')
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { this.sortNumerically(s.label, direction); },
|
||||
}));
|
||||
|
||||
return results.concat(assignmentHeadings);
|
||||
updateCohorts = (event) => {
|
||||
const selectedCohortItem = this.props.cohorts.find(x => x.name === event);
|
||||
let selectedCohortId = null;
|
||||
if (selectedCohortItem) {
|
||||
selectedCohortId = selectedCohortItem.id;
|
||||
}
|
||||
this.props.getUserGrades(
|
||||
this.props.match.params.courseId,
|
||||
selectedCohortId,
|
||||
this.props.selectedTrack,
|
||||
);
|
||||
const updatedQueryStrings = this.updateQueryParams('cohort', selectedCohortId);
|
||||
this.props.history.push(updatedQueryStrings);
|
||||
};
|
||||
|
||||
mapHeadingsExam = (entry) => {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => {
|
||||
this.setState({
|
||||
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc),
|
||||
});
|
||||
},
|
||||
}];
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label && section.category == 'Exam')
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { this.sortNumerically(s.label, direction); },
|
||||
}));
|
||||
|
||||
return results.concat(assignmentHeadings);
|
||||
mapSelectedCohortEntry = (entry) => {
|
||||
const selectedCohortEntry = this.props.cohorts.find(x => x.id === parseInt(entry, 10));
|
||||
if (selectedCohortEntry) {
|
||||
return selectedCohortEntry.name;
|
||||
}
|
||||
return 'Cohorts';
|
||||
};
|
||||
|
||||
mapUserEnteriesPercent = entries => entries.map((entry) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, s) => {
|
||||
acc[s.label] = (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={() => this.setState({
|
||||
modalModel: [{
|
||||
username: entry.username,
|
||||
autoGrade: `${s.score_earned}/${s.score_possible}`,
|
||||
adjustedGrade: (<span><input style={{ width: '25px' }} type="text" value={this.updateVal} /> / {s.score_possible}</span>),
|
||||
assignmentName: `${s.subsection_name}`,
|
||||
}],
|
||||
modalOpen: true,
|
||||
})}
|
||||
>
|
||||
{s.percent}
|
||||
</button>);
|
||||
return acc;
|
||||
}, {});
|
||||
const totals = { total: entry.percent * 100 };
|
||||
return Object.assign(results, assignments, totals);
|
||||
});
|
||||
mapSelectedTrackEntry = (entry) => {
|
||||
const selectedTrackEntry = this.props.tracks.find(x => x.slug === entry);
|
||||
if (selectedTrackEntry) {
|
||||
return selectedTrackEntry.name;
|
||||
}
|
||||
return 'Tracks';
|
||||
};
|
||||
|
||||
mapUserEnteriesAbsolute = entries => entries.map((entry) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, s) => {
|
||||
acc[s.label] = (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={() => this.setState({
|
||||
modalModel: [{
|
||||
username: entry.username,
|
||||
autoGrade: `${s.score_earned}/${s.score_possible}`,
|
||||
adjustedGrade: (<span><input style={{ width: '25px' }} type="text" value={this.updateVal} /> / {s.score_possible}</span>),
|
||||
assignmentName: `${s.subsection_name}`,
|
||||
}],
|
||||
modalOpen: true,
|
||||
})}
|
||||
>
|
||||
{s.score_earned}/{s.score_possible}
|
||||
</button>);
|
||||
return acc;
|
||||
}, {});
|
||||
formatter = {
|
||||
percent: entries => entries.map((entry) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, subsection) => {
|
||||
acc[subsection.label] = (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={() => this.setNewModalState(entry, subsection)}
|
||||
>
|
||||
{subsection.percent}
|
||||
</button>);
|
||||
return acc;
|
||||
}, {});
|
||||
const totals = { total: entry.percent * 100 };
|
||||
return Object.assign(results, assignments, totals);
|
||||
}),
|
||||
|
||||
const totals = { total: entry.percent * 100 };
|
||||
return Object.assign(results, assignments, totals);
|
||||
});
|
||||
absolute: entries => entries.map((entry) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, subsection) => {
|
||||
acc[subsection.label] = (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={() => this.setNewModalState(entry, subsection)}
|
||||
>
|
||||
{subsection.score_earned}/{subsection.score_possible}
|
||||
</button>);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const totals = { total: entry.percent * 100 };
|
||||
return Object.assign(results, assignments, totals);
|
||||
}),
|
||||
};
|
||||
|
||||
lmsInstructorDashboardUrl = courseId => `${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="d-flex justify-content-center">
|
||||
<div className="card" style={{ width: '50rem' }}>
|
||||
<div className="card-body">
|
||||
<a
|
||||
href={this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}
|
||||
className="back-link"
|
||||
>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>Gradebook</h1>
|
||||
<h3> {this.props.match.params.courseId}</h3>
|
||||
<hr />
|
||||
<div className="d-flex justify-content-between" >
|
||||
<div>
|
||||
@@ -217,13 +203,13 @@ export default class Gradebook extends React.Component {
|
||||
<span>
|
||||
<input
|
||||
id="score-view-percent"
|
||||
className="ml-2"
|
||||
className="ml-2 mr-1"
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="percent"
|
||||
onClick={() => this.setState({ grades: this.mapUserEnteriesPercent(this.props.results).sort(this.sortAlphaDesc) })}
|
||||
onClick={() => this.props.toggleFormat('percent')}
|
||||
/>
|
||||
<label className="ml-2 mr-2" htmlFor="score-view-percent">Percent</label>
|
||||
<label className="mr-2" htmlFor="score-view-percent">Percent</label>
|
||||
</span>
|
||||
<span>
|
||||
<input
|
||||
@@ -231,76 +217,99 @@ export default class Gradebook extends React.Component {
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="absolute"
|
||||
onClick={() => this.setState({ grades: this.mapUserEnteriesAbsolute(this.props.results).sort(this.sortAlphaDesc) })}
|
||||
className="mr-1"
|
||||
onClick={() => this.props.toggleFormat('absolute')}
|
||||
/>
|
||||
<label className="ml-2 mr-2" htmlFor="score-view-absolute">Absolute</label>
|
||||
<label htmlFor="score-view-absolute">Absolute</label>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Category:
|
||||
<span>
|
||||
<label className="ml-2 mr-2" htmlFor="category-all">
|
||||
<input
|
||||
id="category-all"
|
||||
className="ml-2"
|
||||
type="radio"
|
||||
name="category"
|
||||
value="all"
|
||||
onClick={() =>
|
||||
this.setState({ headings: this.mapHeadings(this.props.results[0]) })}
|
||||
/>
|
||||
<input
|
||||
id="category-all"
|
||||
className="ml-2 mr-1"
|
||||
type="radio"
|
||||
name="category"
|
||||
value="all"
|
||||
onClick={() => this.props.filterColumns('all', this.props.grades[0])}
|
||||
/>
|
||||
<label className="mr-2" htmlFor="category-all">
|
||||
All
|
||||
</label>
|
||||
</span>
|
||||
<span>
|
||||
<input
|
||||
id="category-homework"
|
||||
className="ml-2"
|
||||
className="mr-1"
|
||||
type="radio"
|
||||
name="category"
|
||||
value="homework"
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
headings: this.mapHeadingsHw(this.props.results[0]),
|
||||
})}
|
||||
onClick={() => this.props.filterColumns('hw', this.props.grades[0])}
|
||||
/>
|
||||
<label className="ml-2 mr-2" htmlFor="category-homework">Homework</label>
|
||||
<label className="mr-2" htmlFor="category-homework">Homework</label>
|
||||
</span>
|
||||
<span>
|
||||
<label className="ml-2 mr-2" htmlFor="Exam">
|
||||
<input
|
||||
id="category-exam"
|
||||
type="radio"
|
||||
name="category"
|
||||
value="exam"
|
||||
onClick={() => this.setState({ headings: this.mapHeadingsExam(this.props.results[0]) })}
|
||||
/>
|
||||
<input
|
||||
id="category-exam"
|
||||
type="radio"
|
||||
name="category"
|
||||
value="exam"
|
||||
className="ml-2 mr-1"
|
||||
onClick={() => this.props.filterColumns('exam', this.props.grades[0])}
|
||||
/>
|
||||
<label htmlFor="Exam">
|
||||
Exam
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
{(this.props.tracks.length > 0 || this.props.cohorts.length > 0) &&
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Student Groups:
|
||||
</span>
|
||||
{this.props.tracks.length > 0 &&
|
||||
<InputSelect
|
||||
name="Tracks"
|
||||
value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
|
||||
options={this.mapTracksEntries(this.props.tracks)}
|
||||
onChange={this.updateTracks}
|
||||
/>
|
||||
}
|
||||
{this.props.cohorts.length > 0 &&
|
||||
<InputSelect
|
||||
name="Cohorts"
|
||||
value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
|
||||
options={this.mapCohortsEntries(this.props.cohorts)}
|
||||
onChange={this.updateCohorts}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginLeft: '10px', marginBottom: '10px' }}>
|
||||
<a href="https://www.google./com">Download Grade Report</a>
|
||||
<a href={`${this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}#view-data_download`}>Download Grade Report</a>
|
||||
</div>
|
||||
<SearchField
|
||||
onSubmit={() => this.setState({ grades: this.mapUserEnteriesPercent(this.props.results).filter(entry => entry.username === '' || entry.username.includes(this.state.filterValue)) }) }
|
||||
onSubmit={value => this.props.searchForUser(this.props.match.params.courseId, value, this.props.selectedCohort, this.props.selectedTrack)}
|
||||
onChange={filterValue => this.setState({ filterValue })}
|
||||
onClear={() => this.setState({
|
||||
grades: this.mapUserEnteriesPercent(this.props.results)
|
||||
.sort(this.sortAlphaDesc),
|
||||
})}
|
||||
onClear={() => this.props.getUserGrades(this.props.match.params.courseId, this.props.selectedCohort, this.props.selectedTrack)}
|
||||
value={this.state.filterValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<StatusAlert
|
||||
alertType="success"
|
||||
dialog="The grade has been successfully edited."
|
||||
onClose={() => this.props.updateBanner(false)}
|
||||
open={this.props.showSuccess}
|
||||
/>
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={this.mapHeadings(this.props.grades[0])}
|
||||
data={this.mapUserEnteriesPercent(this.props.grades)}
|
||||
tableSortable
|
||||
columns={this.props.headings}
|
||||
data={this.formatter[this.props.format](this.props.grades)}
|
||||
defaultSortDirection="desc"
|
||||
defaultSortedColumn="username"
|
||||
/>
|
||||
@@ -312,15 +321,28 @@ export default class Gradebook extends React.Component {
|
||||
<div>
|
||||
<h3>{this.state.modalModel[0].assignmentName}</h3>
|
||||
<Table
|
||||
columns={[{ label: 'Username', key: 'username' }, { label: 'Auto grade', key: 'autoGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
|
||||
data={this.state.modalModel}
|
||||
tableSortable
|
||||
defaultSortDirection="desc"
|
||||
defaultSortedColumn="username"
|
||||
/>
|
||||
columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
|
||||
data={this.state.modalModel}
|
||||
tableSortable
|
||||
defaultSortDirection="desc"
|
||||
defaultSortedColumn="username"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onClose={() => this.setState({ modalOpen: false })}
|
||||
)}
|
||||
buttons={[
|
||||
<Button
|
||||
label="Edit Grade"
|
||||
buttonType="primary"
|
||||
onClick={this.handleAdjustedGradeClick}
|
||||
/>,
|
||||
]}
|
||||
onClose={() => this.setState({
|
||||
modalOpen: false,
|
||||
modalModel: [{}],
|
||||
updateVal: 0,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,19 +351,3 @@ export default class Gradebook extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// CommentDetails.defaultProps = {
|
||||
// id: null,
|
||||
// postId: null,
|
||||
// name: '',
|
||||
// email: 'example@example.com',
|
||||
// body: '',
|
||||
// };
|
||||
|
||||
// CommentDetails.propTypes = {
|
||||
// id: PropTypes.number,
|
||||
// postId: PropTypes.number,
|
||||
// name: PropTypes.string,
|
||||
// email: emailPropType,
|
||||
// body: PropTypes.string,
|
||||
// };
|
||||
|
||||
|
||||
@@ -1,18 +1,55 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Gradebook from '../../components/Gradebook';
|
||||
import { fetchGrades } from '../../data/actions/grades';
|
||||
import {
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
filterColumns,
|
||||
updateBanner,
|
||||
} from '../../data/actions/grades';
|
||||
import { fetchCohorts } from '../../data/actions/cohorts';
|
||||
import { fetchTracks } from '../../data/actions/tracks';
|
||||
|
||||
const mapStateToProps = state => (
|
||||
{
|
||||
grades: state.grades.results,
|
||||
headings: state.grades.headings,
|
||||
tracks: state.tracks.results,
|
||||
cohorts: state.cohorts.results,
|
||||
selectedTrack: state.grades.selectedTrack,
|
||||
selectedCohort: state.grades.selectedCohort,
|
||||
format: state.grades.gradeFormat,
|
||||
showSuccess: state.grades.showSuccess,
|
||||
}
|
||||
);
|
||||
|
||||
const mapDispatchToProps = dispatch => (
|
||||
{
|
||||
getUserGrades: (courseId) => {
|
||||
dispatch(fetchGrades(courseId));
|
||||
getUserGrades: (courseId, cohort, track) => {
|
||||
dispatch(fetchGrades(courseId, cohort, track));
|
||||
},
|
||||
searchForUser: (courseId, searchText, cohort, track) => {
|
||||
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track));
|
||||
},
|
||||
getCohorts: (courseId) => {
|
||||
dispatch(fetchCohorts(courseId));
|
||||
},
|
||||
getTracks: (courseId) => {
|
||||
dispatch(fetchTracks(courseId));
|
||||
},
|
||||
updateGrades: (courseId, updateData) => {
|
||||
dispatch(updateGrades(courseId, updateData));
|
||||
},
|
||||
toggleFormat: (formatType) => {
|
||||
dispatch(toggleGradeFormat(formatType));
|
||||
},
|
||||
filterColumns: (filterType, exampleUser) => {
|
||||
dispatch(filterColumns(filterType, exampleUser));
|
||||
},
|
||||
updateBanner: (showSuccess) => {
|
||||
dispatch(updateBanner(showSuccess));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
31
src/data/actions/cohorts.js
Normal file
31
src/data/actions/cohorts.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const startedFetchingCohorts = () => ({ type: STARTED_FETCHING_COHORTS });
|
||||
const errorFetchingCohorts = () => ({ type: ERROR_FETCHING_COHORTS });
|
||||
const gotCohorts = cohorts => ({ type: GOT_COHORTS, cohorts });
|
||||
|
||||
const fetchCohorts = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingCohorts());
|
||||
return LmsApiService.fetchCohorts(courseId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotCohorts(data.cohorts));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(errorFetchingCohorts());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
fetchCohorts,
|
||||
startedFetchingCohorts,
|
||||
gotCohorts,
|
||||
errorFetchingCohorts,
|
||||
};
|
||||
@@ -3,25 +3,91 @@ import {
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
GRADE_UPDATE_REQUEST,
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
SORT_GRADES,
|
||||
FILTER_COLUMNS,
|
||||
UPDATE_BANNER,
|
||||
} from '../constants/actionTypes/grades';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import { headingMapper } from './utils';
|
||||
|
||||
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
|
||||
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
|
||||
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
|
||||
const gotGrades = grades => ({ type: GOT_GRADES, grades });
|
||||
const gotGrades = (grades, cohort, track, headings) => ({
|
||||
type: GOT_GRADES,
|
||||
grades,
|
||||
cohort,
|
||||
track,
|
||||
headings,
|
||||
});
|
||||
|
||||
const fetchGrades = courseId => (
|
||||
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
|
||||
const gradeUpdateSuccess = (responseData) => ({
|
||||
type: GRADE_UPDATE_SUCCESS,
|
||||
payload: { responseData },
|
||||
});
|
||||
const gradeUpdateFailure = error => ({
|
||||
type: GRADE_UPDATE_FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
|
||||
const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType });
|
||||
const sortGrades = (columnName, direction) => ({ type: SORT_GRADES, columnName, direction });
|
||||
|
||||
const filterColumns = (filterType, exampleUser) => ({
|
||||
type: FILTER_COLUMNS,
|
||||
headings: headingMapper[filterType](exampleUser)
|
||||
});
|
||||
|
||||
const updateBanner = (showSuccess) => ({ type: UPDATE_BANNER, showSuccess });
|
||||
|
||||
const fetchGrades = (courseId, cohort, track, showSuccess) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
return LmsApiService.fetchGradebookData(courseId)
|
||||
return LmsApiService.fetchGradebookData(courseId, null, cohort, track)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotGrades(data.results));
|
||||
dispatch(gotGrades(data.results, cohort, track, headingMapper.all(data.results[0])));
|
||||
dispatch(finishedFetchingGrades());
|
||||
dispatch(updateBanner(!!showSuccess));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingGrades());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const fetchMatchingUserGrades = (courseId, searchText, cohort, track) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotGrades(data.results, cohort, track));
|
||||
dispatch(finishedFetchingGrades());
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingGrades());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const updateGrades = (courseId, updateData) => (
|
||||
(dispatch) => {
|
||||
dispatch(gradeUpdateRequest());
|
||||
return LmsApiService.updateGradebookData(courseId, updateData)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gradeUpdateSuccess(data))
|
||||
dispatch(fetchGrades(courseId, null, null, true))
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(errorFetchingGrades())
|
||||
dispatch(gradeUpdateFailure(error));
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -32,4 +98,13 @@ export {
|
||||
errorFetchingGrades,
|
||||
gotGrades,
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
gradeUpdateRequest,
|
||||
gradeUpdateSuccess,
|
||||
gradeUpdateFailure,
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
sortGrades,
|
||||
filterColumns,
|
||||
updateBanner,
|
||||
};
|
||||
|
||||
31
src/data/actions/tracks.js
Normal file
31
src/data/actions/tracks.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const startedFetchingTracks = () => ({ type: STARTED_FETCHING_TRACKS });
|
||||
const errorFetchingTracks = () => ({ type: ERROR_FETCHING_TRACKS });
|
||||
const gotTracks = tracks => ({ type: GOT_TRACKS, tracks });
|
||||
|
||||
const fetchTracks = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingTracks());
|
||||
return LmsApiService.fetchTracks(courseId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotTracks(data.course_modes));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(errorFetchingTracks());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
fetchTracks,
|
||||
startedFetchingTracks,
|
||||
gotTracks,
|
||||
errorFetchingTracks,
|
||||
};
|
||||
131
src/data/actions/utils.js
Normal file
131
src/data/actions/utils.js
Normal file
@@ -0,0 +1,131 @@
|
||||
|
||||
const sortAlphaDesc = (gradeRowA, gradeRowB) => {
|
||||
const a = gradeRowA.username.toUpperCase();
|
||||
const b = gradeRowB.username.toUpperCase();
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||
const a = gradeRowA.username.toUpperCase();
|
||||
const b = gradeRowB.username.toUpperCase();
|
||||
if (a < b) {
|
||||
return 1;
|
||||
}
|
||||
if (a > b) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const sortNumerically = (colKey, direction) => {
|
||||
function sortNumAsc(gradeRowA, gradeRowB) {
|
||||
if (gradeRowA[colKey] < gradeRowB[colKey]) {
|
||||
return -1;
|
||||
}
|
||||
if (gradeRowA[colKey] > gradeRowB[colKey]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortNumDesc(gradeRowA, gradeRowB) {
|
||||
if (gradeRowA[colKey] < gradeRowB[colKey]) {
|
||||
return 1;
|
||||
}
|
||||
if (gradeRowA[colKey] > gradeRowB[colKey]) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.setState({ grades: [...this.state.grades].sort(direction === 'desc' ? sortNumDesc : sortNumAsc) });
|
||||
};
|
||||
const headingMapper = {
|
||||
all: (entry) => {
|
||||
if (entry) {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => {
|
||||
this.setState({
|
||||
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc),
|
||||
});
|
||||
},
|
||||
}];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label)
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { this.sortNumerically(s.label, direction); },
|
||||
}));
|
||||
|
||||
const totals = [{
|
||||
label: 'Total',
|
||||
key: 'total',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { this.sortNumerically('total', direction); },
|
||||
}];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
hw: (entry) => {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => {
|
||||
this.setState({
|
||||
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc),
|
||||
});
|
||||
},
|
||||
}];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label && section.category == 'Homework')
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: false,
|
||||
onSort: (direction) => { this.sortNumerically(s.label, direction); },
|
||||
}));
|
||||
|
||||
return results.concat(assignmentHeadings);
|
||||
},
|
||||
exam: (entry) => {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: false,
|
||||
onSort: (direction) => {
|
||||
this.setState({
|
||||
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc),
|
||||
});
|
||||
},
|
||||
}];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label && section.category == 'Exam')
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: false,
|
||||
onSort: (direction) => { this.sortNumerically(s.label, direction); },
|
||||
}));
|
||||
|
||||
return results.concat(assignmentHeadings);
|
||||
},
|
||||
};
|
||||
|
||||
export { headingMapper };
|
||||
@@ -1,6 +1,7 @@
|
||||
import { configuration } from '../config';
|
||||
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
|
||||
|
||||
import { configuration } from '../config';
|
||||
|
||||
const apiClient = getAuthenticatedAPIClient({
|
||||
appBaseUrl: configuration.BASE_URL,
|
||||
loginUrl: configuration.LOGIN_URL,
|
||||
|
||||
9
src/data/constants/actionTypes/cohorts.js
Normal file
9
src/data/constants/actionTypes/cohorts.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const STARTED_FETCHING_COHORTS = 'STARTED_FETCHING_COHORTS';
|
||||
const GOT_COHORTS = 'GOT_COHORTS';
|
||||
const ERROR_FETCHING_COHORTS = 'ERROR_FETCHING_COHORTS';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
};
|
||||
@@ -3,9 +3,25 @@ const FINISHED_FETCHING_GRADES = 'FINISHED_FETCHING_GRADES';
|
||||
const ERROR_FETCHING_GRADES = 'ERROR_FETCHING_GRADES';
|
||||
const GOT_GRADES = 'GOT_GRADES';
|
||||
|
||||
const GRADE_UPDATE_REQUEST = 'GRADE_UPDATE_REQUEST';
|
||||
const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
|
||||
const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
|
||||
|
||||
const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT';
|
||||
const SORT_GRADES = 'SORT_GRADES';
|
||||
const FILTER_COLUMNS = 'FILTER_COLUMNS';
|
||||
const UPDATE_BANNER = 'UPDATE_BANNER';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_GRADES,
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
GRADE_UPDATE_REQUEST,
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
SORT_GRADES,
|
||||
FILTER_COLUMNS,
|
||||
UPDATE_BANNER,
|
||||
};
|
||||
|
||||
9
src/data/constants/actionTypes/tracks.js
Normal file
9
src/data/constants/actionTypes/tracks.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const STARTED_FETCHING_TRACKS = 'STARTED_FETCHING_TRACKS';
|
||||
const GOT_TRACKS = 'GOT_TRACKS';
|
||||
const ERROR_FETCHING_TRACKS = 'ERROR_FETCHING_TRACKS';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
};
|
||||
39
src/data/reducers/cohorts.js
Normal file
39
src/data/reducers/cohorts.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
STARTED_FETCHING_COHORTS,
|
||||
ERROR_FETCHING_COHORTS,
|
||||
GOT_COHORTS,
|
||||
} from '../constants/actionTypes/cohorts';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
|
||||
const cohorts = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_COHORTS:
|
||||
return {
|
||||
...state,
|
||||
results: action.cohorts,
|
||||
errorFetching: false,
|
||||
};
|
||||
case STARTED_FETCHING_COHORTS:
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
};
|
||||
case ERROR_FETCHING_COHORTS:
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default cohorts;
|
||||
|
||||
@@ -2,13 +2,20 @@ import {
|
||||
STARTED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_COLUMNS,
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
UPDATE_BANNER,
|
||||
} from '../constants/actionTypes/grades';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
headings: [],
|
||||
startedFetching: false,
|
||||
finishedFetching: false,
|
||||
errorFetching: false,
|
||||
gradeFormat: 'percent',
|
||||
showSuccess: false,
|
||||
};
|
||||
|
||||
const grades = (state = initialState, action) => {
|
||||
@@ -17,8 +24,11 @@ const grades = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
results: action.grades,
|
||||
headings: action.headings,
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
selectedTrack: action.track,
|
||||
selectedCohort: action.cohort,
|
||||
};
|
||||
case STARTED_FETCHING_GRADES:
|
||||
return {
|
||||
@@ -32,6 +42,21 @@ const grades = (state = initialState, action) => {
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
case TOGGLE_GRADE_FORMAT:
|
||||
return {
|
||||
...state,
|
||||
gradeFormat: action.formatType,
|
||||
};
|
||||
case FILTER_COLUMNS:
|
||||
return {
|
||||
...state,
|
||||
headings: action.headings,
|
||||
};
|
||||
case UPDATE_BANNER:
|
||||
return {
|
||||
...state,
|
||||
showSuccess: action.showSuccess,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import cohorts from './cohorts';
|
||||
import grades from './grades';
|
||||
import tracks from './tracks';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
grades,
|
||||
cohorts,
|
||||
tracks,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
|
||||
39
src/data/reducers/tracks.js
Normal file
39
src/data/reducers/tracks.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
|
||||
const initialState = {
|
||||
results: [],
|
||||
startedFetching: false,
|
||||
errorFetching: false,
|
||||
};
|
||||
|
||||
|
||||
const tracks = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_TRACKS:
|
||||
return {
|
||||
...state,
|
||||
results: action.tracks,
|
||||
errorFetching: false,
|
||||
};
|
||||
case STARTED_FETCHING_TRACKS:
|
||||
return {
|
||||
...state,
|
||||
startedFetching: true,
|
||||
};
|
||||
case ERROR_FETCHING_TRACKS:
|
||||
return {
|
||||
...state,
|
||||
finishedFetching: true,
|
||||
errorFetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default tracks;
|
||||
|
||||
@@ -4,11 +4,56 @@ import { configuration } from '../../config';
|
||||
class LmsApiService {
|
||||
static baseUrl = configuration.LMS_BASE_URL;
|
||||
|
||||
static fetchGradebookData(courseId) {
|
||||
const fixedCourseId = 'course-v1:edX+DemoX+Demo_Course'; // TODO: get rid of this in favor of courseId
|
||||
const gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${fixedCourseId}/`;
|
||||
static fetchGradebookData(courseId, searchText, cohort, track) {
|
||||
let gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/`;
|
||||
if (searchText || track || cohort) {
|
||||
gradebookUrl += '?';
|
||||
}
|
||||
if (searchText) {
|
||||
gradebookUrl += `username_contains=${searchText}&`;
|
||||
}
|
||||
if (cohort) {
|
||||
gradebookUrl += `cohort_id=${cohort}&`;
|
||||
}
|
||||
if (track) {
|
||||
gradebookUrl += `enrollment_mode=${track}`;
|
||||
}
|
||||
return apiClient.get(gradebookUrl);
|
||||
}
|
||||
|
||||
static updateGradebookData(courseId, updateData) {
|
||||
/*
|
||||
updateData is expected to be a list of objects with the keys 'user_id' (an integer),
|
||||
'usage_id' (a string) and 'grade', which is an object with the keys:
|
||||
'earned_all_override', 'possible_all_override', 'earned_graded_override', and 'possible_graded_override',
|
||||
each of which should be an integer.
|
||||
Example:
|
||||
[
|
||||
{
|
||||
"user_id": 9,
|
||||
"usage_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
|
||||
"grade": {
|
||||
"earned_all_override": 11,
|
||||
"possible_all_override": 11,
|
||||
"earned_graded_override": 11,
|
||||
"possible_graded_override": 11
|
||||
}
|
||||
}
|
||||
]
|
||||
*/
|
||||
const gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/bulk-update`;
|
||||
return apiClient.post(gradebookUrl, updateData);
|
||||
}
|
||||
|
||||
static fetchTracks(courseId) {
|
||||
const trackUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/course/${courseId}`;
|
||||
return apiClient.get(trackUrl);
|
||||
}
|
||||
|
||||
static fetchCohorts(courseId) {
|
||||
const cohortsUrl = `${LmsApiService.baseUrl}/courses/${courseId}/cohorts/`;
|
||||
return apiClient.get(cohortsUrl);
|
||||
}
|
||||
}
|
||||
|
||||
export default LmsApiService;
|
||||
|
||||
@@ -21,32 +21,3 @@ const App = () => (
|
||||
);
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
|
||||
// const App = () => (
|
||||
// <Provider store={store}>
|
||||
// <Router>
|
||||
// <div>
|
||||
// <header>
|
||||
// <nav>
|
||||
// <ul className="nav">
|
||||
// <li className="nav-item"><Link className="nav-link" to="/">Home</Link></li>
|
||||
// <li className="nav-item"><Link className="nav-link" to="/posts">Posts</Link></li>
|
||||
// <li className="nav-item"><Link className="nav-link" to="/disclosure">Disclosure</Link></li>
|
||||
// <li className="nav-item"><Link className="nav-link" to="/comment-search">Comment Search</Link></li>
|
||||
// </ul>
|
||||
// </nav>
|
||||
// </header>
|
||||
// <main>
|
||||
// <Switch>
|
||||
// <Route exact path="/" component={() => <span>Hello World</span>} />
|
||||
// <Route path="/posts" component={PostsPage} />
|
||||
// <Route path="/disclosure" component={DisclosurePage} />
|
||||
// <Route path="/comment-search" component={CommentSearchPage} />
|
||||
// </Switch>
|
||||
// </main>
|
||||
// </div>
|
||||
// </Router>
|
||||
// </Provider>
|
||||
// );
|
||||
|
||||
// ReactDOM.render(<App />, document.getElementById('root'));
|
||||
Reference in New Issue
Block a user