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;