Skip to content

Commit

Permalink
Merge pull request #50 from edx/schen/frozen_grades
Browse files Browse the repository at this point in the history
fix(feat): Prevent editing if course grades are frozen
  • Loading branch information
schenedx authored Dec 11, 2018
2 parents 8d89bc1 + f1ab3d0 commit 3bf3aca
Show file tree
Hide file tree
Showing 8 changed files with 5,502 additions and 5,431 deletions.
10,802 changes: 5,407 additions & 5,395 deletions package-lock.json

Large diffs are not rendered by default.

47 changes: 30 additions & 17 deletions src/components/Gradebook/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,36 +179,44 @@ export default class Gradebook extends React.Component {
roundGrade = percent => parseFloat(percent.toFixed(DECIMAL_PRECISION));

formatter = {
percent: entries => entries.map((entry) => {
percent: (entries, areGradesFrozen) => entries.map((entry) => {
const results = { username: entry.username };
const assignments = entry.section_breakdown
.filter(section => section.is_graded)
.reduce((acc, subsection) => {
acc[subsection.label] = (
<button
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{this.roundGrade(subsection.percent * 100)}%
</button>);
if (areGradesFrozen) {
acc[subsection.label] = `${this.roundGrade(subsection.percent * 100)} %`;
} else {
acc[subsection.label] = (
<button
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{this.roundGrade(subsection.percent * 100)}%
</button>);
}
return acc;
}, {});
const totals = { total: `${this.roundGrade(entry.percent * 100)}%` };
return Object.assign(results, assignments, totals);
}),

absolute: entries => entries.map((entry) => {
absolute: (entries, areGradesFrozen) => entries.map((entry) => {
const results = { username: entry.username };
const assignments = entry.section_breakdown
.filter(section => section.is_graded)
.reduce((acc, subsection) => {
acc[subsection.label] = (
<button
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{this.roundGrade(subsection.score_earned)}/{this.roundGrade(subsection.score_possible)}
</button>);
if (areGradesFrozen) {
acc[subsection.label] = `${this.roundGrade(subsection.score_earned)}/${this.roundGrade(subsection.score_possible)}`;
} else {
acc[subsection.label] = (
<button
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{this.roundGrade(subsection.score_earned)}/{this.roundGrade(subsection.score_possible)}
</button>);
}
return acc;
}, {});

Expand All @@ -233,6 +241,11 @@ export default class Gradebook extends React.Component {
</a>
<h1>Gradebook</h1>
<h3> {this.props.match.params.courseId}</h3>
{ this.props.areGradesFrozen &&
<div className="alert alert-danger" role="alert" >
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
}
<hr />
<div className="d-flex justify-content-between" >
<div>
Expand Down Expand Up @@ -335,7 +348,7 @@ export default class Gradebook extends React.Component {
<div className="gbook">
<Table
columns={this.props.headings}
data={this.formatter[this.props.format](this.props.grades)}
data={this.formatter[this.props.format](this.props.grades, this.props.areGradesFrozen)}
tableSortable
defaultSortDirection="asc"
defaultSortedColumn="username"
Expand Down
1 change: 1 addition & 0 deletions src/containers/GradebookPage/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const mapStateToProps = state => (
prevPage: state.grades.prevPage,
nextPage: state.grades.nextPage,
assignmnetTypes: state.assignmentTypes.results,
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
showSpinner: state.grades.showSpinner,
}
);
Expand Down
3 changes: 3 additions & 0 deletions src/data/actions/assignmentTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';
import LmsApiService from '../services/LmsApiService';

const startedFetchingAssignmentTypes = () => ({ type: STARTED_FETCHING_ASSIGNMENT_TYPES });
const errorFetchingAssignmentTypes = () => ({ type: ERROR_FETCHING_ASSIGNMENT_TYPES });
const gotAssignmentTypes = assignmentTypes => ({ type: GOT_ASSIGNMENT_TYPES, assignmentTypes });
const gotGradesFrozen = areGradesFrozen => ({ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen });

const fetchAssignmentTypes = courseId => (
(dispatch) => {
Expand All @@ -16,6 +18,7 @@ const fetchAssignmentTypes = courseId => (
.then(response => response.data)
.then((data) => {
dispatch(gotAssignmentTypes(Object.keys(data.assignment_types)));
dispatch(gotGradesFrozen(data.grades_frozen));
})
.catch(() => {
dispatch(errorFetchingAssignmentTypes());
Expand Down
56 changes: 37 additions & 19 deletions src/data/actions/assignmentTypes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';

const mockStore = configureMockStore([thunk]);
Expand All @@ -21,29 +22,30 @@ describe('actions', () => {

describe('fetchAssignmentTypes', () => {
const courseId = 'course-v1:edX+DemoX+Demo_Course';

it('dispatches success action after fetching fetchAssignmentTypes', () => {
const responseData = {
assignment_types: {
Exam: {
drop_count: 0,
min_count: 1,
short_label: 'Exam',
type: 'Exam',
weight: 0.25,
},
Homework: {
drop_count: 1,
min_count: 3,
short_label: 'Ex',
type: 'Homework',
weight: 0.75,
},
const responseData = {
assignment_types: {
Exam: {
drop_count: 0,
min_count: 1,
short_label: 'Exam',
type: 'Exam',
weight: 0.25,
},
Homework: {
drop_count: 1,
min_count: 3,
short_label: 'Ex',
type: 'Homework',
weight: 0.75,
},
};
},
grades_frozen: false,
};
it('dispatches success action after fetching fetchAssignmentTypes', () => {
const expectedActions = [
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: responseData.grades_frozen },
];
const store = mockStore();

Expand All @@ -69,5 +71,21 @@ describe('actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
});

it('dispatches frozen grade action with True value after fetching', () => {
const expectedActions = [
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: true },
];
const store = mockStore();
responseData.grades_frozen = true;
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
.replyOnce(200, JSON.stringify(responseData));

return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});
2 changes: 2 additions & 0 deletions src/data/constants/actionTypes/assignmentTypes.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const STARTED_FETCHING_ASSIGNMENT_TYPES = 'STARTED_FETCHING_ASSIGNMENT_TYPES';
const GOT_ASSIGNMENT_TYPES = 'GOT_ASSIGNMENT_TYPES';
const ERROR_FETCHING_ASSIGNMENT_TYPES = 'ERROR_FETCHING_ASSIGNMENT_TYPES';
const GOT_ARE_GRADES_FROZEN = 'GOT_ARE_GRADES_FROZEN';

export {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
};

8 changes: 8 additions & 0 deletions src/data/reducers/assignmentTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';

const initialState = {
Expand Down Expand Up @@ -31,6 +32,13 @@ const assignmentTypes = (state = initialState, action) => {
finishedFetching: true,
errorFetching: true,
};
case GOT_ARE_GRADES_FROZEN:
return {
...state,
areGradesFrozen: action.areGradesFrozen,
errorFetching: false,
finishedFetching: true,
};
default:
return state;
}
Expand Down
14 changes: 14 additions & 0 deletions src/data/reducers/assignmentTypes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';

const initialState = {
Expand Down Expand Up @@ -51,4 +52,17 @@ describe('assignmentTypes reducer', () => {
type: ERROR_FETCHING_ASSIGNMENT_TYPES,
})).toEqual(expected);
});

it('updates areGradesFrozen success state', () => {
const expected = {
...initialState,
errorFetching: false,
finishedFetching: true,
areGradesFrozen: true,
};
expect(assignmentTypes(undefined, {
type: GOT_ARE_GRADES_FROZEN,
areGradesFrozen: true,
})).toEqual(expected);
});
});

0 comments on commit 3bf3aca

Please sign in to comment.