Added bulk management tab for CSV export/import

The bulk management tab will only be shown for masters courses,
i.e. those containing a masters track

JIRA:EDUCATOR-4343
JIRA:EDUCATOR-4431
This commit is contained in:
Matt Hughes
2019-05-28 15:22:55 -04:00
parent 1a0fe945a5
commit dee42eee7e
10 changed files with 270 additions and 148 deletions

View File

@@ -8,7 +8,11 @@ import {
GRADE_UPDATE_FAILURE,
TOGGLE_GRADE_FORMAT,
FILTER_COLUMNS,
UPDATE_BANNER,
OPEN_BANNER,
CLOSE_BANNER,
START_UPLOAD,
UPLOAD_COMPLETE,
UPLOAD_ERR,
} from '../constants/actionTypes/grades';
import LmsApiService from '../services/LmsApiService';
import { headingMapper, sortAlphaAsc } from './utils';
@@ -16,6 +20,10 @@ import apiClient from '../apiClient';
const defaultAssignmentFilter = 'All';
const startedCsvUpload = () => ({ type: START_UPLOAD });
const finishedCsvUpload = () => ({ type: UPLOAD_COMPLETE });
const csvUploadError = data => ({ type: UPLOAD_ERR, data });
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
@@ -53,12 +61,19 @@ const filterColumns = (filterType, exampleUser) => (
})
);
const updateBanner = showSuccess => ({ type: UPDATE_BANNER, showSuccess });
const openBanner = () => ({ type: OPEN_BANNER });
const closeBanner = () => ({ type: CLOSE_BANNER });
const fetchGrades = (courseId, cohort, track, assignmentType, showSuccess) => (
const fetchGrades = (
courseId,
cohort,
track,
assignmentType,
options = {},
) => (
(dispatch) => {
dispatch(startedFetchingGrades());
return LmsApiService.fetchGradebookData(courseId, null, cohort, track)
return LmsApiService.fetchGradebookData(courseId, options.searchText || null, cohort, track)
.then(response => response.data)
.then((data) => {
dispatch(gotGrades(
@@ -72,7 +87,9 @@ const fetchGrades = (courseId, cohort, track, assignmentType, showSuccess) => (
courseId,
));
dispatch(finishedFetchingGrades());
dispatch(updateBanner(!!showSuccess));
if (options.showSuccess) {
dispatch(openBanner());
}
})
.catch(() => {
dispatch(errorFetchingGrades());
@@ -87,30 +104,11 @@ const fetchMatchingUserGrades = (
track,
assignmentType,
showSuccess,
) => (
(dispatch) => {
dispatch(startedFetchingGrades());
return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track)
.then(response => response.data)
.then((data) => {
dispatch(gotGrades(
data.results.sort(sortAlphaAsc),
cohort,
track,
assignmentType,
headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
data.previous,
data.next,
courseId,
));
dispatch(finishedFetchingGrades());
dispatch(updateBanner(showSuccess));
})
.catch(() => {
dispatch(errorFetchingGrades());
});
}
);
options = {},
) => {
const newOptions = { ...options, searchText, showSuccess };
return fetchGrades(courseId, cohort, track, assignmentType, newOptions);
};
const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
(dispatch) => {
@@ -150,6 +148,7 @@ const updateGrades = (courseId, updateData, searchText, cohort, track) => (
track,
defaultAssignmentFilter,
true,
{ searchText },
));
})
.catch((error) => {
@@ -158,6 +157,21 @@ const updateGrades = (courseId, updateData, searchText, cohort, track) => (
}
);
const submitFileUploadFormData = (courseId, formData) => (
(dispatch) => {
dispatch(startedCsvUpload());
return LmsApiService.uploadGradeCsv(courseId, formData).then(() => (
dispatch(finishedCsvUpload())
)).catch((err) => {
if (err.status === 200 && err.data.error_messages.length) {
const { error_messages: errorMessages, saved, total } = err.data;
return dispatch(csvUploadError({ errorMessages, saved, total }));
}
return dispatch(csvUploadError({ errorMessages: ['Unknown error.'] }));
});
}
);
export {
startedFetchingGrades,
finishedFetchingGrades,
@@ -172,5 +186,6 @@ export {
updateGrades,
toggleGradeFormat,
filterColumns,
updateBanner,
closeBanner,
submitFileUploadFormData,
};

View File

@@ -10,7 +10,6 @@ import {
FINISHED_FETCHING_GRADES,
ERROR_FETCHING_GRADES,
GOT_GRADES,
UPDATE_BANNER,
} from '../constants/actionTypes/grades';
import { sortAlphaAsc } from './utils';
@@ -108,7 +107,6 @@ describe('actions', () => {
courseId,
},
{ type: FINISHED_FETCHING_GRADES },
{ type: UPDATE_BANNER, showSuccess: false },
];
const store = mockStore();
@@ -167,7 +165,6 @@ describe('actions', () => {
courseId,
},
{ type: FINISHED_FETCHING_GRADES },
{ type: UPDATE_BANNER, showSuccess: false },
];
const store = mockStore();

View File

@@ -9,7 +9,12 @@ const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT';
const FILTER_COLUMNS = 'FILTER_COLUMNS';
const UPDATE_BANNER = 'UPDATE_BANNER';
const CLOSE_BANNER = 'CLOSE_BANNER';
const OPEN_BANNER = 'OPEN_BANNER';
const START_UPLOAD = 'START_UPLOAD';
const UPLOAD_COMPLETE = 'UPLOAD_COMPLETE';
const UPLOAD_ERR = 'UPLOAD_ERR';
export {
STARTED_FETCHING_GRADES,
@@ -21,5 +26,9 @@ export {
GRADE_UPDATE_FAILURE,
TOGGLE_GRADE_FORMAT,
FILTER_COLUMNS,
UPDATE_BANNER,
OPEN_BANNER,
CLOSE_BANNER,
START_UPLOAD,
UPLOAD_COMPLETE,
UPLOAD_ERR,
};

View File

@@ -4,7 +4,11 @@ import {
GOT_GRADES,
TOGGLE_GRADE_FORMAT,
FILTER_COLUMNS,
UPDATE_BANNER,
OPEN_BANNER,
CLOSE_BANNER,
START_UPLOAD,
UPLOAD_COMPLETE,
UPLOAD_ERR,
} from '../constants/actionTypes/grades';
const initialState = {
@@ -60,10 +64,35 @@ const grades = (state = initialState, action) => {
...state,
headings: action.headings,
};
case UPDATE_BANNER:
case OPEN_BANNER:
return {
...state,
showSuccess: action.showSuccess,
showSuccess: true,
};
case CLOSE_BANNER:
return {
...state,
showSuccess: false,
};
case START_UPLOAD:
return {
...state,
showSpinner: true,
};
case UPLOAD_COMPLETE:
return {
...state,
showSpinner: false,
bulkManagement: {},
};
case UPLOAD_ERR:
return {
...state,
showSpinner: false,
bulkManagement: {
...(state.bulkManagement || {}),
...action.data,
},
};
default:
return state;

View File

@@ -5,7 +5,7 @@ import {
GOT_GRADES,
TOGGLE_GRADE_FORMAT,
FILTER_COLUMNS,
UPDATE_BANNER,
OPEN_BANNER,
} from '../constants/actionTypes/grades';
const initialState = {
@@ -158,8 +158,7 @@ describe('grades reducer', () => {
showSuccess: expectedShowSuccess,
};
expect(grades(undefined, {
type: UPDATE_BANNER,
showSuccess: expectedShowSuccess,
type: OPEN_BANNER,
})).toEqual(expected);
});

View File

@@ -0,0 +1,4 @@
const getTracks = state => state.tracks.results || [];
const hasMastersTrack = state => getTracks(state).some(track => track.slug === 'masters');
export default hasMastersTrack;

View File

@@ -67,6 +67,26 @@ class LmsApiService {
const rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(courseId)}`;
return apiClient.get(rolesUrl);
}
static getGradeExportCsvUrl(courseId, options = {}) {
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}`;
return downloadUrl;
}
static getGradeImportCsvUrl = LmsApiService.getGradeExportCsvUrl;
static uploadGradeCsv(courseId, formData) {
const fileUploadUrl = LmsApiService.getGradeImportCsvUrl(courseId);
return apiClient.post(fileUploadUrl, formData).then((result) => {
if (result.status === 200 && !result.data.error_messages.length) {
return result.data;
}
return Promise.reject(result);
});
}
}
export default LmsApiService;