Compare commits

...

15 Commits

Author SHA1 Message Date
Simon Chen
ec81eb47d9 Merge pull request #18 from edx/aed/edit-modal
feat(editing): display a status alert after grades have updated.
2018-11-14 21:03:12 -05:00
Alex Dusenbery
cd2a5ae903 feat(editing): display a status alert after grades have updated. 2018-11-14 20:36:41 -05:00
Richard I Reilly
7a02330e9e Merge pull request #17 from edx/rir/frontend-auth-1-1
Return the frontend-auth npm package to 1.1.0
2018-11-14 16:45:51 -05:00
Rick Reilly
a929194a29 Return the frontend-auth npm package to 1.1.0 2018-11-14 16:16:47 -05:00
Simon Chen
febf4d99c6 Merge pull request #14 from edx/schen/add_links
fix(functionality): Add links and subtitles to the UI
2018-11-14 15:52:47 -05:00
Simon Chen
83ed8ab875 Merge pull request #16 from edx/schen/default-wording
fix(ui): update the default wording of the group filters
2018-11-14 15:51:10 -05:00
Simon Chen
6563f54590 fix(functionality): Add links and subtitles to the UI so it helps user navigate 2018-11-14 15:43:48 -05:00
Simon Chen
e1fe31dc94 fix(ui): update the default wording of the group filters 2018-11-14 15:39:56 -05:00
Richard I Reilly
8754263584 Merge pull request #15 from edx/rir/fix-category-filter
Rir/fix category filter
2018-11-14 15:35:34 -05:00
Rick Reilly
c660bd8d15 Fix the category filter 2018-11-14 15:16:40 -05:00
Richard I Reilly
df32123f34 Merge pull request #8 from edx/rir/fix-percent-absolute-filter
Rir/fix percent absolute filter
2018-11-14 14:50:40 -05:00
Rick Reilly
e81db01be2 Fix percent vs absolute radio buttons 2018-11-14 14:32:34 -05:00
Simon Chen
05c3468d93 Merge pull request #12 from edx/schen/fix_download_link
fix(functionality): Update the download report link URL
2018-11-14 14:18:39 -05:00
Simon Chen
1a88343be9 fix(functionality): Update the download report link URL so it goes to the right place 2018-11-14 13:55:12 -05:00
Alex Dusenbery
6f752f3a18 fix(auth): Pin frontend-auth to 1.1.0 for now. 2018-11-14 13:35:09 -05:00
10 changed files with 362 additions and 282 deletions

66
package-lock.json generated
View File

