Compare commits

..

14 Commits

Author SHA1 Message Date
Alex Dusenbery
f0aada7794 Get ws communication working with lms 2019-03-25 17:08:52 -04:00
Richard I Reilly
09e482e893 Merge pull request #87 from edx/rir/remove-sorting
Remove remove all unneeded sorting
2019-02-04 10:45:29 -05:00
Rick Reilly
d4421d47fc Remove remove all unneeded sorting 2019-02-04 10:39:49 -05:00
Kyle McCormick
0ef8e773cc Merge pull request #85 from edx/kdmccormick/page-size
EDUCATOR-3936 Increase users per page from 10 to 25
2019-01-25 11:46:38 -05:00
Kyle McCormick
1dac20b866 EDUCATOR-3936 Increase users per page from 10 to 25 2019-01-25 11:39:38 -05:00
Zachary Hancock
ed2d715ce0 Merge pull request #84 from edx/zhancock/assignment-type-filter
persist assignment type filter
2019-01-24 16:47:22 -05:00
Zach Hancock
c82c49ea59 persist assignment type filter 2019-01-24 16:42:57 -05:00
Richard I Reilly
a9f8aec5f9 Merge pull request #86 from edx/rir/search-affordance
Cosmetic changes to give search more affordance
2019-01-24 16:20:52 -05:00
Rick Reilly
a63e9a5347 Cosmetic changes to give search more affordance 2019-01-24 16:06:53 -05:00
Richard I Reilly
2581812118 Merge pull request #82 from edx/rir/cleanup
Remove the 'is_graded' filter. The api will ensure all subsection gra…
2019-01-23 14:23:55 -05:00
Rick Reilly
c4fe803a95 Remove the 'is_graded' filter. The api will ensure all subsection grades we get are 'is_graded=true' 2019-01-23 13:34:50 -05:00
Richard I Reilly
93be5329ca Merge pull request #79 from edx/rir/lint
fix(lint): Fix all eslint issues and prop validation
2019-01-23 12:21:33 -05:00
Rick Reilly
80ba7e7152 fix(lint): Fix all eslint issues and prop validation 2019-01-23 12:18:32 -05:00
Alex Dusenbery
f88526aa3a Include expired course modes when fetching data from course enrollment API. 2019-01-23 10:16:07 -05:00
25 changed files with 416 additions and 245 deletions

View File

