Compare commits
16 Commits
v1.4.9
...
aed/websoc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0aada7794 | ||
|
|
09e482e893 | ||
|
|
d4421d47fc | ||
|
|
0ef8e773cc | ||
|
|
1dac20b866 | ||
|
|
ed2d715ce0 | ||
|
|
c82c49ea59 | ||
|
|
a9f8aec5f9 | ||
|
|
a63e9a5347 | ||
|
|
2581812118 | ||
|
|
c4fe803a95 | ||
|
|
93be5329ca | ||
|
|
80ba7e7152 | ||
|
|
f88526aa3a | ||
|
|
c0f08eee58 | ||
|
|
ef62ea35dc |
@@ -1,4 +1,5 @@
|
||||
coverage/*
|
||||
dist/
|
||||
node_modules/
|
||||
src/postcss.config.js
|
||||
src/segment.js
|
||||
|
||||
@@ -23,6 +23,7 @@ before_script: greenkeeper-lockfile-update
|
||||
after_script: greenkeeper-lockfile-upload
|
||||
script:
|
||||
- make validate-no-uncommitted-package-lock-changes
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run build
|
||||
after_success:
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
* `config`
|
||||
|
||||
@@ -47,7 +47,7 @@ module.exports = Merge.smart(commonConfig, {
|
||||
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
|
||||
options: {
|
||||
|
||||
@@ -81,3 +81,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mb-85 {
|
||||
margin-bottom: 85px;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Button,
|
||||
InputSelect,
|
||||
@@ -24,12 +25,41 @@ export default class Gradebook extends React.Component {
|
||||
updateVal: 0,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
socket: null,
|
||||
websocketMsg: {
|
||||
visible: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const urlQuery = queryString.parse(this.props.location.search);
|
||||
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) => {
|
||||
@@ -119,6 +149,8 @@ export default class Gradebook extends React.Component {
|
||||
|
||||
updateAssignmentTypes = (event) => {
|
||||
this.props.filterColumns(event, this.props.grades[0]);
|
||||
const updatedQueryStrings = this.updateQueryParams('assignmentType', event);
|
||||
this.props.history.push(updatedQueryStrings);
|
||||
}
|
||||
|
||||
updateTracks = (event) => {
|
||||
@@ -131,6 +163,7 @@ export default class Gradebook extends React.Component {
|
||||
this.props.match.params.courseId,
|
||||
this.props.selectedCohort,
|
||||
selectedTrackSlug,
|
||||
this.props.selectedAssignmentType,
|
||||
);
|
||||
const updatedQueryStrings = this.updateQueryParams('track', selectedTrackSlug);
|
||||
this.props.history.push(updatedQueryStrings);
|
||||
@@ -146,18 +179,9 @@ export default class Gradebook extends React.Component {
|
||||
this.props.match.params.courseId,
|
||||
selectedCohortId,
|
||||
this.props.selectedTrack,
|
||||
this.props.selectedAssignmentType,
|
||||
);
|
||||
const updatedQueryStrings = 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';
|
||||
this.updateQueryParams('cohort', selectedCohortId);
|
||||
};
|
||||
|
||||
mapSelectedCohortEntry = (entry) => {
|
||||
@@ -182,7 +206,6 @@ export default class Gradebook extends React.Component {
|
||||
percent: (entries, areGradesFrozen) => entries.map((entry) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, subsection) => {
|
||||
if (areGradesFrozen) {
|
||||
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) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, subsection) => {
|
||||
const scoreEarned = this.roundGrade(subsection.score_earned);
|
||||
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">
|
||||
<span id="score-view-group-label">Score View:</span>
|
||||
<span>
|
||||
<input
|
||||
id="score-view-percent"
|
||||
className="ml-2 mr-1"
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="percent"
|
||||
defaultChecked
|
||||
onClick={() => this.props.toggleFormat('percent')}
|
||||
/>
|
||||
<label className="mr-2" htmlFor="score-view-percent">Percent</label>
|
||||
<label className="mr-2" htmlFor="score-view-percent">
|
||||
<input
|
||||
id="score-view-percent"
|
||||
className="ml-2 mr-1"
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="percent"
|
||||
defaultChecked
|
||||
onClick={() => this.props.toggleFormat('percent')}
|
||||
/>
|
||||
Percent
|
||||
</label>
|
||||
</span>
|
||||
<span>
|
||||
<input
|
||||
id="score-view-absolute"
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="absolute"
|
||||
className="mr-1"
|
||||
onClick={() => this.props.toggleFormat('absolute')}
|
||||
/>
|
||||
<label htmlFor="score-view-absolute">Absolute</label>
|
||||
<label htmlFor="score-view-absolute">
|
||||
<input
|
||||
id="score-view-absolute"
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="absolute"
|
||||
className="mr-1"
|
||||
onClick={() => this.props.toggleFormat('absolute')}
|
||||
/>
|
||||
Absolute
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
{ this.props.assignmnetTypes.length > 0 &&
|
||||
{ this.props.assignmentTypes.length > 0 &&
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Assignment Types:
|
||||
@@ -295,8 +321,8 @@ export default class Gradebook extends React.Component {
|
||||
<InputSelect
|
||||
name="assignment-types"
|
||||
ariaLabel="Assignment Types"
|
||||
value={this.mapSelectedTrackEntry(this.props.selectedAssignmentType)}
|
||||
options={this.mapAssignmentTypeEntries(this.props.assignmnetTypes)}
|
||||
value={this.props.selectedAssignmentType}
|
||||
options={this.mapAssignmentTypeEntries(this.props.assignmentTypes)}
|
||||
onChange={this.updateAssignmentTypes}
|
||||
/>
|
||||
</div>
|
||||
@@ -325,12 +351,28 @@ export default class Gradebook extends React.Component {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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 })}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
@@ -342,14 +384,20 @@ export default class Gradebook extends React.Component {
|
||||
onClose={() => this.props.updateBanner(false)}
|
||||
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)}
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={this.props.headings}
|
||||
data={this.formatter[this.props.format](this.props.grades, this.props.areGradesFrozen)}
|
||||
tableSortable
|
||||
defaultSortDirection="asc"
|
||||
defaultSortedColumn="username"
|
||||
data={this.formatter[this.props.format](
|
||||
this.props.grades,
|
||||
this.props.areGradesFrozen,
|
||||
)}
|
||||
rowHeaderColumnKey="username"
|
||||
/>
|
||||
</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,
|
||||
};
|
||||
|
||||
@@ -4,13 +4,6 @@ import { Hyperlink } from '@edx/paragon';
|
||||
import EdxLogo from '../../../assets/edx-sm.png';
|
||||
|
||||
export default class Header extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
mobileNavOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
renderLogo() {
|
||||
return (
|
||||
<img src={EdxLogo} alt="edX logo" height="30" width="60" />
|
||||
|
||||
@@ -10,7 +10,7 @@ exports[`PageButtons prev not null, next not null 1`] = `
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="btn btn-outline-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
@@ -25,7 +25,7 @@ exports[`PageButtons prev not null, next not null 1`] = `
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="btn btn-outline-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
@@ -52,7 +52,7 @@ exports[`PageButtons prev not null, next null 1`] = `
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="btn btn-outline-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
@@ -67,7 +67,7 @@ exports[`PageButtons prev not null, next null 1`] = `
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="btn btn-outline-primary"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
@@ -94,7 +94,7 @@ exports[`PageButtons prev null, next not null 1`] = `
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="btn btn-outline-primary"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
@@ -109,7 +109,7 @@ exports[`PageButtons prev null, next not null 1`] = `
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="btn btn-outline-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
@@ -136,7 +136,7 @@ exports[`PageButtons prev null, next null 1`] = `
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="btn btn-outline-primary"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
@@ -151,7 +151,7 @@ exports[`PageButtons prev null, next null 1`] = `
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="btn btn-outline-primary"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
|
||||
export default function PageButtons({prevPage, nextPage, selectedTrack, selectedCohort, getPrevNextGrades}) {
|
||||
export default function PageButtons({
|
||||
prevPage, nextPage, selectedTrack, selectedCohort, selectedAssignmentType,
|
||||
getPrevNextGrades, match,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="d-flex justify-content-center"
|
||||
@@ -13,18 +15,63 @@ export default function PageButtons({prevPage, nextPage, selectedTrack, selected
|
||||
<Button
|
||||
label="Previous Page"
|
||||
style={{ margin: '20px' }}
|
||||
buttonType="primary"
|
||||
buttonType="outline-primary"
|
||||
disabled={!prevPage}
|
||||
onClick={() => getPrevNextGrades(prevPage, selectedCohort, selectedTrack)}
|
||||
onClick={() =>
|
||||
getPrevNextGrades(
|
||||
prevPage,
|
||||
match.params.courseId,
|
||||
selectedCohort,
|
||||
selectedTrack,
|
||||
selectedAssignmentType,
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
label="Next Page"
|
||||
style={{ margin: '20px' }}
|
||||
buttonType="primary"
|
||||
buttonType="outline-primary"
|
||||
disabled={!nextPage}
|
||||
onClick={() => getPrevNextGrades(nextPage, selectedCohort, selectedTrack)}
|
||||
onClick={() =>
|
||||
getPrevNextGrades(
|
||||
nextPage,
|
||||
match.params.courseId,
|
||||
selectedCohort,
|
||||
selectedTrack,
|
||||
selectedAssignmentType,
|
||||
)}
|
||||
/>
|
||||
</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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
fetchPrevNextGrades,
|
||||
gradeUpdateSuccess,
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
filterColumns,
|
||||
@@ -15,6 +16,15 @@ import { fetchTracks } from '../../data/actions/tracks';
|
||||
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
||||
import { getRoles } from '../../data/actions/roles';
|
||||
|
||||
function shouldShowSpinner(state) {
|
||||
if (state.roles.canUserViewGradebook === true) {
|
||||
return state.grades.showSpinner;
|
||||
} else if (state.roles.canUserViewGradebook === false) {
|
||||
return false;
|
||||
} // canUserViewGradebook === null
|
||||
return true;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => (
|
||||
{
|
||||
grades: state.grades.results,
|
||||
@@ -23,37 +33,28 @@ const mapStateToProps = state => (
|
||||
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,
|
||||
assignmnetTypes: state.assignmentTypes.results,
|
||||
assignmentTypes: state.assignmentTypes.results,
|
||||
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
|
||||
showSpinner: shouldShowSpinner(state),
|
||||
canUserViewGradebook: state.roles.canUserViewGradebook
|
||||
canUserViewGradebook: state.roles.canUserViewGradebook,
|
||||
}
|
||||
);
|
||||
|
||||
function shouldShowSpinner (state) {
|
||||
if (state.roles.canUserViewGradebook === true){
|
||||
return state.grades.showSpinner;
|
||||
} else if (state.roles.canUserViewGradebook === false){
|
||||
return false;
|
||||
} else { // canUserViewGradebook === null
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => (
|
||||
{
|
||||
getUserGrades: (courseId, cohort, track) => {
|
||||
dispatch(fetchGrades(courseId, cohort, track));
|
||||
getUserGrades: (courseId, cohort, track, assignmentType) => {
|
||||
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
||||
},
|
||||
searchForUser: (courseId, searchText, cohort, track) => {
|
||||
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, false));
|
||||
searchForUser: (courseId, searchText, cohort, track, assignmentType) => {
|
||||
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, assignmentType, false));
|
||||
},
|
||||
getPrevNextGrades: (endpoint, cohort, track) => {
|
||||
dispatch(fetchPrevNextGrades(endpoint, cohort, track));
|
||||
getPrevNextGrades: (endpoint, courseId, cohort, track, assignmentType) => {
|
||||
dispatch(fetchPrevNextGrades(endpoint, courseId, cohort, track, assignmentType));
|
||||
},
|
||||
getCohorts: (courseId) => {
|
||||
dispatch(fetchCohorts(courseId));
|
||||
@@ -79,6 +80,9 @@ const mapDispatchToProps = dispatch => (
|
||||
getRoles: (matchParams, urlQuery) => {
|
||||
dispatch(getRoles(matchParams, urlQuery));
|
||||
},
|
||||
gradeUpdateSuccess: (courseId, data) => {
|
||||
dispatch(gradeUpdateSuccess(courseId, data));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -7,48 +7,39 @@ import {
|
||||
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 store from '../store';
|
||||
import { headingMapper, gradeSortMap, sortAlphaAsc } from './utils';
|
||||
import { headingMapper, sortAlphaAsc } from './utils';
|
||||
import apiClient from '../apiClient';
|
||||
|
||||
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 finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
|
||||
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
|
||||
const gotGrades = (grades, cohort, track, headings, prev, next) => ({
|
||||
const gotGrades = (grades, cohort, track, assignmentType, headings, prev, next, courseId) => ({
|
||||
type: GOT_GRADES,
|
||||
grades,
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
headings,
|
||||
prev,
|
||||
next,
|
||||
courseId,
|
||||
});
|
||||
|
||||
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
|
||||
const gradeUpdateSuccess = responseData => ({
|
||||
const gradeUpdateSuccess = (courseId, responseData) => ({
|
||||
type: GRADE_UPDATE_SUCCESS,
|
||||
courseId,
|
||||
payload: { responseData },
|
||||
});
|
||||
const gradeUpdateFailure = error => ({
|
||||
const gradeUpdateFailure = (courseId, error) => ({
|
||||
type: GRADE_UPDATE_FAILURE,
|
||||
courseId,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
@@ -58,13 +49,13 @@ const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType
|
||||
const filterColumns = (filterType, exampleUser) => (
|
||||
dispatch => dispatch({
|
||||
type: FILTER_COLUMNS,
|
||||
headings: headingMapper(filterType)(dispatch, exampleUser),
|
||||
headings: headingMapper(filterType)(exampleUser),
|
||||
})
|
||||
);
|
||||
|
||||
const updateBanner = showSuccess => ({ type: UPDATE_BANNER, showSuccess });
|
||||
|
||||
const fetchGrades = (courseId, cohort, track, showSuccess) => (
|
||||
const fetchGrades = (courseId, cohort, track, assignmentType, showSuccess) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
return LmsApiService.fetchGradebookData(courseId, null, cohort, track)
|
||||
@@ -74,9 +65,11 @@ const fetchGrades = (courseId, cohort, track, showSuccess) => (
|
||||
data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]),
|
||||
assignmentType,
|
||||
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
|
||||
data.previous,
|
||||
data.next,
|
||||
courseId,
|
||||
));
|
||||
dispatch(finishedFetchingGrades());
|
||||
dispatch(updateBanner(!!showSuccess));
|
||||
@@ -87,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(startedFetchingGrades());
|
||||
return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track)
|
||||
@@ -97,9 +97,11 @@ const fetchMatchingUserGrades = (courseId, searchText, cohort, track, showSucces
|
||||
data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]),
|
||||
assignmentType,
|
||||
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
|
||||
data.previous,
|
||||
data.next,
|
||||
courseId,
|
||||
));
|
||||
dispatch(finishedFetchingGrades());
|
||||
dispatch(updateBanner(showSuccess));
|
||||
@@ -110,7 +112,7 @@ const fetchMatchingUserGrades = (courseId, searchText, cohort, track, showSucces
|
||||
}
|
||||
);
|
||||
|
||||
const fetchPrevNextGrades = (endpoint, cohort, track) => (
|
||||
const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
return apiClient.get(endpoint)
|
||||
@@ -120,9 +122,11 @@ const fetchPrevNextGrades = (endpoint, cohort, track) => (
|
||||
data.results.sort(sortAlphaAsc),
|
||||
cohort,
|
||||
track,
|
||||
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]),
|
||||
assignmentType,
|
||||
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
|
||||
data.previous,
|
||||
data.next,
|
||||
courseId,
|
||||
));
|
||||
dispatch(finishedFetchingGrades());
|
||||
})
|
||||
@@ -132,18 +136,24 @@ const fetchPrevNextGrades = (endpoint, cohort, track) => (
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
const updateGrades = (courseId, updateData, searchText, cohort, track) => (
|
||||
(dispatch) => {
|
||||
dispatch(gradeUpdateRequest());
|
||||
return LmsApiService.updateGradebookData(courseId, updateData)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gradeUpdateSuccess(data));
|
||||
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, true));
|
||||
dispatch(gradeUpdateSuccess(courseId, data));
|
||||
// dispatch(fetchMatchingUserGrades(
|
||||
// courseId,
|
||||
// searchText,
|
||||
// cohort,
|
||||
// track,
|
||||
// defaultAssignmentFilter,
|
||||
// true,
|
||||
// ));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(gradeUpdateFailure(error));
|
||||
dispatch(gradeUpdateFailure(courseId, error));
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -161,7 +171,6 @@ export {
|
||||
gradeUpdateFailure,
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
sortGrades,
|
||||
filterColumns,
|
||||
updateBanner,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,8 @@ describe('actions', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const expectedCohort = 1;
|
||||
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 = {
|
||||
next: `${fetchGradesURL}&cursor=2344fda`,
|
||||
previous: null,
|
||||
@@ -94,22 +95,20 @@ describe('actions', () => {
|
||||
grades: responseData.results.sort(sortAlphaAsc),
|
||||
cohort: expectedCohort,
|
||||
track: expectedTrack,
|
||||
assignmentType: expectedAssignmentType,
|
||||
headings: [
|
||||
{
|
||||
columnSortable: true,
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
onSort: expect.anything(),
|
||||
},
|
||||
{
|
||||
columnSortable: true,
|
||||
key: 'total',
|
||||
label: 'Total',
|
||||
onSort: expect.anything(),
|
||||
},
|
||||
],
|
||||
prev: responseData.previous,
|
||||
next: responseData.next,
|
||||
courseId,
|
||||
},
|
||||
{ type: FINISHED_FETCHING_GRADES },
|
||||
{ type: UPDATE_BANNER, showSuccess: false },
|
||||
@@ -119,7 +118,13 @@ describe('actions', () => {
|
||||
axiosMock.onGet(fetchGradesURL)
|
||||
.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);
|
||||
});
|
||||
});
|
||||
@@ -134,7 +139,51 @@ describe('actions', () => {
|
||||
axiosMock.onGet(fetchGradesURL)
|
||||
.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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ const getRoles = (courseId, urlQuery) => (
|
||||
&& allowedRoles.includes(role.role)));
|
||||
dispatch(gotRoles(canUserViewGradebook, courseId));
|
||||
if (canUserViewGradebook) {
|
||||
dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track));
|
||||
dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track, urlQuery.assignmentType));
|
||||
dispatch(fetchTracks(courseId));
|
||||
dispatch(fetchCohorts(courseId));
|
||||
dispatch(fetchAssignmentTypes(courseId));
|
||||
|
||||
@@ -57,7 +57,10 @@ describe('actions', () => {
|
||||
];
|
||||
const store = mockStore();
|
||||
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(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
@@ -75,7 +78,10 @@ describe('actions', () => {
|
||||
const store = mockStore();
|
||||
|
||||
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(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
@@ -91,7 +97,10 @@ describe('actions', () => {
|
||||
const store = mockStore();
|
||||
|
||||
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(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
@@ -105,7 +114,10 @@ describe('actions', () => {
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(200, JSON.stringify(makeRoleListObj([], false)));
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([], false)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
@@ -123,7 +135,10 @@ describe('actions', () => {
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(200, JSON.stringify(makeRoleListObj([], true)));
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([], true)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('actions', () => {
|
||||
|
||||
describe('fetchTracks', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const trackUrl = `${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}?include_expired=1`;
|
||||
|
||||
it('dispatches success action after fetching tracks', () => {
|
||||
const responseData = {
|
||||
@@ -54,7 +55,7 @@ describe('actions', () => {
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}`)
|
||||
axiosMock.onGet(trackUrl)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchTracks(courseId)).then(() => {
|
||||
@@ -69,7 +70,7 @@ describe('actions', () => {
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}`)
|
||||
axiosMock.onGet(trackUrl)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchTracks(courseId)).then(() => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { sortGrades } from './grades';
|
||||
|
||||
const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||
const a = gradeRowA.username.toUpperCase();
|
||||
const b = gradeRowB.username.toUpperCase();
|
||||
@@ -12,78 +10,24 @@ const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||
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) => {
|
||||
function all(dispatch, entry) {
|
||||
function all(entry) {
|
||||
if (entry) {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { dispatch(sortGrades('username', direction)); },
|
||||
}];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label)
|
||||
.filter(section => section.label)
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: direction => dispatch(sortGrades(s.label, direction)),
|
||||
}));
|
||||
|
||||
const totals = [{
|
||||
label: 'Total',
|
||||
key: 'total',
|
||||
columnSortable: true,
|
||||
onSort: direction => dispatch(sortGrades('total', direction)),
|
||||
}];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
@@ -91,28 +35,24 @@ const headingMapper = (filterKey) => {
|
||||
return [];
|
||||
}
|
||||
|
||||
function some(dispatch, entry) {
|
||||
function some(entry) {
|
||||
if (!entry) return [];
|
||||
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { dispatch(sortGrades('username', direction)); },
|
||||
}];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(section => section.is_graded && section.label && section.category === filterKey)
|
||||
.filter(section => section.label && section.category === filterKey)
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: false,
|
||||
onSort: (direction) => { this.sortNumerically(s.label, direction); },
|
||||
}));
|
||||
|
||||
const totals = [{
|
||||
label: 'Total',
|
||||
key: 'total',
|
||||
columnSortable: true,
|
||||
onSort: direction => dispatch(sortGrades('total', direction)),
|
||||
}];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
@@ -121,5 +61,5 @@ const headingMapper = (filterKey) => {
|
||||
return filterKey === 'All' ? all : some;
|
||||
};
|
||||
|
||||
export { headingMapper, gradeSortMap, sortAlphaAsc };
|
||||
export { headingMapper, sortAlphaAsc };
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ 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';
|
||||
|
||||
@@ -21,7 +20,6 @@ export {
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
SORT_GRADES,
|
||||
FILTER_COLUMNS,
|
||||
UPDATE_BANNER,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const GOT_ROLES = 'GOT_ROLES';
|
||||
const ERROR_FETCHING_ROLES = 'ERROR_FETCHING_ROLES'
|
||||
const ERROR_FETCHING_ROLES = 'ERROR_FETCHING_ROLES';
|
||||
|
||||
export {
|
||||
GOT_ROLES,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_COLUMNS,
|
||||
UPDATE_BANNER,
|
||||
SORT_GRADES,
|
||||
} from '../constants/actionTypes/grades';
|
||||
|
||||
const initialState = {
|
||||
@@ -32,9 +31,11 @@ const grades = (state = initialState, action) => {
|
||||
errorFetching: false,
|
||||
selectedTrack: action.track,
|
||||
selectedCohort: action.cohort,
|
||||
selectedAssignmentType: action.assignmentType,
|
||||
prevPage: action.prev,
|
||||
nextPage: action.next,
|
||||
showSpinner: false,
|
||||
courseId: action.courseId,
|
||||
};
|
||||
case STARTED_FETCHING_GRADES:
|
||||
return {
|
||||
@@ -64,11 +65,6 @@ const grades = (state = initialState, action) => {
|
||||
...state,
|
||||
showSuccess: action.showSuccess,
|
||||
};
|
||||
case SORT_GRADES:
|
||||
return {
|
||||
...state,
|
||||
results: action.results,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_COLUMNS,
|
||||
UPDATE_BANNER,
|
||||
SORT_GRADES,
|
||||
} from '../constants/actionTypes/grades';
|
||||
|
||||
const initialState = {
|
||||
@@ -113,6 +112,7 @@ describe('grades reducer', () => {
|
||||
prevPage: expectedPrev,
|
||||
nextPage: expectedNext,
|
||||
showSpinner: false,
|
||||
courseId,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: GOT_GRADES,
|
||||
@@ -123,6 +123,7 @@ describe('grades reducer', () => {
|
||||
track: expectedTrack,
|
||||
cohort: expectedCohortId,
|
||||
showSpinner: true,
|
||||
courseId,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -162,17 +163,6 @@ describe('grades reducer', () => {
|
||||
})).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', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import {
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES,
|
||||
} from '../constants/actionTypes/roles';
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES,
|
||||
} from '../constants/actionTypes/roles';
|
||||
|
||||
const initialState = {
|
||||
canUserViewGradebook: null,
|
||||
};
|
||||
const initialState = {
|
||||
canUserViewGradebook: null,
|
||||
};
|
||||
|
||||
const roles = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_ROLES:
|
||||
return {
|
||||
...state,
|
||||
canUserViewGradebook: action.canUserViewGradebook,
|
||||
};
|
||||
case ERROR_FETCHING_ROLES:
|
||||
return {
|
||||
...state,
|
||||
canUserViewGradebook: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}};
|
||||
const roles = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case GOT_ROLES:
|
||||
return {
|
||||
...state,
|
||||
canUserViewGradebook: action.canUserViewGradebook,
|
||||
};
|
||||
case ERROR_FETCHING_ROLES:
|
||||
return {
|
||||
...state,
|
||||
canUserViewGradebook: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default roles;
|
||||
export default roles;
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('tracks reducer', () => {
|
||||
it('updates canUserViewGradebook to true', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
canUserViewGradebook: true
|
||||
canUserViewGradebook: true,
|
||||
};
|
||||
expect(roles(undefined, {
|
||||
type: GOT_ROLES,
|
||||
@@ -27,7 +27,7 @@ describe('tracks reducer', () => {
|
||||
it('updates canUserViewGradebook to false', () => {
|
||||
const expected = {
|
||||
...initialState,
|
||||
canUserViewGradebook: false
|
||||
canUserViewGradebook: false,
|
||||
};
|
||||
expect(roles(undefined, {
|
||||
type: GOT_ROLES,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { configuration } from '../../config';
|
||||
|
||||
class LmsApiService {
|
||||
static baseUrl = configuration.LMS_BASE_URL;
|
||||
static pageSize = 10
|
||||
static pageSize = 25
|
||||
|
||||
static fetchGradebookData(courseId, searchText, cohort, track) {
|
||||
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),
|
||||
'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.
|
||||
Example:
|
||||
[
|
||||
@@ -46,7 +49,7 @@ class LmsApiService {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,21 @@ import { GOT_GRADES, GRADE_UPDATE_SUCCESS, GRADE_UPDATE_FAILURE } from './consta
|
||||
import reducers from './reducers';
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
const trackingCategory = 'gradebook';
|
||||
|
||||
const eventsMap = {
|
||||
[GOT_ROLES]: trackPageView(action => ({
|
||||
category: trackingCategory,
|
||||
page: action.courseId,
|
||||
})),
|
||||
[GOT_GRADES]: trackEvent(action => ({
|
||||
name: 'Grades displayed or paginated',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
courseId: action.courseId,
|
||||
track: action.track,
|
||||
cohort: action.cohort,
|
||||
assignmentType: action.assignmentType,
|
||||
prev: action.prev,
|
||||
next: action.next,
|
||||
},
|
||||
@@ -27,12 +32,16 @@ const eventsMap = {
|
||||
[GRADE_UPDATE_SUCCESS]: trackEvent(action => ({
|
||||
name: 'Grades Updated',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
courseId: action.courseId,
|
||||
updatedGrades: action.payload.responseData,
|
||||
},
|
||||
})),
|
||||
[GRADE_UPDATE_FAILURE]: trackEvent(action => ({
|
||||
name: 'Grades Fail to Update',
|
||||
properties: {
|
||||
category: trackingCategory,
|
||||
courseId: action.courseId,
|
||||
error: action.payload.error,
|
||||
},
|
||||
})),
|
||||
|
||||
@@ -12,8 +12,6 @@ import store from './data/store';
|
||||
import FooterLogo from '../assets/edx-footer.png';
|
||||
import './App.scss';
|
||||
|
||||
var courseId = window.location.pathname.substring(1);
|
||||
|
||||
const App = () => (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
|
||||
Reference in New Issue
Block a user