@@ -2965,20 +2965,18 @@
}
},
"@edx/frontend-auth": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-1.2.0.tgz",
"integrity": "sha512-acQ+rxDYN84z0gUBkcRhUQO+guMMcRSei/PxYvr9j/aCpa8GoelL/K+TKyFuSfEuUcCuzmfUnJHpU6JtulzQcw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-1.1.0.tgz",
"integrity": "sha512-Pl6CUgwRPHcKx3REX1CXJIGl/WqSCg20IPGNUbpsyQ4ZuIaOi/bxI6dYfGhmOWt8LPLeBiEmGerofEWLNydZ9w==",
"requires": {
"axios": "^0.18.0",
"jwt-decode": "^2.2.0",
"prop-types": "^15.5.10",
"pubsub-js": "^1.7.0",
"react": "^16.4.2",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"redux": "^4.0.0",
"universal-cookie": "^3.0.4",
"url-parse": "^1.4.3"
"universal-cookie": "^3.0.4"
},
"dependencies": {
"invariant": {
@@ -2989,11 +2987,6 @@
"loose-envify": "^1.0.0"
}
},
"querystringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.0.tgz",
"integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg=="
},
"react": {
"version": "16.6.3",
"resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz",
@@ -3071,15 +3064,6 @@
"object-assign": "^4.1.1"
}
},
"url-parse": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.4.tgz",
"integrity": "sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==",
"requires": {
"querystringify": "^2.0.0",
"requires-port": "^1.0.0"
}
},
"warning": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
@@ -3091,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",
@@ -3128,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": {
@@ -3148,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"
}
}
}
},
@@ -3561,7 +3554,7 @@
},
"@types/object-assign": {
"version": "4.0.30",
"resolved": "http://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI="
},
"@types/tapable": {
@@ -19181,11 +19174,6 @@
"randombytes": "^2.0.1"
}
},
"pubsub-js": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.7.0.tgz",
"integrity": "sha512-Pb68P9qFZxnvDipHMuj9oT1FoIgBcXJ9C9eWdHCLZAnulaUoJ3+Y87RhGMYilWpun6DMWVmvK70T4RP4drZMSA=="
},
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
@@ -20123,7 +20111,8 @@
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"resolve": {
"version": "1.5.0",
@@ -20525,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",

View File

@@ -25,8 +25,8 @@
},
"dependencies": {
"@edx/edx-bootstrap": "^0.4.3",
"@edx/frontend-auth": "^1.2.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",

View File

@@ -1,3 +1,6 @@
.back-link{
float:right;
}
.student-filters{
display: flex;
.label{

View File

@@ -1,9 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import emailPropType from 'email-prop-type';
import { Button, Modal, SearchField, Table, InputSelect } 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) {
@@ -32,134 +30,6 @@ export default class Gradebook extends React.Component {
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) });
}
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),
});
},
}];
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 [];
};
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);
};
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);
};
setNewModalState = (userEntry, subsection) => {
this.setState({
modalModel: [{
@@ -170,7 +40,7 @@ export default class Gradebook extends React.Component {
<input
style={{ width: '25px' }}
type="text"
onChange={(event) => this.setState({updateVal: event.target.value})}
onChange={event => this.setState({ updateVal: event.target.value })}
/> / {subsection.score_possible}
</span>
),
@@ -179,58 +49,28 @@ export default class Gradebook extends React.Component {
modalOpen: true,
updateModuleId: subsection.module_id,
updateUserId: userEntry.user_id,
})
});
}
mapUserEntriesPercent = 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);
});
mapUserEntriesAbsolute = 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);
});
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,
user_id: this.state.updateUserId,
usage_id: this.state.updateModuleId,
grade: {
earned_graded_override: this.state.updateVal,
},
},
]);
this.setState({
modalModel: [{}],
modalOpen: false,
updateModuleId: null,
updateUserId: null,
});
}
updateQueryParams = (queryKey, queryValue) => {
const parsed = queryString.parse(this.props.location.search);
parsed[queryKey] = queryValue;
@@ -238,27 +78,27 @@ export default class Gradebook extends React.Component {
};
mapCohortsEntries = (entries) => {
let mapped = entries.map(entry => ({
const mapped = entries.map(entry => ({
id: entry.id,
label: entry.name,
}));
mapped.unshift({id:0, label:'Cohorts'});
mapped.unshift({ id: 0, label: 'Cohort-All' });
return mapped;
};
mapTracksEntries = (entries) => {
let mapped = entries.map(entry => ({
const mapped = entries.map(entry => ({
id: entry.slug,
label: entry.name,
}));
mapped.unshift({ label:'Tracks' });
mapped.unshift({ label: 'Track-All' });
return mapped;
};
updateTracks = (event) => {
const selectedTrackItem = this.props.tracks.find(x=>x.name===event);
const selectedTrackItem = this.props.tracks.find(x => x.name === event);
let selectedTrackSlug = null;
if(selectedTrackItem) {
if (selectedTrackItem) {
selectedTrackSlug = selectedTrackItem.slug;
}
this.props.getUserGrades(
@@ -266,14 +106,14 @@ export default class Gradebook extends React.Component {
this.props.selectedCohort,
selectedTrackSlug,
);
const updatedQueryStrings = this.updateQueryParams('track', selectedTrackSlug)
const updatedQueryStrings = this.updateQueryParams('track', selectedTrackSlug);
this.props.history.push(updatedQueryStrings);
};
updateCohorts = (event) => {
const selectedCohortItem = this.props.cohorts.find(x=>x.name===event);
const selectedCohortItem = this.props.cohorts.find(x => x.name === event);
let selectedCohortId = null;
if(selectedCohortItem) {
if (selectedCohortItem) {
selectedCohortId = selectedCohortItem.id;
}
this.props.getUserGrades(
@@ -281,7 +121,7 @@ export default class Gradebook extends React.Component {
selectedCohortId,
this.props.selectedTrack,
);
const updatedQueryStrings = this.updateQueryParams('cohort', selectedCohortId)
const updatedQueryStrings = this.updateQueryParams('cohort', selectedCohortId);
this.props.history.push(updatedQueryStrings);
};
@@ -301,12 +141,60 @@ export default class Gradebook extends React.Component {
return 'Tracks';
};
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);
}),
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>
@@ -315,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.mapUserEntriesPercent(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
@@ -329,50 +217,48 @@ export default class Gradebook extends React.Component {
type="radio"
name="score-view"
value="absolute"
onClick={() => this.setState({ grades: this.mapUserEntriesAbsolute(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>
@@ -384,7 +270,7 @@ export default class Gradebook extends React.Component {
</span>
{this.props.tracks.length > 0 &&
<InputSelect
name='Tracks'
name="Tracks"
value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
options={this.mapTracksEntries(this.props.tracks)}
onChange={this.updateTracks}
@@ -392,7 +278,7 @@ export default class Gradebook extends React.Component {
}
{this.props.cohorts.length > 0 &&
<InputSelect
name='Cohorts'
name="Cohorts"
value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
options={this.mapCohortsEntries(this.props.cohorts)}
onChange={this.updateCohorts}
@@ -403,7 +289,7 @@ export default class Gradebook extends React.Component {
</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={value => this.props.searchForUser(this.props.match.params.courseId, value, this.props.selectedCohort, this.props.selectedTrack)}
@@ -414,11 +300,16 @@ export default class Gradebook extends React.Component {
</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.mapUserEntriesPercent(this.props.grades)}
tableSortable
columns={this.props.headings}
data={this.formatter[this.props.format](this.props.grades)}
defaultSortDirection="desc"
defaultSortedColumn="username"
/>
@@ -430,12 +321,12 @@ export default class Gradebook extends React.Component {
<div>
<h3>{this.state.modalModel[0].assignmentName}</h3>
<Table
columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
data={this.state.modalModel}
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>
)}
buttons={[
@@ -443,7 +334,7 @@ export default class Gradebook extends React.Component {
label="Edit Grade"
buttonType="primary"
onClick={this.handleAdjustedGradeClick}
/>
/>,
]}
onClose={() => this.setState({
modalOpen: false,

View File

@@ -1,17 +1,27 @@
import { connect } from 'react-redux';
import Gradebook from '../../components/Gradebook';
import { fetchGrades, fetchMatchingUserGrades, updateGrades } 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,
}
);
@@ -32,6 +42,15 @@ const mapDispatchToProps = dispatch => (
updateGrades: (courseId, updateData) => {
dispatch(updateGrades(courseId, updateData));
},
toggleFormat: (formatType) => {
dispatch(toggleGradeFormat(formatType));
},
filterColumns: (filterType, exampleUser) => {
dispatch(filterColumns(filterType, exampleUser));
},
updateBanner: (showSuccess) => {
dispatch(updateBanner(showSuccess));
},
}
);

View File

@@ -6,39 +6,57 @@ import {
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, cohort, track) => ({
const gotGrades = (grades, cohort, track, headings) => ({
type: GOT_GRADES,
grades,
cohort,
track,
headings,
});
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
const gradeUpdateSuccess = responseData => ({
const gradeUpdateSuccess = (responseData) => ({
type: GRADE_UPDATE_SUCCESS,
payload: { responseData },
})
});
const gradeUpdateFailure = error => ({
type: GRADE_UPDATE_FAILURE,
payload: { error },
});
const fetchGrades = (courseId, cohort, track) => (
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, null, cohort, track)
.then(response => response.data)
.then((data) => {
dispatch(gotGrades(data.results, cohort, track));
dispatch(gotGrades(data.results, cohort, track, headingMapper.all(data.results[0])));
dispatch(finishedFetchingGrades());
dispatch(updateBanner(!!showSuccess));
})
.catch((error) => {
.catch(() => {
dispatch(errorFetchingGrades());
});
}
@@ -53,7 +71,7 @@ const fetchMatchingUserGrades = (courseId, searchText, cohort, track) => (
dispatch(gotGrades(data.results, cohort, track));
dispatch(finishedFetchingGrades());
})
.catch((error) => {
.catch(() => {
dispatch(errorFetchingGrades());
});
}
@@ -66,6 +84,7 @@ const updateGrades = (courseId, updateData) => (
.then(response => response.data)
.then((data) => {
dispatch(gradeUpdateSuccess(data))
dispatch(fetchGrades(courseId, null, null, true))
})
.catch((error) => {
dispatch(gradeUpdateFailure(error));
@@ -84,4 +103,8 @@ export {
gradeUpdateSuccess,
gradeUpdateFailure,
updateGrades,
toggleGradeFormat,
sortGrades,
filterColumns,
updateBanner,
};

131
src/data/actions/utils.js Normal file
View 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 };

View File

@@ -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,

View File

@@ -7,6 +7,11 @@ 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,
@@ -15,4 +20,8 @@ export {
GRADE_UPDATE_REQUEST,
GRADE_UPDATE_SUCCESS,
GRADE_UPDATE_FAILURE,
TOGGLE_GRADE_FORMAT,
SORT_GRADES,
FILTER_COLUMNS,
UPDATE_BANNER,
};

View File

@@ -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,6 +24,7 @@ const grades = (state = initialState, action) => {
return {
...state,
results: action.grades,
headings: action.headings,
finishedFetching: true,
errorFetching: false,
selectedTrack: action.track,
@@ -34,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;
}