@@ -1,4 +1,5 @@
coverage/* coverage/*
dist/ dist/
node_modules/ node_modules/
src/postcss.config.js
src/segment.js src/segment.js

View File

@@ -23,6 +23,7 @@ before_script: greenkeeper-lockfile-update
after_script: greenkeeper-lockfile-upload after_script: greenkeeper-lockfile-upload
script: script:
- make validate-no-uncommitted-package-lock-changes - make validate-no-uncommitted-package-lock-changes
- npm run lint
- npm run test - npm run test
- npm run build - npm run build
after_success: after_success:

View File

@@ -49,6 +49,13 @@ in which you'd like to enable the gradebook. Add a course override flag using a
``grades.writable_gradebook``. Make sure to check the ``enabled`` box. Alternatively, you could add this as a ``grades.writable_gradebook``. Make sure to check the ``enabled`` box. Alternatively, you could add this as a
regular waffle flag to enable the gradebook for all courses. regular waffle flag to enable the gradebook for all courses.
## Running tests
1. Assuming that you're operating in the context of the edX devstack,
run `gradebook-shell` from your devstack directory. This will start a bash shell inside your
running gradebook container.
2. Run `make test` (which executes `npm run test`). This will run all of the gradebook tests.
## Directory Structure ## Directory Structure
* `config` * `config`

View File

@@ -47,7 +47,7 @@ module.exports = Merge.smart(commonConfig, {
minimize: true, minimize: true,
}, },
}, },
'postcss-loader', 'postcss-loader', // for autoprefixing, needs to be before the sass loader, not sure why
{ {
loader: 'sass-loader', // compiles Sass to CSS loader: 'sass-loader', // compiles Sass to CSS
options: { options: {

View File

@@ -81,3 +81,6 @@
} }
} }
.mb-85 {
margin-bottom: 85px;
}

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { import {
Button, Button,
InputSelect, InputSelect,
@@ -24,12 +25,41 @@ export default class Gradebook extends React.Component {
updateVal: 0, updateVal: 0,
updateModuleId: null, updateModuleId: null,
updateUserId: null, updateUserId: null,
socket: null,
websocketMsg: {
visible: false,
},
}; };
} }
componentDidMount() { componentDidMount() {
const urlQuery = queryString.parse(this.props.location.search); const urlQuery = queryString.parse(this.props.location.search);
this.props.getRoles(this.props.match.params.courseId, urlQuery); this.props.getRoles(this.props.match.params.courseId, urlQuery);
const socket = new WebSocket('ws://localhost:8765/ws/gradebook/course-v1:edX+DemoX+Demo_Course/');
socket.onmessage = this.socketMessageFunction;
}
socketMessageFunction = (event) => {
var data = JSON.parse(event.data);
console.log(data);
const userIndex = this.props.grades.findIndex((entry) => entry.user_id == data.user_id);
const username = this.props.grades[userIndex].username;
const subsectionIndex = this.props.grades[userIndex].section_breakdown.findIndex((entry) => entry.module_id = data.subsection_id);
const subsectionName = this.props.grades[userIndex].section_breakdown[subsectionIndex].label;
let subsectionGrade = this.props.grades[userIndex].section_breakdown[subsectionIndex];
subsectionGrade.score_earned = data.override.earned_graded_override;
subsectionGrade.score_possible = data.override.possible_graded_override;
const updatedMsg = {
visible: true,
username: username,
subsectionName: subsectionName,
};
this.setState({ websocketMsg: updatedMsg });
this.props.gradeUpdateSuccess(this.props.match.params.courseId, this.props.grades);
} }
setNewModalState = (userEntry, subsection) => { setNewModalState = (userEntry, subsection) => {
@@ -119,6 +149,8 @@ export default class Gradebook extends React.Component {
updateAssignmentTypes = (event) => { updateAssignmentTypes = (event) => {
this.props.filterColumns(event, this.props.grades[0]); this.props.filterColumns(event, this.props.grades[0]);
const updatedQueryStrings = this.updateQueryParams('assignmentType', event);
this.props.history.push(updatedQueryStrings);
} }
updateTracks = (event) => { updateTracks = (event) => {
@@ -131,6 +163,7 @@ export default class Gradebook extends React.Component {
this.props.match.params.courseId, this.props.match.params.courseId,
this.props.selectedCohort, this.props.selectedCohort,
selectedTrackSlug, selectedTrackSlug,
this.props.selectedAssignmentType,
); );
const updatedQueryStrings = this.updateQueryParams('track', selectedTrackSlug); const updatedQueryStrings = this.updateQueryParams('track', selectedTrackSlug);
this.props.history.push(updatedQueryStrings); this.props.history.push(updatedQueryStrings);
@@ -146,18 +179,9 @@ export default class Gradebook extends React.Component {
this.props.match.params.courseId, this.props.match.params.courseId,
selectedCohortId, selectedCohortId,
this.props.selectedTrack, this.props.selectedTrack,
this.props.selectedAssignmentType,
); );
const updatedQueryStrings = this.updateQueryParams('cohort', selectedCohortId); this.updateQueryParams('cohort', selectedCohortId);
this.props.history.push(updatedQueryStrings);
};
mapSelectedAssignmentTypeEntry = (entry) => {
const selectedAssignmentTypeEntry = this.props.assignmentTypes
.find(x => x.id === parseInt(entry, 10));
if (selectedAssignmentTypeEntry) {
return selectedAssignmentTypeEntry.name;
}
return 'All';
}; };
mapSelectedCohortEntry = (entry) => { mapSelectedCohortEntry = (entry) => {
@@ -182,7 +206,6 @@ export default class Gradebook extends React.Component {
percent: (entries, areGradesFrozen) => entries.map((entry) => { percent: (entries, areGradesFrozen) => entries.map((entry) => {
const results = { username: entry.username }; const results = { username: entry.username };
const assignments = entry.section_breakdown const assignments = entry.section_breakdown
.filter(section => section.is_graded)
.reduce((acc, subsection) => { .reduce((acc, subsection) => {
if (areGradesFrozen) { if (areGradesFrozen) {
acc[subsection.label] = `${this.roundGrade(subsection.percent * 100)} %`; acc[subsection.label] = `${this.roundGrade(subsection.percent * 100)} %`;
@@ -204,7 +227,6 @@ export default class Gradebook extends React.Component {
absolute: (entries, areGradesFrozen) => entries.map((entry) => { absolute: (entries, areGradesFrozen) => entries.map((entry) => {
const results = { username: entry.username }; const results = { username: entry.username };
const assignments = entry.section_breakdown const assignments = entry.section_breakdown
.filter(section => section.is_graded)
.reduce((acc, subsection) => { .reduce((acc, subsection) => {
const scoreEarned = this.roundGrade(subsection.score_earned); const scoreEarned = this.roundGrade(subsection.score_earned);
const scorePossible = this.roundGrade(subsection.score_possible); const scorePossible = this.roundGrade(subsection.score_possible);
@@ -264,30 +286,34 @@ export default class Gradebook extends React.Component {
<div role="radiogroup" aria-labelledby="score-view-group-label"> <div role="radiogroup" aria-labelledby="score-view-group-label">
<span id="score-view-group-label">Score View:</span> <span id="score-view-group-label">Score View:</span>
<span> <span>
<input <label className="mr-2" htmlFor="score-view-percent">
id="score-view-percent" <input
className="ml-2 mr-1" id="score-view-percent"
type="radio" className="ml-2 mr-1"
name="score-view" type="radio"
value="percent" name="score-view"
defaultChecked value="percent"
onClick={() => this.props.toggleFormat('percent')} defaultChecked
/> onClick={() => this.props.toggleFormat('percent')}
<label className="mr-2" htmlFor="score-view-percent">Percent</label> />
Percent
</label>
</span> </span>
<span> <span>
<input <label htmlFor="score-view-absolute">
id="score-view-absolute" <input
type="radio" id="score-view-absolute"
name="score-view" type="radio"
value="absolute" name="score-view"
className="mr-1" value="absolute"
onClick={() => this.props.toggleFormat('absolute')} className="mr-1"
/> onClick={() => this.props.toggleFormat('absolute')}
<label htmlFor="score-view-absolute">Absolute</label> />
Absolute
</label>
</span> </span>
</div> </div>
{ this.props.assignmnetTypes.length > 0 && { this.props.assignmentTypes.length > 0 &&
<div className="student-filters"> <div className="student-filters">
<span className="label"> <span className="label">
Assignment Types: Assignment Types:
@@ -295,8 +321,8 @@ export default class Gradebook extends React.Component {
<InputSelect <InputSelect
name="assignment-types" name="assignment-types"
ariaLabel="Assignment Types" ariaLabel="Assignment Types"
value={this.mapSelectedTrackEntry(this.props.selectedAssignmentType)} value={this.props.selectedAssignmentType}
options={this.mapAssignmentTypeEntries(this.props.assignmnetTypes)} options={this.mapAssignmentTypeEntries(this.props.assignmentTypes)}
onChange={this.updateAssignmentTypes} onChange={this.updateAssignmentTypes}
/> />
</div> </div>
@@ -325,12 +351,28 @@ export default class Gradebook extends React.Component {
</div> </div>
<div> <div>
<div style={{ marginLeft: '10px', marginBottom: '10px' }}> <div style={{ marginLeft: '10px', marginBottom: '10px' }}>
<a href={`${this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}#view-data_download`}>Generate Grade Report</a> <a className="btn btn-outline-primary mb-85" href={`${this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}#view-data_download`}>Generate Grade Report</a>
</div> </div>
<SearchField <SearchField
onSubmit={value => this.props.searchForUser(this.props.match.params.courseId, value, this.props.selectedCohort, this.props.selectedTrack)} onSubmit={value =>
this.props.searchForUser(
this.props.match.params.courseId,
value,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
)
}
inputLabel="Search Username:"
onChange={filterValue => this.setState({ filterValue })} onChange={filterValue => this.setState({ filterValue })}
onClear={() => this.props.getUserGrades(this.props.match.params.courseId, this.props.selectedCohort, this.props.selectedTrack)} onClear={() =>
this.props.getUserGrades(
this.props.match.params.courseId,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
)
}
value={this.state.filterValue} value={this.state.filterValue}
/> />
</div> </div>
@@ -342,14 +384,20 @@ export default class Gradebook extends React.Component {
onClose={() => this.props.updateBanner(false)} onClose={() => this.props.updateBanner(false)}
open={this.props.showSuccess} open={this.props.showSuccess}
/> />
<StatusAlert
alertType="success"
dialog={`Grade for user ${this.state.websocketMsg.username} in ${this.state.websocketMsg.subsectionName} was updated.`}
onClose={() => this.setState({ websocketMsg : false })}
open={this.state.websocketMsg.visible}
/>
{PageButtons(this.props)} {PageButtons(this.props)}
<div className="gbook"> <div className="gbook">
<Table <Table
columns={this.props.headings} columns={this.props.headings}
data={this.formatter[this.props.format](this.props.grades, this.props.areGradesFrozen)} data={this.formatter[this.props.format](
tableSortable this.props.grades,
defaultSortDirection="asc" this.props.areGradesFrozen,
defaultSortedColumn="username" )}
rowHeaderColumnKey="username" rowHeaderColumnKey="username"
/> />
</div> </div>
@@ -390,3 +438,78 @@ export default class Gradebook extends React.Component {
} }
} }
Gradebook.defaultProps = {
areGradesFrozen: false,
assignmentTypes: [],
canUserViewGradebook: false,
cohorts: [],
grades: [],
location: {
search: '',
},
match: {
params: {
courseId: '',
},
},
selectedCohort: null,
selectedTrack: null,
selectedAssignmentType: 'All',
showSpinner: false,
tracks: [],
};
Gradebook.propTypes = {
areGradesFrozen: PropTypes.bool,
assignmentTypes: PropTypes.arrayOf(PropTypes.string),
canUserViewGradebook: PropTypes.bool,
cohorts: PropTypes.arrayOf(PropTypes.string),
filterColumns: PropTypes.func.isRequired,
format: PropTypes.string.isRequired,
getRoles: PropTypes.func.isRequired,
getUserGrades: PropTypes.func.isRequired,
grades: PropTypes.arrayOf(PropTypes.shape({
percent: PropTypes.number,
section_breakdown: PropTypes.arrayOf(PropTypes.shape({
attempted: PropTypes.bool,
category: PropTypes.string,
label: PropTypes.string,
module_id: PropTypes.string,
percent: PropTypes.number,
scoreEarned: PropTypes.number,
scorePossible: PropTypes.number,
subsection_name: PropTypes.string,
})),
user_id: PropTypes.number,
user_name: PropTypes.string,
})),
headings: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string,
key: PropTypes.string,
})).isRequired,
history: PropTypes.shape({
push: PropTypes.func,
}).isRequired,
location: PropTypes.shape({
search: PropTypes.string,
}),
match: PropTypes.shape({
params: PropTypes.shape({
courseId: PropTypes.string,
}),
}),
searchForUser: PropTypes.func.isRequired,
selectedAssignmentType: PropTypes.string,
selectedCohort: PropTypes.shape({
name: PropTypes.string,
}),
selectedTrack: PropTypes.string,
showSpinner: PropTypes.bool,
showSuccess: PropTypes.bool.isRequired,
toggleFormat: PropTypes.func.isRequired,
tracks: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
})),
updateBanner: PropTypes.func.isRequired,
updateGrades: PropTypes.func.isRequired,
};

View File

@@ -4,13 +4,6 @@ import { Hyperlink } from '@edx/paragon';
import EdxLogo from '../../../assets/edx-sm.png'; import EdxLogo from '../../../assets/edx-sm.png';
export default class Header extends React.Component { export default class Header extends React.Component {
constructor(props) {
super(props);
this.state = {
mobileNavOpen: false,
};
}
renderLogo() { renderLogo() {
return ( return (
<img src={EdxLogo} alt="edX logo" height="30" width="60" /> <img src={EdxLogo} alt="edX logo" height="30" width="60" />

View File

@@ -10,7 +10,7 @@ exports[`PageButtons prev not null, next not null 1`] = `
} }
> >
<button <button
className="btn btn-primary" className="btn btn-outline-primary"
disabled={false} disabled={false}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@@ -25,7 +25,7 @@ exports[`PageButtons prev not null, next not null 1`] = `
Previous Page Previous Page
</button> </button>
<button <button
className="btn btn-primary" className="btn btn-outline-primary"
disabled={false} disabled={false}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@@ -52,7 +52,7 @@ exports[`PageButtons prev not null, next null 1`] = `
} }
> >
<button <button
className="btn btn-primary" className="btn btn-outline-primary"
disabled={false} disabled={false}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@@ -67,7 +67,7 @@ exports[`PageButtons prev not null, next null 1`] = `
Previous Page Previous Page
</button> </button>
<button <button
className="btn btn-primary" className="btn btn-outline-primary"
disabled={true} disabled={true}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@@ -94,7 +94,7 @@ exports[`PageButtons prev null, next not null 1`] = `
} }
> >
<button <button
className="btn btn-primary" className="btn btn-outline-primary"
disabled={true} disabled={true}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@@ -109,7 +109,7 @@ exports[`PageButtons prev null, next not null 1`] = `
Previous Page Previous Page
</button> </button>
<button <button
className="btn btn-primary" className="btn btn-outline-primary"
disabled={false} disabled={false}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@@ -136,7 +136,7 @@ exports[`PageButtons prev null, next null 1`] = `
} }
> >
<button <button
className="btn btn-primary" className="btn btn-outline-primary"
disabled={true} disabled={true}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@@ -151,7 +151,7 @@ exports[`PageButtons prev null, next null 1`] = `
Previous Page Previous Page
</button> </button>
<button <button
className="btn btn-primary" className="btn btn-outline-primary"
disabled={true} disabled={true}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}

View File

@@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon'; import { Button } from '@edx/paragon';
export default function PageButtons({ export default function PageButtons({
prevPage, nextPage, selectedTrack, selectedCohort, getPrevNextGrades, match, prevPage, nextPage, selectedTrack, selectedCohort, selectedAssignmentType,
getPrevNextGrades, match,
}) { }) {
return ( return (
<div <div
@@ -13,18 +15,63 @@ export default function PageButtons({
<Button <Button
label="Previous Page" label="Previous Page"
style={{ margin: '20px' }} style={{ margin: '20px' }}
buttonType="primary" buttonType="outline-primary"
disabled={!prevPage} disabled={!prevPage}
onClick={() => getPrevNextGrades(prevPage, selectedCohort, selectedTrack, match.params.courseId)} onClick={() =>
getPrevNextGrades(
prevPage,
match.params.courseId,
selectedCohort,
selectedTrack,
selectedAssignmentType,
)}
/> />
<Button <Button
label="Next Page" label="Next Page"
style={{ margin: '20px' }} style={{ margin: '20px' }}
buttonType="primary" buttonType="outline-primary"
disabled={!nextPage} disabled={!nextPage}
onClick={() => getPrevNextGrades(nextPage, selectedCohort, selectedTrack, match.params.courseId)} onClick={() =>
getPrevNextGrades(
nextPage,
match.params.courseId,
selectedCohort,
selectedTrack,
selectedAssignmentType,
)}
/> />
</div> </div>
); );
} }
PageButtons.defaultProps = {
match: {
params: {
courseId: '',
},
},
nextPage: '',
prevPage: '',
selectedCohort: null,
selectedTrack: null,
selectedAssignmentType: null,
};
PageButtons.propTypes = {
getPrevNextGrades: PropTypes.func.isRequired,
match: PropTypes.shape({
params: PropTypes.shape({
courseId: PropTypes.string,
}),
}),
nextPage: PropTypes.string,
prevPage: PropTypes.string,
selectedAssignmentType: PropTypes.string,
selectedCohort: PropTypes.shape({
name: PropTypes.string,
}),
selectedTrack: PropTypes.shape({
name: PropTypes.string,
}),
};

View File

@@ -5,6 +5,7 @@ import {
fetchGrades, fetchGrades,
fetchMatchingUserGrades, fetchMatchingUserGrades,
fetchPrevNextGrades, fetchPrevNextGrades,
gradeUpdateSuccess,
updateGrades, updateGrades,
toggleGradeFormat, toggleGradeFormat,
filterColumns, filterColumns,
@@ -15,25 +16,6 @@ import { fetchTracks } from '../../data/actions/tracks';
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes'; import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
import { getRoles } from '../../data/actions/roles'; import { getRoles } from '../../data/actions/roles';
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,
prevPage: state.grades.prevPage,
nextPage: state.grades.nextPage,
assignmnetTypes: state.assignmentTypes.results,
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
showSpinner: shouldShowSpinner(state),
canUserViewGradebook: state.roles.canUserViewGradebook,
}
);
function shouldShowSpinner(state) { function shouldShowSpinner(state) {
if (state.roles.canUserViewGradebook === true) { if (state.roles.canUserViewGradebook === true) {
return state.grades.showSpinner; return state.grades.showSpinner;
@@ -43,16 +25,36 @@ function shouldShowSpinner(state) {
return true; return true;
} }
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,
selectedAssignmentType: state.grades.selectedAssignmentType,
format: state.grades.gradeFormat,
showSuccess: state.grades.showSuccess,
prevPage: state.grades.prevPage,
nextPage: state.grades.nextPage,
assignmentTypes: state.assignmentTypes.results,
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
showSpinner: shouldShowSpinner(state),
canUserViewGradebook: state.roles.canUserViewGradebook,
}
);
const mapDispatchToProps = dispatch => ( const mapDispatchToProps = dispatch => (
{ {
getUserGrades: (courseId, cohort, track) => { getUserGrades: (courseId, cohort, track, assignmentType) => {
dispatch(fetchGrades(courseId, cohort, track)); dispatch(fetchGrades(courseId, cohort, track, assignmentType));
}, },
searchForUser: (courseId, searchText, cohort, track) => { searchForUser: (courseId, searchText, cohort, track, assignmentType) => {
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, false)); dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, assignmentType, false));
}, },
getPrevNextGrades: (endpoint, cohort, track, courseId) => { getPrevNextGrades: (endpoint, courseId, cohort, track, assignmentType) => {
dispatch(fetchPrevNextGrades(endpoint, cohort, track, courseId)); dispatch(fetchPrevNextGrades(endpoint, courseId, cohort, track, assignmentType));
}, },
getCohorts: (courseId) => { getCohorts: (courseId) => {
dispatch(fetchCohorts(courseId)); dispatch(fetchCohorts(courseId));
@@ -78,6 +80,9 @@ const mapDispatchToProps = dispatch => (
getRoles: (matchParams, urlQuery) => { getRoles: (matchParams, urlQuery) => {
dispatch(getRoles(matchParams, urlQuery)); dispatch(getRoles(matchParams, urlQuery));
}, },
gradeUpdateSuccess: (courseId, data) => {
dispatch(gradeUpdateSuccess(courseId, data));
},
} }
); );

View File

@@ -7,36 +7,24 @@ import {
GRADE_UPDATE_SUCCESS, GRADE_UPDATE_SUCCESS,
GRADE_UPDATE_FAILURE, GRADE_UPDATE_FAILURE,
TOGGLE_GRADE_FORMAT, TOGGLE_GRADE_FORMAT,
SORT_GRADES,
FILTER_COLUMNS, FILTER_COLUMNS,
UPDATE_BANNER, UPDATE_BANNER,
} from '../constants/actionTypes/grades'; } from '../constants/actionTypes/grades';
import LmsApiService from '../services/LmsApiService'; import LmsApiService from '../services/LmsApiService';
import store from '../store'; import { headingMapper, sortAlphaAsc } from './utils';
import { headingMapper, gradeSortMap, sortAlphaAsc } from './utils';
import apiClient from '../apiClient'; import apiClient from '../apiClient';
const defaultAssignmentFilter = 'All'; const defaultAssignmentFilter = 'All';
const sortGrades = (columnName, direction) => {
const sortFn = gradeSortMap(columnName, direction);
const { results } = store.getState().grades;
results.sort(sortFn);
/* have to make a copy of results or React wont know there was
* a change and wont trigger a re-render
*/
return ({ type: SORT_GRADES, results: [...results] });
};
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES }); const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES }); const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES }); const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
const gotGrades = (grades, cohort, track, headings, prev, next, courseId) => ({ const gotGrades = (grades, cohort, track, assignmentType, headings, prev, next, courseId) => ({
type: GOT_GRADES, type: GOT_GRADES,
grades, grades,
cohort, cohort,
track, track,
assignmentType,
headings, headings,
prev, prev,
next, next,
@@ -61,13 +49,13 @@ const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType
const filterColumns = (filterType, exampleUser) => ( const filterColumns = (filterType, exampleUser) => (
dispatch => dispatch({ dispatch => dispatch({
type: FILTER_COLUMNS, type: FILTER_COLUMNS,
headings: headingMapper(filterType)(dispatch, exampleUser), headings: headingMapper(filterType)(exampleUser),
}) })
); );
const updateBanner = showSuccess => ({ type: UPDATE_BANNER, showSuccess }); const updateBanner = showSuccess => ({ type: UPDATE_BANNER, showSuccess });
const fetchGrades = (courseId, cohort, track, showSuccess) => ( const fetchGrades = (courseId, cohort, track, assignmentType, showSuccess) => (
(dispatch) => { (dispatch) => {
dispatch(startedFetchingGrades()); dispatch(startedFetchingGrades());
return LmsApiService.fetchGradebookData(courseId, null, cohort, track) return LmsApiService.fetchGradebookData(courseId, null, cohort, track)
@@ -77,7 +65,8 @@ const fetchGrades = (courseId, cohort, track, showSuccess) => (
data.results.sort(sortAlphaAsc), data.results.sort(sortAlphaAsc),
cohort, cohort,
track, track,
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]), assignmentType,
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
data.previous, data.previous,
data.next, data.next,
courseId, courseId,
@@ -91,7 +80,14 @@ const fetchGrades = (courseId, cohort, track, showSuccess) => (
} }
); );
const fetchMatchingUserGrades = (courseId, searchText, cohort, track, showSuccess) => ( const fetchMatchingUserGrades = (
courseId,
searchText,
cohort,
track,
assignmentType,
showSuccess,
) => (
(dispatch) => { (dispatch) => {
dispatch(startedFetchingGrades()); dispatch(startedFetchingGrades());
return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track) return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track)
@@ -101,7 +97,8 @@ const fetchMatchingUserGrades = (courseId, searchText, cohort, track, showSucces
data.results.sort(sortAlphaAsc), data.results.sort(sortAlphaAsc),
cohort, cohort,
track, track,
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]), assignmentType,
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
data.previous, data.previous,
data.next, data.next,
courseId, courseId,
@@ -115,7 +112,7 @@ const fetchMatchingUserGrades = (courseId, searchText, cohort, track, showSucces
} }
); );
const fetchPrevNextGrades = (endpoint, cohort, track, courseId) => ( const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
(dispatch) => { (dispatch) => {
dispatch(startedFetchingGrades()); dispatch(startedFetchingGrades());
return apiClient.get(endpoint) return apiClient.get(endpoint)
@@ -125,7 +122,8 @@ const fetchPrevNextGrades = (endpoint, cohort, track, courseId) => (
data.results.sort(sortAlphaAsc), data.results.sort(sortAlphaAsc),
cohort, cohort,
track, track,
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]), assignmentType,
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
data.previous, data.previous,
data.next, data.next,
courseId, courseId,
@@ -138,7 +136,6 @@ const fetchPrevNextGrades = (endpoint, cohort, track, courseId) => (
} }
); );
const updateGrades = (courseId, updateData, searchText, cohort, track) => ( const updateGrades = (courseId, updateData, searchText, cohort, track) => (
(dispatch) => { (dispatch) => {
dispatch(gradeUpdateRequest()); dispatch(gradeUpdateRequest());
@@ -146,7 +143,14 @@ const updateGrades = (courseId, updateData, searchText, cohort, track) => (
.then(response => response.data) .then(response => response.data)
.then((data) => { .then((data) => {
dispatch(gradeUpdateSuccess(courseId, data)); dispatch(gradeUpdateSuccess(courseId, data));
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, true)); // dispatch(fetchMatchingUserGrades(
// courseId,
// searchText,
// cohort,
// track,
// defaultAssignmentFilter,
// true,
// ));
}) })
.catch((error) => { .catch((error) => {
dispatch(gradeUpdateFailure(courseId, error)); dispatch(gradeUpdateFailure(courseId, error));
@@ -167,7 +171,6 @@ export {
gradeUpdateFailure, gradeUpdateFailure,
updateGrades, updateGrades,
toggleGradeFormat, toggleGradeFormat,
sortGrades,
filterColumns, filterColumns,
updateBanner, updateBanner,
}; };

View File

@@ -27,7 +27,8 @@ describe('actions', () => {
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const expectedCohort = 1; const expectedCohort = 1;
const expectedTrack = 'verified'; const expectedTrack = 'verified';
const fetchGradesURL = `${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/?page_size=10&cohort_id=${expectedCohort}&enrollment_mode=${expectedTrack}`; const expectedAssignmentType = 'Exam';
const fetchGradesURL = `${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/?page_size=25&cohort_id=${expectedCohort}&enrollment_mode=${expectedTrack}`;
const responseData = { const responseData = {
next: `${fetchGradesURL}&cursor=2344fda`, next: `${fetchGradesURL}&cursor=2344fda`,
previous: null, previous: null,
@@ -94,18 +95,15 @@ describe('actions', () => {
grades: responseData.results.sort(sortAlphaAsc), grades: responseData.results.sort(sortAlphaAsc),
cohort: expectedCohort, cohort: expectedCohort,
track: expectedTrack, track: expectedTrack,
assignmentType: expectedAssignmentType,
headings: [ headings: [
{ {
columnSortable: true,
key: 'username', key: 'username',
label: 'Username', label: 'Username',
onSort: expect.anything(),
}, },
{ {
columnSortable: true,
key: 'total', key: 'total',
label: 'Total', label: 'Total',
onSort: expect.anything(),
}, },
], ],
prev: responseData.previous, prev: responseData.previous,
@@ -120,7 +118,13 @@ describe('actions', () => {
axiosMock.onGet(fetchGradesURL) axiosMock.onGet(fetchGradesURL)
.replyOnce(200, JSON.stringify(responseData)); .replyOnce(200, JSON.stringify(responseData));
return store.dispatch(fetchGrades(courseId, expectedCohort, expectedTrack, false)).then(() => { return store.dispatch(fetchGrades(
courseId,
expectedCohort,
expectedTrack,
expectedAssignmentType,
false,
)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
}); });
@@ -135,7 +139,51 @@ describe('actions', () => {
axiosMock.onGet(fetchGradesURL) axiosMock.onGet(fetchGradesURL)
.replyOnce(500, JSON.stringify({})); .replyOnce(500, JSON.stringify({}));
return store.dispatch(fetchGrades(courseId, expectedCohort, expectedTrack, false)).then(() => { return store.dispatch(fetchGrades(
courseId,
expectedCohort,
expectedTrack,
expectedAssignmentType,
false,
)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it('dispatches success action on empty response after fetching grades', () => {
const emptyResponseData = {
next: responseData.next,
previous: responseData.previous,
results: [],
};
const expectedActions = [
{ type: STARTED_FETCHING_GRADES },
{
type: GOT_GRADES,
grades: [],
cohort: expectedCohort,
track: expectedTrack,
assignmentType: expectedAssignmentType,
headings: [],
prev: responseData.previous,
next: responseData.next,
courseId,
},
{ type: FINISHED_FETCHING_GRADES },
{ type: UPDATE_BANNER, showSuccess: false },
];
const store = mockStore();
axiosMock.onGet(fetchGradesURL)
.replyOnce(200, JSON.stringify(emptyResponseData));
return store.dispatch(fetchGrades(
courseId,
expectedCohort,
expectedTrack,
expectedAssignmentType,
false,
)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
}); });

View File

@@ -26,7 +26,7 @@ const getRoles = (courseId, urlQuery) => (
&& allowedRoles.includes(role.role))); && allowedRoles.includes(role.role)));
dispatch(gotRoles(canUserViewGradebook, courseId)); dispatch(gotRoles(canUserViewGradebook, courseId));
if (canUserViewGradebook) { if (canUserViewGradebook) {
dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track)); dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track, urlQuery.assignmentType));
dispatch(fetchTracks(courseId)); dispatch(fetchTracks(courseId));
dispatch(fetchCohorts(courseId)); dispatch(fetchCohorts(courseId));
dispatch(fetchAssignmentTypes(courseId)); dispatch(fetchAssignmentTypes(courseId));

View File

@@ -57,7 +57,10 @@ describe('actions', () => {
]; ];
const store = mockStore(); const store = mockStore();
axiosMock.onGet(rolesUrl) axiosMock.onGet(rolesUrl)
.replyOnce(200, JSON.stringify(makeRoleListObj([course1StaffRole, course2DummyRole], false))); .replyOnce(
200,
JSON.stringify(makeRoleListObj([course1StaffRole, course2DummyRole], false)),
);
return store.dispatch(getRoles(course1Id, urlParams)).then(() => { return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
@@ -75,7 +78,10 @@ describe('actions', () => {
const store = mockStore(); const store = mockStore();
axiosMock.onGet(rolesUrl) axiosMock.onGet(rolesUrl)
.replyOnce(200, JSON.stringify(makeRoleListObj([course1DummyRole, course2DummyRole], true))); .replyOnce(
200,
JSON.stringify(makeRoleListObj([course1DummyRole, course2DummyRole], true)),
);
return store.dispatch(getRoles(course1Id, urlParams)).then(() => { return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
@@ -91,7 +97,10 @@ describe('actions', () => {
const store = mockStore(); const store = mockStore();
axiosMock.onGet(rolesUrl) axiosMock.onGet(rolesUrl)
.replyOnce(200, JSON.stringify(makeRoleListObj([course1DummyRole, course2StaffRole], false))); .replyOnce(
200,
JSON.stringify(makeRoleListObj([course1DummyRole, course2StaffRole], false)),
);
return store.dispatch(getRoles(course1Id, urlParams)).then(() => { return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
@@ -105,7 +114,10 @@ describe('actions', () => {
const store = mockStore(); const store = mockStore();
axiosMock.onGet(rolesUrl) axiosMock.onGet(rolesUrl)
.replyOnce(200, JSON.stringify(makeRoleListObj([], false))); .replyOnce(
200,
JSON.stringify(makeRoleListObj([], false)),
);
return store.dispatch(getRoles(course1Id, urlParams)).then(() => { return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
@@ -123,7 +135,10 @@ describe('actions', () => {
const store = mockStore(); const store = mockStore();
axiosMock.onGet(rolesUrl) axiosMock.onGet(rolesUrl)
.replyOnce(200, JSON.stringify(makeRoleListObj([], true))); .replyOnce(
200,
JSON.stringify(makeRoleListObj([], true)),
);
return store.dispatch(getRoles(course1Id, urlParams)).then(() => { return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);

View File

@@ -21,6 +21,7 @@ describe('actions', () => {
describe('fetchTracks', () => { describe('fetchTracks', () => {
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const trackUrl = `${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}?include_expired=1`;
it('dispatches success action after fetching tracks', () => { it('dispatches success action after fetching tracks', () => {
const responseData = { const responseData = {
@@ -54,7 +55,7 @@ describe('actions', () => {
]; ];
const store = mockStore(); const store = mockStore();
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}`) axiosMock.onGet(trackUrl)
.replyOnce(200, JSON.stringify(responseData)); .replyOnce(200, JSON.stringify(responseData));
return store.dispatch(fetchTracks(courseId)).then(() => { return store.dispatch(fetchTracks(courseId)).then(() => {
@@ -69,7 +70,7 @@ describe('actions', () => {
]; ];
const store = mockStore(); const store = mockStore();
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}`) axiosMock.onGet(trackUrl)
.replyOnce(500, JSON.stringify({})); .replyOnce(500, JSON.stringify({}));
return store.dispatch(fetchTracks(courseId)).then(() => { return store.dispatch(fetchTracks(courseId)).then(() => {

View File

@@ -1,5 +1,3 @@
import { sortGrades } from './grades';
const sortAlphaAsc = (gradeRowA, gradeRowB) => { const sortAlphaAsc = (gradeRowA, gradeRowB) => {
const a = gradeRowA.username.toUpperCase(); const a = gradeRowA.username.toUpperCase();
const b = gradeRowB.username.toUpperCase(); const b = gradeRowB.username.toUpperCase();
@@ -12,78 +10,24 @@ const sortAlphaAsc = (gradeRowA, gradeRowB) => {
return 0; return 0;
}; };
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 sortNumerically = (colKey, direction) => {
function getPercents(gradeRowA, gradeRowB) {
if (colKey !== 'total') {
return {
a: gradeRowA.section_breakdown.find(x => x.label === colKey).percent,
b: gradeRowB.section_breakdown.find(x => x.label === colKey).percent,
};
}
return {
a: gradeRowA.percent,
b: gradeRowB.percent,
};
}
function sortNumAsc(gradeRowA, gradeRowB) {
const { a, b } = getPercents(gradeRowA, gradeRowB);
return a - b;
}
function sortNumDesc(gradeRowA, gradeRowB) {
const { a, b } = getPercents(gradeRowA, gradeRowB);
return b - a;
}
return direction === 'desc' ? sortNumDesc : sortNumAsc;
};
function gradeSortMap(columnName, direction) {
if (columnName === 'username' && direction === 'desc') {
return sortAlphaDesc;
} else if (columnName === 'username') {
return sortAlphaAsc;
}
return sortNumerically(columnName, direction);
}
const headingMapper = (filterKey) => { const headingMapper = (filterKey) => {
function all(dispatch, entry) { function all(entry) {
if (entry) { if (entry) {
const results = [{ const results = [{
label: 'Username', label: 'Username',
key: 'username', key: 'username',
columnSortable: true,
onSort: (direction) => { dispatch(sortGrades('username', direction)); },
}]; }];
const assignmentHeadings = entry.section_breakdown const assignmentHeadings = entry.section_breakdown
.filter(section => section.is_graded && section.label) .filter(section => section.label)
.map(s => ({ .map(s => ({
label: s.label, label: s.label,
key: s.label, key: s.label,
columnSortable: true,
onSort: direction => dispatch(sortGrades(s.label, direction)),
})); }));
const totals = [{ const totals = [{
label: 'Total', label: 'Total',
key: 'total', key: 'total',
columnSortable: true,
onSort: direction => dispatch(sortGrades('total', direction)),
}]; }];
return results.concat(assignmentHeadings).concat(totals); return results.concat(assignmentHeadings).concat(totals);
@@ -91,28 +35,24 @@ const headingMapper = (filterKey) => {
return []; return [];
} }
function some(dispatch, entry) { function some(entry) {
if (!entry) return [];
const results = [{ const results = [{
label: 'Username', label: 'Username',
key: 'username', key: 'username',
columnSortable: true,
onSort: (direction) => { dispatch(sortGrades('username', direction)); },
}]; }];
const assignmentHeadings = entry.section_breakdown const assignmentHeadings = entry.section_breakdown
.filter(section => section.is_graded && section.label && section.category === filterKey) .filter(section => section.label && section.category === filterKey)
.map(s => ({ .map(s => ({
label: s.label, label: s.label,
key: s.label, key: s.label,
columnSortable: false,
onSort: (direction) => { this.sortNumerically(s.label, direction); },
})); }));
const totals = [{ const totals = [{
label: 'Total', label: 'Total',
key: 'total', key: 'total',
columnSortable: true,
onSort: direction => dispatch(sortGrades('total', direction)),
}]; }];
return results.concat(assignmentHeadings).concat(totals); return results.concat(assignmentHeadings).concat(totals);
@@ -121,5 +61,5 @@ const headingMapper = (filterKey) => {
return filterKey === 'All' ? all : some; return filterKey === 'All' ? all : some;
}; };
export { headingMapper, gradeSortMap, sortAlphaAsc }; export { headingMapper, sortAlphaAsc };

View File

@@ -8,7 +8,6 @@ const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE'; const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT'; const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT';
const SORT_GRADES = 'SORT_GRADES';
const FILTER_COLUMNS = 'FILTER_COLUMNS'; const FILTER_COLUMNS = 'FILTER_COLUMNS';
const UPDATE_BANNER = 'UPDATE_BANNER'; const UPDATE_BANNER = 'UPDATE_BANNER';
@@ -21,7 +20,6 @@ export {
GRADE_UPDATE_SUCCESS, GRADE_UPDATE_SUCCESS,
GRADE_UPDATE_FAILURE, GRADE_UPDATE_FAILURE,
TOGGLE_GRADE_FORMAT, TOGGLE_GRADE_FORMAT,
SORT_GRADES,
FILTER_COLUMNS, FILTER_COLUMNS,
UPDATE_BANNER, UPDATE_BANNER,
}; };

View File

@@ -1,5 +1,5 @@
const GOT_ROLES = 'GOT_ROLES'; const GOT_ROLES = 'GOT_ROLES';
const ERROR_FETCHING_ROLES = 'ERROR_FETCHING_ROLES' const ERROR_FETCHING_ROLES = 'ERROR_FETCHING_ROLES';
export { export {
GOT_ROLES, GOT_ROLES,

View File

@@ -5,7 +5,6 @@ import {
TOGGLE_GRADE_FORMAT, TOGGLE_GRADE_FORMAT,
FILTER_COLUMNS, FILTER_COLUMNS,
UPDATE_BANNER, UPDATE_BANNER,
SORT_GRADES,
} from '../constants/actionTypes/grades'; } from '../constants/actionTypes/grades';
const initialState = { const initialState = {
@@ -32,6 +31,7 @@ const grades = (state = initialState, action) => {
errorFetching: false, errorFetching: false,
selectedTrack: action.track, selectedTrack: action.track,
selectedCohort: action.cohort, selectedCohort: action.cohort,
selectedAssignmentType: action.assignmentType,
prevPage: action.prev, prevPage: action.prev,
nextPage: action.next, nextPage: action.next,
showSpinner: false, showSpinner: false,
@@ -65,11 +65,6 @@ const grades = (state = initialState, action) => {
...state, ...state,
showSuccess: action.showSuccess, showSuccess: action.showSuccess,
}; };
case SORT_GRADES:
return {
...state,
results: action.results,
};
default: default:
return state; return state;
} }

View File

@@ -6,7 +6,6 @@ import {
TOGGLE_GRADE_FORMAT, TOGGLE_GRADE_FORMAT,
FILTER_COLUMNS, FILTER_COLUMNS,
UPDATE_BANNER, UPDATE_BANNER,
SORT_GRADES,
} from '../constants/actionTypes/grades'; } from '../constants/actionTypes/grades';
const initialState = { const initialState = {
@@ -164,17 +163,6 @@ describe('grades reducer', () => {
})).toEqual(expected); })).toEqual(expected);
}); });
it('updates sort grades state success', () => {
const expected = {
...initialState,
results: gradesData,
};
expect(grades(undefined, {
type: SORT_GRADES,
results: gradesData,
})).toEqual(expected);
});
it('updates fetch grades failure state', () => { it('updates fetch grades failure state', () => {
const expected = { const expected = {
...initialState, ...initialState,

View File

@@ -1,26 +1,27 @@
import { import {
GOT_ROLES, GOT_ROLES,
ERROR_FETCHING_ROLES, ERROR_FETCHING_ROLES,
} from '../constants/actionTypes/roles'; } from '../constants/actionTypes/roles';
const initialState = { const initialState = {
canUserViewGradebook: null, canUserViewGradebook: null,
}; };
const roles = (state = initialState, action) => { const roles = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case GOT_ROLES: case GOT_ROLES:
return { return {
...state, ...state,
canUserViewGradebook: action.canUserViewGradebook, canUserViewGradebook: action.canUserViewGradebook,
}; };
case ERROR_FETCHING_ROLES: case ERROR_FETCHING_ROLES:
return { return {
...state, ...state,
canUserViewGradebook: false, canUserViewGradebook: false,
}; };
default: default:
return state; return state;
}}; }
};
export default roles; export default roles;

View File

@@ -16,7 +16,7 @@ describe('tracks reducer', () => {
it('updates canUserViewGradebook to true', () => { it('updates canUserViewGradebook to true', () => {
const expected = { const expected = {
...initialState, ...initialState,
canUserViewGradebook: true canUserViewGradebook: true,
}; };
expect(roles(undefined, { expect(roles(undefined, {
type: GOT_ROLES, type: GOT_ROLES,
@@ -27,7 +27,7 @@ describe('tracks reducer', () => {
it('updates canUserViewGradebook to false', () => { it('updates canUserViewGradebook to false', () => {
const expected = { const expected = {
...initialState, ...initialState,
canUserViewGradebook: false canUserViewGradebook: false,
}; };
expect(roles(undefined, { expect(roles(undefined, {
type: GOT_ROLES, type: GOT_ROLES,

View File

@@ -3,7 +3,7 @@ import { configuration } from '../../config';
class LmsApiService { class LmsApiService {
static baseUrl = configuration.LMS_BASE_URL; static baseUrl = configuration.LMS_BASE_URL;
static pageSize = 10 static pageSize = 25
static fetchGradebookData(courseId, searchText, cohort, track) { static fetchGradebookData(courseId, searchText, cohort, track) {
let gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/`; let gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/`;
@@ -25,7 +25,10 @@ class LmsApiService {
/* /*
updateData is expected to be a list of objects with the keys 'user_id' (an integer), 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: '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', 'earned_all_override',
'possible_all_override',
'earned_graded_override',
and 'possible_graded_override',
each of which should be an integer. each of which should be an integer.
Example: Example:
[ [
@@ -46,7 +49,7 @@ class LmsApiService {
} }
static fetchTracks(courseId) { static fetchTracks(courseId) {
const trackUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/course/${courseId}`; const trackUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/course/${courseId}?include_expired=1`;
return apiClient.get(trackUrl); return apiClient.get(trackUrl);
} }

View File

@@ -24,6 +24,7 @@ const eventsMap = {
courseId: action.courseId, courseId: action.courseId,
track: action.track, track: action.track,
cohort: action.cohort, cohort: action.cohort,
assignmentType: action.assignmentType,
prev: action.prev, prev: action.prev,
next: action.next, next: action.next,
}, },

View File

@@ -12,8 +12,6 @@ import store from './data/store';
import FooterLogo from '../assets/edx-footer.png'; import FooterLogo from '../assets/edx-footer.png';
import './App.scss'; import './App.scss';
var courseId = window.location.pathname.substring(1);
const App = () => ( const App = () => (
<Provider store={store}> <Provider store={store}>
<Router> <Router>