diff --git a/src/components/Gradebook/index.jsx b/src/components/Gradebook/index.jsx index 7253000..bb2a5a8 100644 --- a/src/components/Gradebook/index.jsx +++ b/src/components/Gradebook/index.jsx @@ -487,6 +487,14 @@ export default class Gradebook extends React.Component { buttonType="primary" onClick={this.handleClickImportGrades} /> + )} @@ -513,6 +521,7 @@ Gradebook.defaultProps = { tracks: [], bulkImportError: '', showBulkManagement: false, + bulkManagementHistory: [], }; Gradebook.propTypes = { @@ -565,4 +574,9 @@ Gradebook.propTypes = { submitFileUploadFormData: PropTypes.func.isRequired, bulkImportError: PropTypes.string, showBulkManagement: PropTypes.bool, + bulkManagementHistory: PropTypes.arrayOf(PropTypes.shape({ + operation: PropTypes.oneOf(['commit', 'error']), + user: PropTypes.string, + modified: PropTypes.string, + })), }; diff --git a/src/containers/GradebookPage/index.jsx b/src/containers/GradebookPage/index.jsx index 76524e6..ab362fc 100644 --- a/src/containers/GradebookPage/index.jsx +++ b/src/containers/GradebookPage/index.jsx @@ -13,7 +13,8 @@ import { } from '../../data/actions/grades'; import { fetchCohorts } from '../../data/actions/cohorts'; import { fetchTracks } from '../../data/actions/tracks'; -import hasMastersTrack from '../../data/selectors/tracks'; +import stateHasMastersTrack from '../../data/selectors/tracks'; +import getBulkManagementHistory from '../../data/selectors/grades'; import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes'; import { getRoles } from '../../data/actions/roles'; import LmsApiService from '../../data/services/LmsApiService'; @@ -53,7 +54,8 @@ const mapStateToProps = (state, ownProps) => ( state.grades.bulkManagement.errorMessages ? `Errors while processing: ${state.grades.bulkManagement.errorMessages.join(', ')}` : '', - showBulkManagement: hasMastersTrack(state), + showBulkManagement: stateHasMastersTrack(state), + bulkManagementHistory: getBulkManagementHistory(state), } ); diff --git a/src/data/actions/grades.js b/src/data/actions/grades.js index 20a98a5..307d48b 100644 --- a/src/data/actions/grades.js +++ b/src/data/actions/grades.js @@ -13,6 +13,8 @@ import { START_UPLOAD, UPLOAD_COMPLETE, UPLOAD_ERR, + GOT_BULK_HISTORY, + BULK_HISTORY_ERR, } from '../constants/actionTypes/grades'; import LmsApiService from '../services/LmsApiService'; import { headingMapper, sortAlphaAsc } from './utils'; @@ -23,6 +25,8 @@ const defaultAssignmentFilter = 'All'; const startedCsvUpload = () => ({ type: START_UPLOAD }); const finishedCsvUpload = () => ({ type: UPLOAD_COMPLETE }); const csvUploadError = data => ({ type: UPLOAD_ERR, data }); +const gotBulkHistory = data => ({ type: GOT_BULK_HISTORY, data }); +const bulkHistoryError = () => ({ type: BULK_HISTORY_ERR }); const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES }); const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES }); @@ -163,7 +167,7 @@ const submitFileUploadFormData = (courseId, formData) => ( return LmsApiService.uploadGradeCsv(courseId, formData).then(() => ( dispatch(finishedCsvUpload()) )).catch((err) => { - if (err.status === 200 && err.data.error_messages.length) { + if (err.response.status === 200 && err.response.error_messages.length) { const { error_messages: errorMessages, saved, total } = err.data; return dispatch(csvUploadError({ errorMessages, saved, total })); } @@ -172,6 +176,14 @@ const submitFileUploadFormData = (courseId, formData) => ( } ); +const fetchBulkUpgradeHistory = courseId => ( + dispatch => + // todo add loading effect + LmsApiService.fetchGradeBulkOperationHistory(courseId).then((response) => { + dispatch(gotBulkHistory(response)); + }).catch(() => dispatch(bulkHistoryError())) +); + export { startedFetchingGrades, finishedFetchingGrades, @@ -188,4 +200,5 @@ export { filterColumns, closeBanner, submitFileUploadFormData, + fetchBulkUpgradeHistory, }; diff --git a/src/data/actions/tracks.js b/src/data/actions/tracks.js index 81f43cc..39eb79a 100644 --- a/src/data/actions/tracks.js +++ b/src/data/actions/tracks.js @@ -3,6 +3,8 @@ import { GOT_TRACKS, ERROR_FETCHING_TRACKS, } from '../constants/actionTypes/tracks'; +import { hasMastersTrack } from '../selectors/tracks'; +import { fetchBulkUpgradeHistory } from './grades'; import LmsApiService from '../services/LmsApiService'; const startedFetchingTracks = () => ({ type: STARTED_FETCHING_TRACKS }); @@ -16,6 +18,9 @@ const fetchTracks = courseId => ( .then(response => response.data) .then((data) => { dispatch(gotTracks(data.course_modes)); + if (hasMastersTrack(data.course_modes)) { + dispatch(fetchBulkUpgradeHistory(courseId)); + } }) .catch(() => { dispatch(errorFetchingTracks()); diff --git a/src/data/constants/actionTypes/grades.js b/src/data/constants/actionTypes/grades.js index 77921f7..6838397 100644 --- a/src/data/constants/actionTypes/grades.js +++ b/src/data/constants/actionTypes/grades.js @@ -15,6 +15,8 @@ const OPEN_BANNER = 'OPEN_BANNER'; const START_UPLOAD = 'START_UPLOAD'; const UPLOAD_COMPLETE = 'UPLOAD_COMPLETE'; const UPLOAD_ERR = 'UPLOAD_ERR'; +const GOT_BULK_HISTORY = 'GOT_BULK_HISTORY'; +const BULK_HISTORY_ERR = 'BULK_HISTORY_ERR'; export { STARTED_FETCHING_GRADES, @@ -31,4 +33,6 @@ export { START_UPLOAD, UPLOAD_COMPLETE, UPLOAD_ERR, + GOT_BULK_HISTORY, + BULK_HISTORY_ERR, }; diff --git a/src/data/reducers/grades.js b/src/data/reducers/grades.js index e6f7339..30acdbb 100644 --- a/src/data/reducers/grades.js +++ b/src/data/reducers/grades.js @@ -9,6 +9,7 @@ import { START_UPLOAD, UPLOAD_COMPLETE, UPLOAD_ERR, + GOT_BULK_HISTORY, } from '../constants/actionTypes/grades'; const initialState = { @@ -22,6 +23,7 @@ const initialState = { prevPage: null, nextPage: null, showSpinner: true, + bulkManagement: {}, }; const grades = (state = initialState, action) => { @@ -79,24 +81,35 @@ const grades = (state = initialState, action) => { ...state, showSpinner: true, }; - case UPLOAD_COMPLETE: + case UPLOAD_COMPLETE: { + const { errorMessages, ...rest } = state.bulkManagement; return { ...state, showSpinner: false, - bulkManagement: {}, + bulkManagement: { ...rest }, }; + } case UPLOAD_ERR: return { ...state, showSpinner: false, bulkManagement: { - ...(state.bulkManagement || {}), + ...state.bulkManagement, ...action.data, }, }; + case GOT_BULK_HISTORY: + return { + ...state, + bulkManagement: { + ...state.bulkManagement, + history: action.data, + }, + }; default: return state; } }; +export { initialState as initialGradesState }; export default grades; diff --git a/src/data/reducers/grades.test.js b/src/data/reducers/grades.test.js index 4a8ecb2..2cc3002 100644 --- a/src/data/reducers/grades.test.js +++ b/src/data/reducers/grades.test.js @@ -1,4 +1,4 @@ -import grades from './grades'; +import grades, { initialGradesState as initialState } from './grades'; import { STARTED_FETCHING_GRADES, ERROR_FETCHING_GRADES, @@ -8,19 +8,6 @@ import { OPEN_BANNER, } from '../constants/actionTypes/grades'; -const initialState = { - results: [], - headings: [], - startedFetching: false, - finishedFetching: false, - errorFetching: false, - gradeFormat: 'percent', - showSuccess: false, - prevPage: null, - nextPage: null, - showSpinner: true, -}; - const courseId = 'course-v1:edX+DemoX+Demo_Course'; const headingsData = [ { name: 'exam' }, diff --git a/src/data/selectors/grades.js b/src/data/selectors/grades.js new file mode 100644 index 0000000..28c71c5 --- /dev/null +++ b/src/data/selectors/grades.js @@ -0,0 +1,3 @@ +const getBulkManagementHistory = state => state.grades.bulkManagement.history; + +export default getBulkManagementHistory; diff --git a/src/data/selectors/tracks.js b/src/data/selectors/tracks.js index 6d755f6..d7e40b0 100644 --- a/src/data/selectors/tracks.js +++ b/src/data/selectors/tracks.js @@ -1,4 +1,13 @@ -const getTracks = state => state.tracks.results || []; -const hasMastersTrack = state => getTracks(state).some(track => track.slug === 'masters'); +const compose = (...fns) => { + const [firstFunc, ...rest] = fns.reverse(); + return (...args) => + rest.reduce((accum, fn) => fn(accum), firstFunc(...args)); +}; -export default hasMastersTrack; +const getTracks = state => state.tracks.results || []; +const trackIsMasters = track => track.slug === 'masters'; +const hasMastersTrack = tracks => tracks.some(trackIsMasters); +const stateHasMastersTrack = compose(hasMastersTrack, getTracks); + +export { hasMastersTrack, trackIsMasters }; +export default stateHasMastersTrack; diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js index b8852e0..1409c8d 100644 --- a/src/data/services/LmsApiService.js +++ b/src/data/services/LmsApiService.js @@ -72,7 +72,7 @@ class LmsApiService { const trackQueryParam = options.track ? [`track=${options.track}`] : []; const cohortQueryParam = options.cohort ? [`cohort=${options.cohort}`] : []; const queryParams = [...trackQueryParam, ...cohortQueryParam].join('&'); - const downloadUrl = `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}?${queryParams}`; + const downloadUrl = `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}/?${queryParams}`; return downloadUrl; } @@ -87,6 +87,11 @@ class LmsApiService { return Promise.reject(result); }); } + + static fetchGradeBulkOperationHistory(courseId) { + 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'))); + } } export default LmsApiService;