Compare commits

...

12 Commits

Author SHA1 Message Date
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
Alex Dusenbery
7bbc9a84dc fix(auth): use frontend-auth 1.2.0 (allows us to POST without CSRF violation). 2018-11-14 12:12:13 -05:00
10 changed files with 332 additions and 249 deletions

44
package-lock.json generated
View File

@@ -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",
@@ -3089,7 +3098,7 @@
"dependencies": {
"@edx/edx-bootstrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@edx/edx-bootstrap/-/edx-bootstrap-1.0.0.tgz",
"resolved": "http://registry.npmjs.org/@edx/edx-bootstrap/-/edx-bootstrap-1.0.0.tgz",
"integrity": "sha512-ZVoGAqWo9NtPKoNRgOgiW+Qr83TyZ+CWiKFpTmqaG3fm0qBgysnYYRooh1pyaJPedgFw2ljGrAAHYpn94R3pfw==",
"requires": {
"bootstrap": "4.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"
}
}
}
},
@@ -3536,7 +3554,7 @@
},
"@types/object-assign": {
"version": "4.0.30",
"resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"resolved": "http://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI="
},
"@types/tapable": {

View File

@@ -26,7 +26,7 @@
"dependencies": {
"@edx/edx-bootstrap": "^0.4.3",
"@edx/frontend-auth": "^1.1.0",
"@edx/paragon": "^3.7.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 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,54 +49,16 @@ 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,
},
},
]);
@@ -238,27 +70,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 +98,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 +113,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 +133,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 +195,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 +209,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 +262,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 +270,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 +281,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)}
@@ -416,9 +294,8 @@ export default class Gradebook extends React.Component {
<br />
<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 +307,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 +320,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,25 @@
import { connect } from 'react-redux';
import Gradebook from '../../components/Gradebook';
import { fetchGrades, fetchMatchingUserGrades, updateGrades } from '../../data/actions/grades';
import {
fetchGrades,
fetchMatchingUserGrades,
updateGrades,
toggleGradeFormat,
filterColumns,
} 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,
}
);
@@ -32,6 +40,12 @@ const mapDispatchToProps = dispatch => (
updateGrades: (courseId, updateData) => {
dispatch(updateGrades(courseId, updateData));
},
toggleFormat: (formatType) => {
dispatch(toggleGradeFormat(formatType));
},
filterColumns: (filterType, exampleUser) => {
dispatch(filterColumns(filterType, exampleUser));
},
}
);

View File

@@ -6,39 +6,53 @@ import {
GRADE_UPDATE_REQUEST,
GRADE_UPDATE_SUCCESS,
GRADE_UPDATE_FAILURE,
TOGGLE_GRADE_FORMAT,
SORT_GRADES,
FILTER_COLUMNS,
} 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 => ({
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 fetchGrades = (courseId, cohort, track) => (
(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());
})
.catch((error) => {
.catch(() => {
dispatch(errorFetchingGrades());
});
}
@@ -53,7 +67,7 @@ const fetchMatchingUserGrades = (courseId, searchText, cohort, track) => (
dispatch(gotGrades(data.results, cohort, track));
dispatch(finishedFetchingGrades());
})
.catch((error) => {
.catch(() => {
dispatch(errorFetchingGrades());
});
}
@@ -65,7 +79,7 @@ const updateGrades = (courseId, updateData) => (
return LmsApiService.updateGradebookData(courseId, updateData)
.then(response => response.data)
.then((data) => {
dispatch(gradeUpdateSuccess(data))
dispatch(gradeUpdateSuccess(data));
})
.catch((error) => {
dispatch(gradeUpdateFailure(error));
@@ -84,4 +98,7 @@ export {
gradeUpdateSuccess,
gradeUpdateFailure,
updateGrades,
toggleGradeFormat,
sortGrades,
filterColumns,
};

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,10 @@ 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';
export {
STARTED_FETCHING_GRADES,
FINISHED_FETCHING_GRADES,
@@ -15,4 +19,7 @@ export {
GRADE_UPDATE_REQUEST,
GRADE_UPDATE_SUCCESS,
GRADE_UPDATE_FAILURE,
TOGGLE_GRADE_FORMAT,
SORT_GRADES,
FILTER_COLUMNS,
};

View File

@@ -2,13 +2,17 @@ import {
STARTED_FETCHING_GRADES,
ERROR_FETCHING_GRADES,
GOT_GRADES,
TOGGLE_GRADE_FORMAT,
FILTER_COLUMNS,
} from '../constants/actionTypes/grades';
const initialState = {
results: [],
headings: [],
startedFetching: false,
finishedFetching: false,
errorFetching: false,
gradeFormat: 'percent',
};
const grades = (state = initialState, action) => {
@@ -17,6 +21,7 @@ const grades = (state = initialState, action) => {
return {
...state,
results: action.grades,
headings: action.headings,
finishedFetching: true,
errorFetching: false,
selectedTrack: action.track,
@@ -34,6 +39,16 @@ 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,
};
default:
return state;
}