Merge pull request #99 from edx/andytr1/gradebook-show-override-history

EDUCATOR-4353 - ui - show override history
This commit is contained in:
Andytr1
2019-07-10 15:29:16 -04:00
committed by GitHub
8 changed files with 208 additions and 39 deletions

View File

@@ -42,6 +42,25 @@
margin-left: 10px;
}
}
.grade-history-header{
float: left;
}
.grade-history-assignment{
padding-right: 49px;
}
.grade-history-student{
padding-right: 75px;
}
.grade-history-original-grade{
padding-right: 25px;
}
.grade-history-current-grade{
padding-right: 25px;
}
.gbook {
overflow-x: scroll;
@@ -98,4 +117,8 @@
.mb-85 {
margin-bottom: 85px;
}
.modal-dialog {
max-width: 1000px;
}

View File

@@ -13,8 +13,12 @@ import {
import queryString from 'query-string';
import { configuration } from '../../config';
import PageButtons from '../PageButtons';
import { formatDateForDisplay } from '../../data/actions/utils';
const DECIMAL_PRECISION = 2;
const GRADE_OVERRIDE_HISTORY_COLUMNS = [{ label: 'Date', key: 'date' }, { label: 'Grader', key: 'grader' },
{ label: 'Reason', key: 'reason' },
{ label: 'Adjusted grade', key: 'adjustedGrade' }];
export default class Gradebook extends React.Component {
constructor(props) {
@@ -22,48 +26,51 @@ export default class Gradebook extends React.Component {
this.state = {
filterValue: '',
modalOpen: false,
modalModel: [{}],
updateVal: 0,
adjustedGradeValue: 0,
updateModuleId: null,
updateUserId: null,
reasonForChange: '',
};
this.fileFormRef = React.createRef();
this.fileInputRef = React.createRef();
this.myRef = React.createRef();
}
componentDidMount() {
const urlQuery = queryString.parse(this.props.location.search);
this.props.getRoles(this.props.courseId, urlQuery);
this.overrideReasonInput.focus();
}
setNewModalState = (userEntry, subsection) => {
let adjustedGradePossible = '';
let currentGradePossible = '';
if (subsection.attempted) {
adjustedGradePossible = ` / ${subsection.score_possible}`;
currentGradePossible = `/${subsection.score_possible}`;
}
this.setState({
modalModel: [{
username: userEntry.username,
currentGrade: `${subsection.score_earned}${currentGradePossible}`,
adjustedGrade: (
<span>
<input
style={{ width: '25px' }}
type="text"
onChange={event => this.setState({ updateVal: event.target.value })}
/>{adjustedGradePossible}
</span>
),
assignmentName: `${subsection.subsection_name}`,
}],
modalOpen: true,
updateModuleId: subsection.module_id,
updateUserId: userEntry.user_id,
});
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
setNewModalState = (userEntry, subsection) => {
this.props.fetchGradeOverrideHistory(
subsection.module_id,
userEntry.user_id,
);
let adjustedGradePossible = '100';
if (subsection.attempted) {
adjustedGradePossible = ` / ${subsection.score_possible}`;
}
this.setState({
modalAssignmentName: `${subsection.subsection_name}`,
modalOpen: true,
updateModuleId: subsection.module_id,
updateUserId: userEntry.user_id,
updateUserName: userEntry.username,
todaysDate: formatDateForDisplay(new Date()),
originalGrade: subsection.score_earned,
adjustedGradePossible,
reasonForChange: '',
adjustedGradeValue: '',
});
}
getLearnerInformation = entry => (
<div>
<div>{entry.username}</div>
@@ -85,7 +92,8 @@ export default class Gradebook extends React.Component {
user_id: this.state.updateUserId,
usage_id: this.state.updateModuleId,
grade: {
earned_graded_override: this.state.updateVal,
earned_graded_override: this.state.adjustedGradeValue,
comment: this.state.reasonForChange,
},
},
],
@@ -95,10 +103,11 @@ export default class Gradebook extends React.Component {
);
this.setState({
modalModel: [{}],
modalOpen: false,
updateModuleId: null,
updateUserId: null,
reasonForChange: '',
adjustedGradeValue: '',
});
}
@@ -435,11 +444,46 @@ export default class Gradebook extends React.Component {
closeText="Cancel"
body={(
<div>
<h3>{this.state.modalModel[0].assignmentName}</h3>
<Table
columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
data={this.state.modalModel}
<div>
<div className="grade-history-header grade-history-assignment">Assignment: </div> <div>{this.state.modalAssignmentName}</div>
<div className="grade-history-header grade-history-student">Student: </div> <div>{this.state.updateUserName}</div>
<div className="grade-history-header grade-history-original-grade">Original Grade: </div> <div>{this.state.originalGrade}</div>
<div className="grade-history-header grade-history-current-grade">Current Grade: </div> <div>{this.props.gradeOverrideCurrentPossibleGradedOverride}</div>
</div>
<StatusAlert
alertType="danger"
dialog="Error retrieving grade override history."
open={this.props.errorFetchingGradeOverrideHistory}
dismissible={false}
/>
{ !this.props.errorFetchingGradeOverrideHistory && (
<Table
columns={GRADE_OVERRIDE_HISTORY_COLUMNS}
data={[...this.props.gradeOverrides, {
date: this.state.todaysDate,
grader: this.state.updateUserName,
reason: (<input
type="text"
name="reasonForChange"
value={this.state.reasonForChange}
onChange={value => this.onChange(value)}
ref={(input) => { this.overrideReasonInput = input; }}
/>),
adjustedGrade: (
<span>
<input
type="text"
name="adjustedGradeValue"
value={this.state.adjustedGradeValue}
onChange={value => this.onChange(value)}
/> {this.state.adjustedGradePossible}
</span>),
}]}
/>)}
<div>Showing most recent actions(max 5). To see more, please contact
support.
</div>
<div>Note: Once you save, your changes will be visible to students.</div>
</div>
)}
@@ -452,10 +496,10 @@ export default class Gradebook extends React.Component {
]}
onClose={() => this.setState({
modalOpen: false,
modalModel: [{}],
updateVal: 0,
adjustedGradeValue: 0,
updateModuleId: null,
updateUserId: null,
reasonForChange: '',
})}
/>
</div>
@@ -510,6 +554,8 @@ Gradebook.defaultProps = {
canUserViewGradebook: false,
cohorts: [],
grades: [],
gradeOverrides: [],
gradeOverrideCurrentPossibleGradedOverride: null,
location: {
search: '',
},
@@ -522,6 +568,7 @@ Gradebook.defaultProps = {
bulkImportError: '',
showBulkManagement: false,
bulkManagementHistory: [],
errorFetchingGradeOverrideHistory: '',
};
Gradebook.propTypes = {
@@ -533,6 +580,7 @@ Gradebook.propTypes = {
format: PropTypes.string.isRequired,
getRoles: PropTypes.func.isRequired,
getUserGrades: PropTypes.func.isRequired,
fetchGradeOverrideHistory: PropTypes.func.isRequired,
grades: PropTypes.arrayOf(PropTypes.shape({
percent: PropTypes.number,
section_breakdown: PropTypes.arrayOf(PropTypes.shape({
@@ -548,6 +596,13 @@ Gradebook.propTypes = {
user_id: PropTypes.number,
user_name: PropTypes.string,
})),
gradeOverrides: PropTypes.arrayOf(PropTypes.shape({
date: PropTypes.string,
grader: PropTypes.string,
reason: PropTypes.string,
adjustedGrade: PropTypes.number,
})),
gradeOverrideCurrentPossibleGradedOverride: PropTypes.number,
headings: PropTypes.arrayOf(PropTypes.string).isRequired,
history: PropTypes.shape({
push: PropTypes.func,
@@ -573,6 +628,7 @@ Gradebook.propTypes = {
gradeExportUrl: PropTypes.string.isRequired,
submitFileUploadFormData: PropTypes.func.isRequired,
bulkImportError: PropTypes.string,
errorFetchingGradeOverrideHistory: PropTypes.string,
showBulkManagement: PropTypes.bool,
bulkManagementHistory: PropTypes.arrayOf(PropTypes.shape({
operation: PropTypes.oneOf(['commit', 'error']),

View File

@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import Gradebook from '../../components/Gradebook';
import {
fetchGrades,
fetchGradeOverrideHistory,
fetchMatchingUserGrades,
fetchPrevNextGrades,
updateGrades,
@@ -32,6 +33,12 @@ const mapStateToProps = (state, ownProps) => (
{
courseId: ownProps.match.params.courseId,
grades: state.grades.results,
gradeOverrides: state.grades.gradeOverrideHistoryResults,
gradeOverrideCurrentEarnedAllOverride: state.grades.gradeOverrideCurrentEarnedAllOverride,
gradeOverrideCurrentPossibleAllOverride: state.grades.gradeOverrideCurrentPossibleAllOverride,
gradeOverrideCurrentEarnedGradedOverride: state.grades.gradeOverrideCurrentEarnedGradedOverride,
gradeOverrideCurrentPossibleGradedOverride:
state.grades.gradeOverrideCurrentPossibleGradedOverride,
headings: state.grades.headings,
tracks: state.tracks.results,
cohorts: state.cohorts.results,
@@ -40,6 +47,7 @@ const mapStateToProps = (state, ownProps) => (
selectedAssignmentType: state.grades.selectedAssignmentType,
format: state.grades.gradeFormat,
showSuccess: state.grades.showSuccess,
errorFetchingGradeOverrideHistory: state.grades.errorFetchingOverrideHistory,
prevPage: state.grades.prevPage,
nextPage: state.grades.nextPage,
assignmentTypes: state.assignmentTypes.results,
@@ -61,6 +69,7 @@ const mapStateToProps = (state, ownProps) => (
const mapDispatchToProps = {
getUserGrades: fetchGrades,
fetchGradeOverrideHistory,
searchForUser: fetchMatchingUserGrades,
getPrevNextGrades: fetchPrevNextGrades,
getCohorts: fetchCohorts,

View File

@@ -15,9 +15,11 @@ import {
UPLOAD_ERR,
GOT_BULK_HISTORY,
BULK_HISTORY_ERR,
GOT_GRADE_OVERRIDE_HISTORY,
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
} from '../constants/actionTypes/grades';
import LmsApiService from '../services/LmsApiService';
import { headingMapper, sortAlphaAsc } from './utils';
import { headingMapper, sortAlphaAsc, formatDateForDisplay } from './utils';
import apiClient from '../apiClient';
const defaultAssignmentFilter = 'All';
@@ -31,6 +33,7 @@ const bulkHistoryError = () => ({ type: BULK_HISTORY_ERR });
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
const errorFetchingGradeOverrideHistory = () => ({ type: ERROR_FETCHING_GRADE_OVERRIDE_HISTORY });
const gotGrades = (grades, cohort, track, assignmentType, headings, prev, next, courseId) => ({
type: GOT_GRADES,
grades,
@@ -43,6 +46,18 @@ const gotGrades = (grades, cohort, track, assignmentType, headings, prev, next,
courseId,
});
const gotGradeOverrideHistory = ({
overrideHistory, currentEarnedAllOverride, currentPossibleAllOverride,
currentEarnedGradedOverride, currentPossibleGradedOverride,
}) => ({
type: GOT_GRADE_OVERRIDE_HISTORY,
overrideHistory,
currentEarnedAllOverride,
currentPossibleAllOverride,
currentEarnedGradedOverride,
currentPossibleGradedOverride,
});
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
const gradeUpdateSuccess = (courseId, responseData) => ({
type: GRADE_UPDATE_SUCCESS,
@@ -101,6 +116,31 @@ const fetchGrades = (
}
);
const formatGradeOverrideForDisplay = historyArray => historyArray.map(item => ({
date: formatDateForDisplay(new Date(item.history_date)),
grader: item.history_user,
reason: item.override_reason,
adjustedGrade: item.earned_graded_override,
}));
const fetchGradeOverrideHistory = (subsectionId, userId) => (
dispatch =>
LmsApiService.fetchGradeOverrideHistory(subsectionId, userId)
.then(response => response.data)
.then((data) => {
dispatch(gotGradeOverrideHistory({
overrideHistory: formatGradeOverrideForDisplay(data.history),
currentEarnedAllOverride: data.override.earned_all_override,
currentPossibleAllOverride: data.override.possible_all_override,
currentEarnedGradedOverride: data.override.earned_graded_override,
currentPossibleGradedOverride: data.override.possible_graded_override,
}));
})
.catch(() => {
dispatch(errorFetchingGradeOverrideHistory());
})
);
const fetchMatchingUserGrades = (
courseId,
searchText,
@@ -201,4 +241,5 @@ export {
closeBanner,
submitFileUploadFormData,
fetchBulkUpgradeHistory,
fetchGradeOverrideHistory,
};

View File

@@ -1,3 +1,9 @@
const formatDateForDisplay = (inputDate) => {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
const timeOptions = { hour: '2-digit', minute: '2-digit' };
return `${inputDate.toLocaleDateString('en-US', options)} at ${inputDate.toLocaleTimeString('en-US', timeOptions)}`;
};
const sortAlphaAsc = (gradeRowA, gradeRowB) => {
const a = gradeRowA.username.toUpperCase();
const b = gradeRowB.username.toUpperCase();
@@ -34,5 +40,5 @@ const headingMapper = (filterKey) => {
};
};
export { headingMapper, sortAlphaAsc };
export { headingMapper, sortAlphaAsc, formatDateForDisplay };

View File

@@ -2,6 +2,8 @@ const STARTED_FETCHING_GRADES = 'STARTED_FETCHING_GRADES';
const FINISHED_FETCHING_GRADES = 'FINISHED_FETCHING_GRADES';
const ERROR_FETCHING_GRADES = 'ERROR_FETCHING_GRADES';
const GOT_GRADES = 'GOT_GRADES';
const GOT_GRADE_OVERRIDE_HISTORY = 'GOT_GRADE_OVERRIDE_HISTORY';
const ERROR_FETCHING_GRADE_OVERRIDE_HISTORY = 'ERROR_FETCHING_GRADE_OVERRIDE_HISTORY';
const GRADE_UPDATE_REQUEST = 'GRADE_UPDATE_REQUEST';
const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
@@ -35,4 +37,6 @@ export {
UPLOAD_ERR,
GOT_BULK_HISTORY,
BULK_HISTORY_ERR,
GOT_GRADE_OVERRIDE_HISTORY,
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
};

View File

@@ -10,10 +10,17 @@ import {
UPLOAD_COMPLETE,
UPLOAD_ERR,
GOT_BULK_HISTORY,
GOT_GRADE_OVERRIDE_HISTORY,
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
} from '../constants/actionTypes/grades';
const initialState = {
results: [],
gradeOverrideHistoryResults: [],
gradeOverrideCurrentEarnedAllOverride: null,
gradeOverrideCurrentPossibleAllOverride: null,
gradeOverrideCurrentEarnedGradedOverride: null,
gradeOverrideCurrentPossibleGradedOverride: null,
headings: [],
startedFetching: false,
finishedFetching: false,
@@ -43,6 +50,23 @@ const grades = (state = initialState, action) => {
showSpinner: false,
courseId: action.courseId,
};
case GOT_GRADE_OVERRIDE_HISTORY:
return {
...state,
gradeOverrideHistoryResults: action.overrideHistory,
gradeOverrideCurrentEarnedAllOverride: action.currentEarnedAllOverride,
gradeOverrideCurrentPossibleAllOverride: action.currentPossibleAllOverride,
gradeOverrideCurrentEarnedGradedOverride: action.currentEarnedGradedOverride,
gradeOverrideCurrentPossibleGradedOverride: action.currentPossibleGradedOverride,
};
case ERROR_FETCHING_GRADE_OVERRIDE_HISTORY:
return {
...state,
finishedFetchingOverrideHistory: true,
errorFetchingOverrideHistory: true,
};
case STARTED_FETCHING_GRADES:
return {
...state,

View File

@@ -39,7 +39,8 @@ class LmsApiService {
"earned_all_override": 11,
"possible_all_override": 11,
"earned_graded_override": 11,
"possible_graded_override": 11
"possible_graded_override": 11,
"comment": "reason for override"
}
}
]
@@ -92,6 +93,11 @@ class LmsApiService {
const url = `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}/history/`;
return apiClient.get(url).then(response => response.data).catch(() => Promise.reject(Error('unhandled response error')));
}
static fetchGradeOverrideHistory(subsectionId, userId) {
const historyUrl = `${LmsApiService.baseUrl}/api/grades/v1/subsection/${subsectionId}/?user_id=${userId}&history_record_limit=5`;
return apiClient.get(historyUrl);
}
}
export default LmsApiService;