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:
@@ -15,6 +15,7 @@
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "NODE_ENV=development BABEL_ENV=development node_modules/.bin/webpack-dev-server --config=config/webpack.dev.config.js --progress",
|
||||
"test": "jest --coverage --passWithNoTests",
|
||||
"watch-tests": "jest --watch",
|
||||
"travis-deploy-once": "travis-deploy-once"
|
||||
},
|
||||
"author": "edX",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
StatusAlert,
|
||||
Table,
|
||||
Icon,
|
||||
Tabs,
|
||||
} from '@edx/paragon';
|
||||
import queryString from 'query-string';
|
||||
import { configuration } from '../../config';
|
||||
@@ -26,11 +27,13 @@ export default class Gradebook extends React.Component {
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
};
|
||||
this.fileFormRef = React.createRef();
|
||||
this.fileInputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const urlQuery = queryString.parse(this.props.location.search);
|
||||
this.props.getRoles(this.props.match.params.courseId, urlQuery);
|
||||
this.props.getRoles(this.props.courseId, urlQuery);
|
||||
}
|
||||
|
||||
setNewModalState = (userEntry, subsection) => {
|
||||
@@ -68,9 +71,16 @@ export default class Gradebook extends React.Component {
|
||||
</div>
|
||||
)
|
||||
|
||||
getActiveTabs = () => {
|
||||
if (this.props.showBulkManagement) {
|
||||
return ['Grades', 'Bulk Management'];
|
||||
}
|
||||
return ['Grades'];
|
||||
};
|
||||
|
||||
handleAdjustedGradeClick = () => {
|
||||
this.props.updateGrades(
|
||||
this.props.match.params.courseId, [
|
||||
this.props.courseId, [
|
||||
{
|
||||
user_id: this.state.updateUserId,
|
||||
usage_id: this.state.updateModuleId,
|
||||
@@ -138,7 +148,7 @@ export default class Gradebook extends React.Component {
|
||||
selectedTrackSlug = selectedTrackItem.slug;
|
||||
}
|
||||
this.props.getUserGrades(
|
||||
this.props.match.params.courseId,
|
||||
this.props.courseId,
|
||||
this.props.selectedCohort,
|
||||
selectedTrackSlug,
|
||||
this.props.selectedAssignmentType,
|
||||
@@ -154,7 +164,7 @@ export default class Gradebook extends React.Component {
|
||||
selectedCohortId = selectedCohortItem.id;
|
||||
}
|
||||
this.props.getUserGrades(
|
||||
this.props.match.params.courseId,
|
||||
this.props.courseId,
|
||||
selectedCohortId,
|
||||
this.props.selectedTrack,
|
||||
this.props.selectedAssignmentType,
|
||||
@@ -162,6 +172,29 @@ export default class Gradebook extends React.Component {
|
||||
this.updateQueryParams('cohort', selectedCohortId);
|
||||
};
|
||||
|
||||
handleClickExportGrades = () => {
|
||||
window.location = this.props.gradeExportUrl;
|
||||
};
|
||||
|
||||
handleClickImportGrades = () => {
|
||||
const fileInput = this.fileInputRef.current;
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
};
|
||||
|
||||
handleFileInputChange = (event) => {
|
||||
const fileInput = event.target;
|
||||
const file = fileInput.files[0];
|
||||
const form = this.fileFormRef.current;
|
||||
if (file && form) {
|
||||
const formData = new FormData(form);
|
||||
this.props.submitFileUploadFormData(this.props.courseId, formData).then(() => {
|
||||
fileInput.value = null;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
mapSelectedCohortEntry = (entry) => {
|
||||
const selectedCohortEntry = this.props.cohorts.find(x => x.id === parseInt(entry, 10));
|
||||
if (selectedCohortEntry) {
|
||||
@@ -265,13 +298,13 @@ export default class Gradebook extends React.Component {
|
||||
<div className="gradebook-container">
|
||||
<div>
|
||||
<a
|
||||
href={this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}
|
||||
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
|
||||
className="mb-3"
|
||||
>
|
||||
<span aria-hidden="true">{'<< '}</span> {'Back to Dashboard'}
|
||||
</a>
|
||||
<h1>Gradebook</h1>
|
||||
<h3> {this.props.match.params.courseId}</h3>
|
||||
<h3> {this.props.courseId}</h3>
|
||||
{ this.props.areGradesFrozen &&
|
||||
<div className="alert alert-warning" role="alert" >
|
||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||
@@ -352,13 +385,10 @@ export default class Gradebook extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginLeft: '10px', marginBottom: '10px' }}>
|
||||
<a className="btn btn-outline-primary mb-85" href={`${this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}#view-data_download`}>Generate Grade Report</a>
|
||||
</div>
|
||||
<SearchField
|
||||
onSubmit={value =>
|
||||
this.props.searchForUser(
|
||||
this.props.match.params.courseId,
|
||||
this.props.courseId,
|
||||
value,
|
||||
this.props.selectedCohort,
|
||||
this.props.selectedTrack,
|
||||
@@ -369,7 +399,7 @@ export default class Gradebook extends React.Component {
|
||||
onChange={filterValue => this.setState({ filterValue })}
|
||||
onClear={() =>
|
||||
this.props.getUserGrades(
|
||||
this.props.match.params.courseId,
|
||||
this.props.courseId,
|
||||
this.props.selectedCohort,
|
||||
this.props.selectedTrack,
|
||||
this.props.selectedAssignmentType,
|
||||
@@ -380,55 +410,85 @@ export default class Gradebook extends React.Component {
|
||||
<small className="form-text text-muted search-help-text">Search by username, email, or student key</small>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<StatusAlert
|
||||
alertType="success"
|
||||
dialog="The grade has been successfully edited."
|
||||
onClose={() => this.props.updateBanner(false)}
|
||||
open={this.props.showSuccess}
|
||||
/>
|
||||
{PageButtons(this.props)}
|
||||
<div className="gbook">
|
||||
<Table
|
||||
className={['table-striped']}
|
||||
columns={this.formatHeadings()}
|
||||
data={this.formatter[this.props.format](
|
||||
this.props.grades,
|
||||
this.props.areGradesFrozen,
|
||||
)}
|
||||
rowHeaderColumnKey="username"
|
||||
/>
|
||||
</div>
|
||||
{PageButtons(this.props)}
|
||||
<Modal
|
||||
open={this.state.modalOpen}
|
||||
title="Edit Grades"
|
||||
closeText="Cancel"
|
||||
body={(
|
||||
<div>
|
||||
<h3>{this.state.modalModel[0].assignmentName}</h3>
|
||||
<Tabs labels={this.getActiveTabs()}>
|
||||
<div>
|
||||
<StatusAlert
|
||||
alertType="success"
|
||||
dialog="The grade has been successfully edited."
|
||||
onClose={() => this.props.closeBanner()}
|
||||
open={this.props.showSuccess}
|
||||
/>
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
|
||||
data={this.state.modalModel}
|
||||
columns={this.formatHeadings()}
|
||||
data={this.formatter[this.props.format](
|
||||
this.props.grades,
|
||||
this.props.areGradesFrozen,
|
||||
)}
|
||||
rowHeaderColumnKey="username"
|
||||
/>
|
||||
<div>Note: Once you save, your changes will be visible to students.</div>
|
||||
</div>
|
||||
)}
|
||||
buttons={[
|
||||
<Button
|
||||
label="Save Grade"
|
||||
buttonType="primary"
|
||||
onClick={this.handleAdjustedGradeClick}
|
||||
/>,
|
||||
]}
|
||||
onClose={() => this.setState({
|
||||
modalOpen: false,
|
||||
modalModel: [{}],
|
||||
updateVal: 0,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
})}
|
||||
/>
|
||||
{PageButtons(this.props)}
|
||||
<Modal
|
||||
open={this.state.modalOpen}
|
||||
title="Edit Grades"
|
||||
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>Note: Once you save, your changes will be visible to students.</div>
|
||||
</div>
|
||||
)}
|
||||
buttons={[
|
||||
<Button
|
||||
label="Save Grade"
|
||||
buttonType="primary"
|
||||
onClick={this.handleAdjustedGradeClick}
|
||||
/>,
|
||||
]}
|
||||
onClose={() => this.setState({
|
||||
modalOpen: false,
|
||||
modalModel: [{}],
|
||||
updateVal: 0,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{this.props.showBulkManagement && (
|
||||
<div>
|
||||
<form ref={this.fileFormRef} action={this.props.gradeExportUrl} method="post">
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog={this.props.bulkImportError}
|
||||
open={this.props.bulkImportError}
|
||||
dismissible={false}
|
||||
/>
|
||||
<input
|
||||
className="d-none"
|
||||
type="file"
|
||||
name="csv"
|
||||
label="Upload Grade CSV"
|
||||
onChange={this.handleFileInputChange}
|
||||
ref={this.fileInputRef}
|
||||
/>
|
||||
</form>
|
||||
<Button
|
||||
label="Export Grades"
|
||||
buttonType="primary"
|
||||
onClick={this.handleClickExportGrades}
|
||||
/>
|
||||
<Button
|
||||
label="Import Grades"
|
||||
buttonType="primary"
|
||||
onClick={this.handleClickImportGrades}
|
||||
/>
|
||||
</div>)}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,16 +505,14 @@ Gradebook.defaultProps = {
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
match: {
|
||||
params: {
|
||||
courseId: '',
|
||||
},
|
||||
},
|
||||
courseId: '',
|
||||
selectedCohort: null,
|
||||
selectedTrack: null,
|
||||
selectedAssignmentType: 'All',
|
||||
showSpinner: false,
|
||||
tracks: [],
|
||||
bulkImportError: '',
|
||||
showBulkManagement: false,
|
||||
};
|
||||
|
||||
Gradebook.propTypes = {
|
||||
@@ -488,11 +546,7 @@ Gradebook.propTypes = {
|
||||
location: PropTypes.shape({
|
||||
search: PropTypes.string,
|
||||
}),
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
courseId: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
courseId: PropTypes.string,
|
||||
searchForUser: PropTypes.func.isRequired,
|
||||
selectedAssignmentType: PropTypes.string,
|
||||
selectedCohort: PropTypes.shape({
|
||||
@@ -505,6 +559,10 @@ Gradebook.propTypes = {
|
||||
tracks: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
})),
|
||||
updateBanner: PropTypes.func.isRequired,
|
||||
closeBanner: PropTypes.func.isRequired,
|
||||
updateGrades: PropTypes.func.isRequired,
|
||||
gradeExportUrl: PropTypes.string.isRequired,
|
||||
submitFileUploadFormData: PropTypes.func.isRequired,
|
||||
bulkImportError: PropTypes.string,
|
||||
showBulkManagement: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -8,12 +8,15 @@ import {
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
filterColumns,
|
||||
updateBanner,
|
||||
closeBanner,
|
||||
submitFileUploadFormData,
|
||||
} from '../../data/actions/grades';
|
||||
import { fetchCohorts } from '../../data/actions/cohorts';
|
||||
import { fetchTracks } from '../../data/actions/tracks';
|
||||
import hasMastersTrack from '../../data/selectors/tracks';
|
||||
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
||||
import { getRoles } from '../../data/actions/roles';
|
||||
import LmsApiService from '../../data/services/LmsApiService';
|
||||
|
||||
function shouldShowSpinner(state) {
|
||||
if (state.roles.canUserViewGradebook === true) {
|
||||
@@ -24,8 +27,9 @@ function shouldShowSpinner(state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => (
|
||||
const mapStateToProps = (state, ownProps) => (
|
||||
{
|
||||
courseId: ownProps.match.params.courseId,
|
||||
grades: state.grades.results,
|
||||
headings: state.grades.headings,
|
||||
tracks: state.tracks.results,
|
||||
@@ -41,46 +45,32 @@ const mapStateToProps = state => (
|
||||
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
|
||||
showSpinner: shouldShowSpinner(state),
|
||||
canUserViewGradebook: state.roles.canUserViewGradebook,
|
||||
gradeExportUrl: LmsApiService.getGradeExportCsvUrl(ownProps.match.params.courseId, {
|
||||
cohort: state.grades.selectedCohort,
|
||||
track: state.grades.selectedTrack,
|
||||
}),
|
||||
bulkImportError: state.grades.bulkManagement &&
|
||||
state.grades.bulkManagement.errorMessages ?
|
||||
`Errors while processing: ${state.grades.bulkManagement.errorMessages.join(', ')}` :
|
||||
'',
|
||||
showBulkManagement: hasMastersTrack(state),
|
||||
}
|
||||
);
|
||||
|
||||
const mapDispatchToProps = dispatch => (
|
||||
{
|
||||
getUserGrades: (courseId, cohort, track, assignmentType) => {
|
||||
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
||||
},
|
||||
searchForUser: (courseId, searchText, cohort, track, assignmentType) => {
|
||||
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, assignmentType, false));
|
||||
},
|
||||
getPrevNextGrades: (endpoint, courseId, cohort, track, assignmentType) => {
|
||||
dispatch(fetchPrevNextGrades(endpoint, courseId, cohort, track, assignmentType));
|
||||
},
|
||||
getCohorts: (courseId) => {
|
||||
dispatch(fetchCohorts(courseId));
|
||||
},
|
||||
getTracks: (courseId) => {
|
||||
dispatch(fetchTracks(courseId));
|
||||
},
|
||||
getAssignmentTypes: (courseId) => {
|
||||
dispatch(fetchAssignmentTypes(courseId));
|
||||
},
|
||||
updateGrades: (courseId, updateData, searchText, cohort, track) => {
|
||||
dispatch(updateGrades(courseId, updateData, searchText, cohort, track));
|
||||
},
|
||||
toggleFormat: (formatType) => {
|
||||
dispatch(toggleGradeFormat(formatType));
|
||||
},
|
||||
filterColumns: (filterType, exampleUser) => {
|
||||
dispatch(filterColumns(filterType, exampleUser));
|
||||
},
|
||||
updateBanner: (showSuccess) => {
|
||||
dispatch(updateBanner(showSuccess));
|
||||
},
|
||||
getRoles: (matchParams, urlQuery) => {
|
||||
dispatch(getRoles(matchParams, urlQuery));
|
||||
},
|
||||
}
|
||||
);
|
||||
const mapDispatchToProps = {
|
||||
getUserGrades: fetchGrades,
|
||||
searchForUser: fetchMatchingUserGrades,
|
||||
getPrevNextGrades: fetchPrevNextGrades,
|
||||
getCohorts: fetchCohorts,
|
||||
getTracks: fetchTracks,
|
||||
getAssignmentTypes: fetchAssignmentTypes,
|
||||
updateGrades,
|
||||
toggleFormat: toggleGradeFormat,
|
||||
filterColumns,
|
||||
closeBanner,
|
||||
getRoles,
|
||||
submitFileUploadFormData,
|
||||
};
|
||||
|
||||
const GradebookPage = connect(
|
||||
mapStateToProps,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
4
src/data/selectors/tracks.js
Normal file
4
src/data/selectors/tracks.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const getTracks = state => state.tracks.results || [];
|
||||
const hasMastersTrack = state => getTracks(state).some(track => track.slug === 'masters');
|
||||
|
||||
export default hasMastersTrack;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user