diff --git a/src/components/Gradebook/gradebook.scss b/src/components/Gradebook/gradebook.scss
index 489dbee..f93c7a1 100644
--- a/src/components/Gradebook/gradebook.scss
+++ b/src/components/Gradebook/gradebook.scss
@@ -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;
}
\ No newline at end of file
diff --git a/src/components/Gradebook/index.jsx b/src/components/Gradebook/index.jsx
index bb2a5a8..1531fc3 100644
--- a/src/components/Gradebook/index.jsx
+++ b/src/components/Gradebook/index.jsx
@@ -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: (
-
- this.setState({ updateVal: event.target.value })}
- />{adjustedGradePossible}
-
- ),
- 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 => (
{entry.username}
@@ -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={(
-
{this.state.modalModel[0].assignmentName}
-
+ Assignment:
{this.state.modalAssignmentName}
+ Student:
{this.state.updateUserName}
+ Original Grade:
{this.state.originalGrade}
+ Current Grade:
{this.props.gradeOverrideCurrentPossibleGradedOverride}
+
+
+ { !this.props.errorFetchingGradeOverrideHistory && (
+ this.onChange(value)}
+ ref={(input) => { this.overrideReasonInput = input; }}
+ />),
+ adjustedGrade: (
+
+ this.onChange(value)}
+ /> {this.state.adjustedGradePossible}
+ ),
+ }]}
+ />)}
+
+ Showing most recent actions(max 5). To see more, please contact
+ support.
+
Note: Once you save, your changes will be visible to students.
)}
@@ -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: '',
})}
/>
@@ -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']),
diff --git a/src/containers/GradebookPage/index.jsx b/src/containers/GradebookPage/index.jsx
index ab362fc..f47bd1c 100644
--- a/src/containers/GradebookPage/index.jsx
+++ b/src/containers/GradebookPage/index.jsx
@@ -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,
diff --git a/src/data/actions/grades.js b/src/data/actions/grades.js
index 307d48b..8edeb77 100644
--- a/src/data/actions/grades.js
+++ b/src/data/actions/grades.js
@@ -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,
};
diff --git a/src/data/actions/utils.js b/src/data/actions/utils.js
index 052b6de..970d6e2 100644
--- a/src/data/actions/utils.js
+++ b/src/data/actions/utils.js
@@ -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 };
diff --git a/src/data/constants/actionTypes/grades.js b/src/data/constants/actionTypes/grades.js
index 6838397..ab920d1 100644
--- a/src/data/constants/actionTypes/grades.js
+++ b/src/data/constants/actionTypes/grades.js
@@ -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,
};
diff --git a/src/data/reducers/grades.js b/src/data/reducers/grades.js
index 30acdbb..4eab724 100644
--- a/src/data/reducers/grades.js
+++ b/src/data/reducers/grades.js
@@ -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,
diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js
index 1409c8d..dbb614d 100644
--- a/src/data/services/LmsApiService.js
+++ b/src/data/services/LmsApiService.js
@@ -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;