Merge pull request #99 from edx/andytr1/gradebook-show-override-history
EDUCATOR-4353 - ui - show override history
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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']),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user