update actions to use redux toolkit action creators

This commit is contained in:
Ben Warzeski
2021-05-07 00:29:08 -04:00
parent 1f93215648
commit b2a737e936
41 changed files with 716 additions and 892 deletions

View File

@@ -12,8 +12,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import selectors from 'data/selectors';
import { configuration } from '../../config';
import { submitFileUploadFormData } from '../../data/actions/grades';
import { configuration } from 'config';
import { submitFileUploadFormData } from 'data/thunkActions/grades';
export class BulkManagement extends React.Component {
constructor(props) {

View File

@@ -11,10 +11,7 @@ import {
} from '@edx/paragon';
import selectors from 'data/selectors';
import {
doneViewingAssignment,
updateGrades,
} from '../../data/actions/grades';
import * as actions from 'data/actions';
const GRADE_OVERRIDE_HISTORY_COLUMNS = [
{ label: 'Date', key: 'date' },
@@ -203,8 +200,8 @@ export const mapStateToProps = (state) => {
};
export const mapDispatchToProps = {
doneViewingAssignment,
updateGrades,
doneViewingAssignment: actions.grades.doneViewingAssignment,
updateGrades: actions.grades.updateGrades,
};
export default connect(mapStateToProps, mapDispatchToProps)(EditModal);

View File

@@ -3,9 +3,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import * as gradesActions from 'data/actions/grades';
import * as filterActions from 'data/actions/filters';
import selectors from 'data/selectors';
import * as actions from 'data/actions';
import { updateGradesIfAssignmentGradeFiltersSet } from 'data/thunkActions/grades';
import SelectGroup from '../SelectGroup';
@@ -97,8 +97,8 @@ export const mapStateToProps = (state) => {
};
export const mapDispatchToProps = {
updateAssignmentFilter: filterActions.updateAssignmentFilter,
updateGradesIfAssignmentGradeFiltersSet: gradesActions.updateGradesIfAssignmentGradeFiltersSet,
updateAssignmentFilter: actions.filters.update.assignment,
updateGradesIfAssignmentGradeFiltersSet,
};
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentFilter);

View File

@@ -5,9 +5,8 @@ import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
import * as gradesActions from 'data/actions/grades';
import * as filterActions from 'data/actions/filters';
import selectors from 'data/selectors';
import * as actions from 'data/actions';
import PercentGroup from '../PercentGroup';
@@ -118,8 +117,8 @@ export const mapStateToProps = (state) => {
};
export const mapDispatchToProps = {
getUserGrades: gradesActions.fetchGrades,
updateAssignmentLimits: filterActions.updateAssignmentLimits,
getUserGrades: fetchGrades,
updateAssignmentLimits: actions.filters.update.assignmentLimits,
};
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentGradeFilter);

View File

@@ -3,8 +3,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
<<<<<<< HEAD
import * as gradesActions from 'data/actions/grades';
import selectors from 'data/selectors';
=======
import { selectableAssignmentLabels } from 'data/selectors/filters';
import * as actions from 'data/actions';
>>>>>>> 5694a3b... update actions to use redux toolkit action creators
import SelectGroup from '../SelectGroup';
@@ -72,7 +77,7 @@ export const mapStateToProps = (state) => ({
});
export const mapDispatchToProps = {
filterAssignmentType: gradesActions.filterAssignmentType,
filterAssignmentType: actions.filters.update.assignmentType,
};
export default connect(mapStateToProps, mapDispatchToProps)(AssignmentTypeFilter);

View File

@@ -6,9 +6,8 @@ import {
Button,
} from '@edx/paragon';
import { updateCourseGradeFilter } from 'data/actions/filters';
import { fetchGrades } from 'data/actions/grades';
import selectors from 'data/selectors';
import actions from 'data/actions';
import PercentGroup from '../PercentGroup';
export class CourseGradeFilter extends React.Component {
@@ -37,11 +36,11 @@ export class CourseGradeFilter extends React.Component {
updateCourseGradeFilters() {
const { courseGradeMin, courseGradeMax } = this.props.filterValues;
this.props.updateFilter(
this.props.updateFilter({
courseGradeMin,
courseGradeMax,
this.props.courseId,
);
courseId: this.props.courseId,
});
this.props.getUserGrades(
this.props.courseId,
this.props.selectedCohort,
@@ -129,7 +128,7 @@ export const mapStateToProps = (state) => {
};
export const mapDispatchToProps = {
updateFilter: updateCourseGradeFilter,
updateFilter: actions.filter.update.courseGrade,
getUserGrades: fetchGrades,
};

View File

@@ -3,7 +3,6 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchGrades } from 'data/actions/grades';
import selectors from 'data/selectors';
import SelectGroup from '../SelectGroup';

View File

@@ -7,10 +7,10 @@ import {
Table, OverlayTrigger, Tooltip, Icon,
} from '@edx/paragon';
import { formatDateForDisplay } from '../../data/actions/utils';
import { fetchGradeOverrideHistory } from '../../data/actions/grades';
import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../../data/constants/grades';
import selectors from '../../data/selectors';
import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from 'data/constants/grades';
import selectors from 'data/selectors';
import { formatDateForDisplay } from 'data/actions/utils';
import { fetchGradeOverrideHistory } from 'data/thunkActions/grades';
const DECIMAL_PRECISION = 2;
@@ -219,7 +219,7 @@ GradebookTable.propTypes = {
export const mapStateToProps = (state) => {
const { assignmentTypes, grades, root } = selectors;
return {
returg {
areGradesFrozen: assignmentTypes.areGradesFrozen(state),
format: grades.gradeFormat(state),
grades: grades.allGrades(state),

View File

@@ -8,7 +8,7 @@ import selectors from 'data/selectors';
import {
fetchGrades,
fetchMatchingUserGrades,
} from '../../data/actions/grades';
} from 'data/thunkActions/grades';
/**
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { StatusAlert } from '@edx/paragon';
import selectors from 'data/selectors';
import { closeBanner } from '../../data/actions/grades';
import * as actions from 'data/actions';
export const maxCourseGradeInvalidMessage = 'Maximum course grade value must be between 0 and 100. ';
export const minCourseGradeInvalidMessage = 'Minimum course grade value must be between 0 and 100. ';
@@ -66,7 +66,7 @@ export const mapStateToProps = (state) => ({
});
export const mapDispatchToProps = {
handleCloseSuccessBanner: closeBanner,
handleCloseSuccessBanner: actions.grades.banner.close,
};
export default connect(mapStateToProps, mapDispatchToProps)(StatusAlerts);

View File

@@ -1,27 +1,19 @@
import { connect } from 'react-redux';
import selectors from 'data/selectors';
import Gradebook from '../../components/Gradebook';
import { fetchAssignmentTypes } from 'data/thunkActions/assignmentTypes';
import {
fetchGradeOverrideHistory,
fetchGrades,
fetchPrevNextGrades,
filterAssignmentType,
submitFileUploadFormData,
toggleGradeFormat,
downloadBulkGradesReport,
downloadInterventionReport,
} from '../../data/actions/grades';
import { fetchCohorts } from '../../data/actions/cohorts';
import { fetchTracks } from '../../data/actions/tracks';
import {
initializeFilters,
resetFilters,
updateAssignmentFilter,
updateAssignmentLimits,
} from '../../data/actions/filters';
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
import { getRoles } from '../../data/actions/roles';
} from 'data/thunkActions/grades';
import { fetchCohorts } from 'data/thunkActions/cohorts';
import { fetchTracks } from 'data/thunkActions/tracks';
import { getRoles } from 'data/thunkActions/roles';
import * as actions from 'data/actions';
import selectors from 'data/selectors';
import Gradebook from 'components/Gradebook';
const mapStateToProps = (state, ownProps) => {
const {
@@ -61,22 +53,22 @@ const mapStateToProps = (state, ownProps) => {
};
const mapDispatchToProps = {
downloadBulkGradesReport,
downloadInterventionReport,
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
downloadInterventionReport: actions.grades.downloadReport.intervention,
fetchGradeOverrideHistory,
filterAssignmentType,
filterAssignmentType: actions.filter.update.assignmentType,
getAssignmentTypes: fetchAssignmentTypes,
getCohorts: fetchCohorts,
getPrevNextGrades: fetchPrevNextGrades,
getRoles,
getTracks: fetchTracks,
getUserGrades: fetchGrades,
initializeFilters,
resetFilters,
initializeFilters: actions.filters.initialize,
resetFilters: actions.filters.reset,
submitFileUploadFormData,
toggleFormat: toggleGradeFormat,
updateAssignmentFilter,
updateAssignmentLimits,
toggleFormat: actions.grades.toggleGradeFormat,
updateAssignmentFilter: actions.filters.update.assignment,
updateAssignmentLimits: actions.filters.update.assignmentLimits,
};
const GradebookPage = connect(

View File

@@ -1,40 +1,14 @@
import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';
import GOT_BULK_MANAGEMENT_CONFIG from '../constants/actionTypes/config';
import LmsApiService from '../services/LmsApiService';
import { createAction } from '@reduxjs/toolkit';
const startedFetchingAssignmentTypes = () => ({ type: STARTED_FETCHING_ASSIGNMENT_TYPES });
const errorFetchingAssignmentTypes = () => ({ type: ERROR_FETCHING_ASSIGNMENT_TYPES });
const gotAssignmentTypes = assignmentTypes => ({ type: GOT_ASSIGNMENT_TYPES, assignmentTypes });
const gotGradesFrozen = areGradesFrozen => ({ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen });
const gotBulkManagementConfig = bulkManagementEnabled => ({
type: GOT_BULK_MANAGEMENT_CONFIG,
data: bulkManagementEnabled,
});
const errorFetching = createAction('assignmentTypes/errorFetching');
const startedFetching = createAction('assignmentTypes/startedFetching');
const fetchAssignmentTypes = courseId => (
(dispatch) => {
dispatch(startedFetchingAssignmentTypes());
return LmsApiService.fetchAssignmentTypes(courseId)
.then(response => response.data)
.then((data) => {
dispatch(gotAssignmentTypes(Object.keys(data.assignment_types)));
dispatch(gotGradesFrozen(data.grades_frozen));
dispatch(gotBulkManagementConfig(data.can_see_bulk_management));
})
.catch(() => {
dispatch(errorFetchingAssignmentTypes());
});
}
);
const received = createAction('assignmentTypes/results'); // payload
const gotGradesFrozen = createAction('assignmentTypes/gotGradesFrozen'); // payload
export {
fetchAssignmentTypes,
startedFetchingAssignmentTypes,
gotAssignmentTypes,
errorFetchingAssignmentTypes,
errorFetching,
startedFetching,
received,
gotGradesFrozen,
};

View File

@@ -1,31 +1,12 @@
import {
STARTED_FETCHING_COHORTS,
GOT_COHORTS,
ERROR_FETCHING_COHORTS,
} from '../constants/actionTypes/cohorts';
import LmsApiService from '../services/LmsApiService';
import { createAction } from '@reduxjs/toolkit';
const startedFetchingCohorts = () => ({ type: STARTED_FETCHING_COHORTS });
const errorFetchingCohorts = () => ({ type: ERROR_FETCHING_COHORTS });
const gotCohorts = cohorts => ({ type: GOT_COHORTS, cohorts });
const fetchCohorts = courseId => (
(dispatch) => {
dispatch(startedFetchingCohorts());
return LmsApiService.fetchCohorts(courseId)
.then(response => response.data)
.then((data) => {
dispatch(gotCohorts(data.cohorts));
})
.catch(() => {
dispatch(errorFetchingCohorts());
});
}
);
const startedFetching = createAction('cohorts/startedFetching');
const errorFetching = createAction('cohorts/errorFetching');
const received = createAction('cohorts/received');
export {
fetchCohorts,
startedFetchingCohorts,
gotCohorts,
errorFetchingCohorts,
errorFetching,
received,
startedFetching,
};

View File

@@ -0,0 +1,8 @@
/* eslint-disable import/prefer-default-export */
import { createAction } from '@reduxjs/toolkit';
const gotBulkManagementConfig = createAction('config/gotBulkManagement'); // payload
export {
gotBulkManagementConfig,
};

View File

@@ -1,18 +1,7 @@
import filterSelectors from 'data/selectors/filters';
import { createAction } from '@reduxjs/toolkit';
import initialFilters from '../constants/filters';
import {
INITIALIZE_FILTERS,
RESET_FILTERS,
UPDATE_ASSIGNMENT_FILTER,
UPDATE_ASSIGNMENT_LIMITS,
UPDATE_COURSE_GRADE_LIMITS,
UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
} from '../constants/actionTypes/filters';
import { fetchGrades } from './grades';
const { allFilters } = filterSelectors;
const initializeFilters = ({
const initialize = createAction('filters/initialize', ({
assignment = initialFilters.assignment,
assignmentType = initialFilters.assignmentType,
track = initialFilters.track,
@@ -23,8 +12,7 @@ const initializeFilters = ({
courseGradeMax = initialFilters.assignmentGradeMax,
includeCourseRoleMembers = initialFilters.includeCourseRoleMembers,
}) => ({
type: INITIALIZE_FILTERS,
data: {
payload: {
assignment: { id: assignment },
assignmentType,
track,
@@ -35,47 +23,19 @@ const initializeFilters = ({
courseGradeMax,
includeCourseRoleMembers: Boolean(includeCourseRoleMembers),
},
});
}));
const resetFilters = filterNames => ({
type: RESET_FILTERS,
filterNames,
});
const updateAssignmentFilter = assignment => ({
type: UPDATE_ASSIGNMENT_FILTER,
data: assignment,
});
const updateAssignmentLimits = (minGrade, maxGrade) => ({
type: UPDATE_ASSIGNMENT_LIMITS,
data: { minGrade, maxGrade },
});
const updateCourseGradeFilter = (courseGradeMin, courseGradeMax, courseId) => ({
type: UPDATE_COURSE_GRADE_LIMITS,
data: {
courseGradeMin,
courseGradeMax,
courseId,
},
});
const updateIncludeCourseRoleMembersFilter = (includeCourseRoleMembers) => ({
type: UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
data: {
includeCourseRoleMembers,
},
});
const updateIncludeCourseRoleMembers = includeCourseRoleMembers => (dispatch, getState) => {
dispatch(updateIncludeCourseRoleMembersFilter(includeCourseRoleMembers));
const state = getState();
const { cohort, track, assignmentType } = allFilters(state);
dispatch(fetchGrades(state.grades.courseId, cohort, track, assignmentType));
const reset = createAction('filters/reset'); // paylaod
const update = {
assignment: createAction('filters/update/assignment'),
assignmentType: createAction('filters/update/assignmentType'),
assignmentLimits: createAction('filters/update/assignmentLimits'),
courseGrade: createAction('filters/update/courseGrade'),
includeCourseRoleMembers: createAction('filters/update/includeCourseRoleMembers'),
};
export {
initializeFilters, resetFilters, updateAssignmentFilter,
updateAssignmentLimits, updateCourseGradeFilter, updateIncludeCourseRoleMembers,
initialize,
reset,
update,
};

View File

@@ -1,359 +1,97 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import gradesSelectors from 'data/selectors/grades';
import filtersSelectors from 'data/selectors/filters';
import {
STARTED_FETCHING_GRADES,
FINISHED_FETCHING_GRADES,
ERROR_FETCHING_GRADES,
GOT_GRADES,
GRADE_UPDATE_REQUEST,
GRADE_UPDATE_SUCCESS,
GRADE_UPDATE_FAILURE,
TOGGLE_GRADE_FORMAT,
FILTER_BY_ASSIGNMENT_TYPE,
OPEN_BANNER,
CLOSE_BANNER,
START_UPLOAD,
UPLOAD_COMPLETE,
UPLOAD_ERR,
GOT_BULK_HISTORY,
BULK_HISTORY_ERR,
GOT_GRADE_OVERRIDE_HISTORY,
DONE_VIEWING_ASSIGNMENT,
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
UPLOAD_OVERRIDE,
UPLOAD_OVERRIDE_ERROR,
BULK_GRADE_REPORT_DOWNLOADED,
INTERVENTION_REPORT_DOWNLOADED,
} from '../constants/actionTypes/grades';
import GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG from '../constants/errors';
import LmsApiService from '../services/LmsApiService';
import { sortAlphaAsc, formatDateForDisplay } from './utils';
import { createAction } from '@reduxjs/toolkit';
const {
formatMaxAssignmentGrade,
formatMinAssignmentGrade,
formatMaxCourseGrade,
formatMinCourseGrade,
} = gradesSelectors;
const { allFilters } = filtersSelectors;
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 });
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
const errorFetchingGradeOverrideHistory = errorMessage => ({
type: ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
errorMessage,
});
const gotGrades = ({
grades, cohort, track, assignmentType, headings, prev,
next, courseId, totalUsersCount, filteredUsersCount,
}) => ({
type: GOT_GRADES,
grades,
cohort,
track,
assignmentType,
headings,
prev,
next,
courseId,
totalUsersCount,
filteredUsersCount,
});
const gotGradeOverrideHistory = ({
overrideHistory, currentEarnedAllOverride, currentPossibleAllOverride,
currentEarnedGradedOverride, currentPossibleGradedOverride,
originalGradeEarnedAll, originalGradePossibleAll, originalGradeEarnedGraded,
originalGradePossibleGraded,
}) => ({
type: GOT_GRADE_OVERRIDE_HISTORY,
overrideHistory,
currentEarnedAllOverride,
currentPossibleAllOverride,
currentEarnedGradedOverride,
currentPossibleGradedOverride,
originalGradeEarnedAll,
originalGradePossibleAll,
originalGradeEarnedGraded,
originalGradePossibleGraded,
});
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
const gradeUpdateSuccess = (courseId, responseData) => ({
type: GRADE_UPDATE_SUCCESS,
courseId,
payload: { responseData },
});
const gradeUpdateFailure = (courseId, error) => ({
type: GRADE_UPDATE_FAILURE,
courseId,
payload: { error },
});
const uploadOverrideSuccess = courseId => ({
type: UPLOAD_OVERRIDE,
courseId,
});
// This action for google analytics only. Doesn't change redux state.
const downloadBulkGradesReport = courseId => ({
type: BULK_GRADE_REPORT_DOWNLOADED,
courseId,
});
// This action for google analytics only. Doesn't change redux state.
const downloadInterventionReport = courseId => ({
type: INTERVENTION_REPORT_DOWNLOADED,
courseId,
});
const uploadOverrideFailure = (courseId, error) => ({
type: UPLOAD_OVERRIDE_ERROR,
courseId,
payload: { error },
});
const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType });
const filterAssignmentType = filterType => (
dispatch => dispatch({
type: FILTER_BY_ASSIGNMENT_TYPE,
filterType,
})
);
const openBanner = () => ({ type: OPEN_BANNER });
const closeBanner = () => ({ type: CLOSE_BANNER });
const fetchGrades = (
courseId,
cohort,
track,
assignmentType,
options = {},
) => (
(dispatch, getState) => {
dispatch(startedFetchingGrades());
const {
assignment,
assignmentGradeMax: assignmentMax,
assignmentGradeMin: assignmentMin,
courseGradeMin,
courseGradeMax,
includeCourseRoleMembers,
} = allFilters(getState());
const { id: assignmentId } = assignment || {};
const assignmentGradeMax = formatMaxAssignmentGrade(assignmentMax, { assignmentId });
const assignmentGradeMin = formatMinAssignmentGrade(assignmentMin, { assignmentId });
const courseGradeMinFormatted = formatMinCourseGrade(courseGradeMin);
const courseGradeMaxFormatted = formatMaxCourseGrade(courseGradeMax);
return LmsApiService.fetchGradebookData(
courseId,
options.searchText || null,
cohort,
track,
{
assignment: assignmentId,
assignmentGradeMax,
assignmentGradeMin,
courseGradeMin: courseGradeMinFormatted,
courseGradeMax: courseGradeMaxFormatted,
includeCourseRoleMembers,
const csvUpload = {
started: createAction('grades/csvUpload/started'),
finished: createAction('grades/csvUpload/finished'),
error: createAction('grades/csvUpload/error'),
};
const bulkHistory = {
received: createAction('grades/bulkHistory/received'),
error: createAction('grades/bulkHistory/error'),
};
const fetching = {
started: createAction('grades/fetching/started'),
finished: createAction('grades/fetching/finished'),
error: createAction('grades/fetching/error'),
};
const overrideHistory = {
errorFetching: createAction('grades/overrideHistory/errorFetching'),
received: createAction(
'grades/overrideHistory/received',
(data) => ({
payload: {
overrideHistory: data.overrideHistory,
currentEarnedAllOverride: data.currentEarnedAllOverride,
currentPossibleAllOverride: data.currentPossibleAllOverride,
currentEarnedGradedOverride: data.currentEarnedGradedOverride,
currentPossibleGradedOverride: data.currentPossibleGradedOverride,
originalGradeEarnedAll: data.originalGradeEarnedAll,
originalGradePossibleAll: data.originalGradePossibleAll,
originalGradeEarnedGraded: data.originalGradeEarnedGraded,
originalGradePossibleGraded: data.originalGradePossibleGraded,
},
)
.then(response => response.data)
.then((data) => {
dispatch(gotGrades({
grades: data.results.sort(sortAlphaAsc),
cohort,
track,
assignmentType,
prev: data.previous,
next: data.next,
courseId,
totalUsersCount: data.total_users_count,
filteredUsersCount: data.filtered_users_count,
}));
dispatch(finishedFetchingGrades());
if (options.showSuccess) {
dispatch(openBanner());
}
})
.catch(() => {
dispatch(errorFetchingGrades());
});
}
);
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 doneViewingAssignment = () => dispatch => dispatch({
type: DONE_VIEWING_ASSIGNMENT,
});
const fetchGradeOverrideHistory = (subsectionId, userId) => (
dispatch => LmsApiService.fetchGradeOverrideHistory(subsectionId, userId)
.then(response => response.data)
.then((data) => {
if (data.success) {
dispatch(gotGradeOverrideHistory({
overrideHistory: formatGradeOverrideForDisplay(data.history),
currentEarnedAllOverride: data.override ? data.override.earned_all_override : null,
currentPossibleAllOverride: data.override ? data.override.possible_all_override : null,
currentEarnedGradedOverride: data.override ? data.override.earned_graded_override : null,
currentPossibleGradedOverride: data.override
? data.override.possible_graded_override : null,
originalGradeEarnedAll: data.original_grade ? data.original_grade.earned_all : null,
originalGradePossibleAll: data.original_grade ? data.original_grade.possible_all : null,
originalGradeEarnedGraded: data.original_grade
? data.original_grade.earned_graded : null,
originalGradePossibleGraded: data.original_grade
? data.original_grade.possible_graded : null,
}));
} else {
dispatch(errorFetchingGradeOverrideHistory(data.error_message));
}
})
.catch(() => {
dispatch(errorFetchingGradeOverrideHistory(GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG));
})
);
const fetchMatchingUserGrades = (
courseId,
searchText,
cohort,
track,
assignmentType,
showSuccess,
options = {},
) => {
const newOptions = { ...options, searchText, showSuccess };
return fetchGrades(courseId, cohort, track, assignmentType, newOptions);
}),
),
};
const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
(dispatch) => {
dispatch(startedFetchingGrades());
return getAuthenticatedHttpClient().get(endpoint)
.then(response => response.data)
.then((data) => {
dispatch(gotGrades({
grades: data.results.sort(sortAlphaAsc),
cohort,
track,
assignmentType,
prev: data.previous,
next: data.next,
courseId,
totalUsersCount: data.total_users_count,
filteredUsersCount: data.filtered_users_count,
}));
dispatch(finishedFetchingGrades());
})
.catch(() => {
dispatch(errorFetchingGrades());
});
}
const received = createAction(
'grades/received',
(data) => ({
payload: {
grades: data.grades,
cohort: data.cohort,
track: data.track,
assignmentType: data.assignmentType,
headings: data.headings,
prev: data.prev,
next: data.next,
courseId: data.courseId,
totalUsersCount: data.totalUsersCount,
filteredUsersCount: data.filteredUsersCount,
},
}),
);
const updateGrades = (courseId, updateData, searchText, cohort, track) => (
(dispatch) => {
dispatch(gradeUpdateRequest());
return LmsApiService.updateGradebookData(courseId, updateData)
.then(response => response.data)
.then((data) => {
dispatch(gradeUpdateSuccess(courseId, data));
dispatch(fetchMatchingUserGrades(
courseId,
searchText,
cohort,
track,
defaultAssignmentFilter,
true,
{ searchText },
));
})
.catch((error) => {
dispatch(gradeUpdateFailure(courseId, error));
});
}
);
const submitFileUploadFormData = (courseId, formData) => (
(dispatch) => {
dispatch(startedCsvUpload());
return LmsApiService.uploadGradeCsv(courseId, formData).then(() => {
dispatch(finishedCsvUpload());
dispatch(uploadOverrideSuccess(courseId));
}).catch((err) => {
dispatch(uploadOverrideFailure(courseId, 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.'] }));
});
}
);
const fetchBulkUpgradeHistory = courseId => (
// todo add loading effect
dispatch => LmsApiService.fetchGradeBulkOperationHistory(courseId).then(
(response) => { dispatch(gotBulkHistory(response)); },
).catch(() => dispatch(bulkHistoryError()))
);
const updateGradesIfAssignmentGradeFiltersSet = (
courseId,
cohort,
track,
assignmentType,
) => (dispatch, getState) => {
const { filters } = getState();
const hasAssignmentGradeFiltersSet = filters.assignmentGradeMax || filters.assignmentGradeMin;
if (hasAssignmentGradeFiltersSet) {
dispatch(fetchGrades(
courseId,
cohort,
track,
assignmentType,
));
}
const update = {
request: createAction('grades/update/request'),
success: createAction('grades/update/success'),
failure: createAction('grades/update/failure', (courseId, error) => ({
payload: { courseId, error },
})),
};
const uploadOverride = {
success: createAction('grades/uploadOverride/success'),
failure: createAction('grades/uploadOverride/failure', (courseId, error) => ({
payload: { courseId, error },
})),
};
// These actions for google analytics only. Doesn't change redux state.
const downloadReport = {
bulkGrades: createAction('grades/downloadReport/bulkGrades'),
intervention: createAction('grades/downloadReport/intervention'),
};
const toggleGradeFormat = createAction('grades/toggleGradeFormat');
const banner = {
open: createAction('grades/banner/open'),
close: createAction('grades/banner/close'),
};
const doneViewingAssignment = createAction('grades/doneViewingAssignment');
export {
startedFetchingGrades,
finishedFetchingGrades,
errorFetchingGrades,
gotGrades,
fetchGrades,
fetchMatchingUserGrades,
fetchPrevNextGrades,
gradeUpdateRequest,
gradeUpdateSuccess,
gradeUpdateFailure,
updateGrades,
toggleGradeFormat,
filterAssignmentType,
closeBanner,
submitFileUploadFormData,
fetchBulkUpgradeHistory,
banner,
bulkHistory,
csvUpload,
doneViewingAssignment,
fetchGradeOverrideHistory,
updateGradesIfAssignmentGradeFiltersSet,
downloadBulkGradesReport,
downloadInterventionReport,
downloadReport,
fetching,
overrideHistory,
received,
toggleGradeFormat,
update,
uploadOverride,
};

15
src/data/actions/index.js Normal file
View File

@@ -0,0 +1,15 @@
import * as assignmentTypes from './assignmentTypes';
import * as cohorts from './cohorts';
import * as config from './config';
import * as grades from './grades';
import * as roles from './roles';
import * as tracks from './tracks';
export {
assignmentTypes,
cohorts,
config,
grades,
roles,
tracks,
};

View File

@@ -1,46 +1,9 @@
import filtersSelectors from 'data/selectors/filters';
import {
GOT_ROLES,
ERROR_FETCHING_ROLES,
} from '../constants/actionTypes/roles';
import { fetchGrades } from './grades';
import { fetchTracks } from './tracks';
import { fetchCohorts } from './cohorts';
import { fetchAssignmentTypes } from './assignmentTypes';
import LmsApiService from '../services/LmsApiService';
import { createAction } from '@reduxjs/toolkit';
const { allFilters } = filtersSelectors;
const allowedRoles = ['staff', 'instructor', 'support'];
const gotRoles = (canUserViewGradebook, courseId) => ({
type: GOT_ROLES,
canUserViewGradebook,
courseId,
});
const errorFetchingRoles = () => ({ type: ERROR_FETCHING_ROLES });
const getRoles = courseId => (
(dispatch, getState) => LmsApiService.fetchUserRoles(courseId)
.then(response => response.data)
.then((response) => {
const canUserViewGradebook = response.is_staff
|| (response.roles.some(role => (role.course_id === courseId)
&& allowedRoles.includes(role.role)));
dispatch(gotRoles(canUserViewGradebook, courseId));
const { cohort, track, assignmentType } = allFilters(getState());
if (canUserViewGradebook) {
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
dispatch(fetchTracks(courseId));
dispatch(fetchCohorts(courseId));
dispatch(fetchAssignmentTypes(courseId));
}
})
.catch(() => {
dispatch(errorFetchingRoles());
}));
const errorFetching = createAction('roles/errorFetching');
const received = createAction('roles/received');
export {
getRoles,
errorFetchingRoles,
errorFetching,
received,
};

View File

@@ -1,39 +1,12 @@
/* eslint-disable camelcase */
import tracksSelectors from 'data/selectors/tracks';
import {
STARTED_FETCHING_TRACKS,
GOT_TRACKS,
ERROR_FETCHING_TRACKS,
} from '../constants/actionTypes/tracks';
import { fetchBulkUpgradeHistory } from './grades';
import LmsApiService from '../services/LmsApiService';
/* eslint-disable import/prefer-default-export */
import { createAction } from '@reduxjs/toolkit';
const { hasMastersTrack } = tracksSelectors;
const startedFetchingTracks = () => ({ type: STARTED_FETCHING_TRACKS });
const errorFetchingTracks = () => ({ type: ERROR_FETCHING_TRACKS });
const gotTracks = (tracks) => ({ type: GOT_TRACKS, tracks });
const fetchTracks = (courseId) => (
(dispatch) => {
dispatch(startedFetchingTracks());
return LmsApiService.fetchTracks(courseId)
.then(({ data }) => data)
.then(({ course_modes }) => {
dispatch(gotTracks(course_modes));
if (hasMastersTrack(course_modes)) {
dispatch(fetchBulkUpgradeHistory(courseId));
}
})
.catch(() => {
dispatch(errorFetchingTracks());
});
}
);
const fetching = {
started: createAction('tracks/fetching/started'),
error: createAction('tracks/fetching/error'),
received: createAction('tracks/fetching/received'),
};
export {
fetchTracks,
startedFetchingTracks,
gotTracks,
errorFetchingTracks,
fetching,
};

View File

@@ -1,11 +0,0 @@
const STARTED_FETCHING_ASSIGNMENT_TYPES = 'STARTED_FETCHING_ASSIGNMENT_TYPES';
const GOT_ASSIGNMENT_TYPES = 'GOT_ASSIGNMENT_TYPES';
const ERROR_FETCHING_ASSIGNMENT_TYPES = 'ERROR_FETCHING_ASSIGNMENT_TYPES';
const GOT_ARE_GRADES_FROZEN = 'GOT_ARE_GRADES_FROZEN';
export {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
};

View File

@@ -1,9 +0,0 @@
const STARTED_FETCHING_COHORTS = 'STARTED_FETCHING_COHORTS';
const GOT_COHORTS = 'GOT_COHORTS';
const ERROR_FETCHING_COHORTS = 'ERROR_FETCHING_COHORTS';
export {
STARTED_FETCHING_COHORTS,
GOT_COHORTS,
ERROR_FETCHING_COHORTS,
};

View File

@@ -1,3 +0,0 @@
const GOT_BULK_MANAGEMENT_CONFIG = 'GOT_BULK_MANAGEMENT_CONFIG';
export default GOT_BULK_MANAGEMENT_CONFIG;

View File

@@ -1,10 +0,0 @@
const INITIALIZE_FILTERS = 'INITIALIZE_FILTERS';
const RESET_FILTERS = 'RESET_FILTERS';
const UPDATE_ASSIGNMENT_FILTER = 'UPDATE_ASSIGNMENT_FILTER';
const UPDATE_ASSIGNMENT_LIMITS = 'UPDATE_ASSIGNMENT_LIMITS';
const UPDATE_COURSE_GRADE_LIMITS = 'UPDATE_COURSE_GRADE_LIMITS';
const UPDATE_INCLUDE_COURSE_ROLE_MEMBERS = 'UPDATE_INCLUDE_COURSE_ROLE_MEMBERS';
export {
INITIALIZE_FILTERS, RESET_FILTERS, UPDATE_ASSIGNMENT_FILTER,
UPDATE_ASSIGNMENT_LIMITS, UPDATE_COURSE_GRADE_LIMITS, UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
};

View File

@@ -1,59 +0,0 @@
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 DONE_VIEWING_ASSIGNMENT = 'DONE_VIEWING_ASSIGNMENT';
const GOT_GRADE_OVERRIDE_HISTORY = 'GOT_GRADE_OVERRIDE_HISTORY';
const ERROR_FETCHING_GRADE_OVERRIDE_HISTORY = 'ERROR_FETCHING_GRADE_OVERRIDE_HISTORY';
const FILTER_SELECTED = 'FILTER_SELECTED';
const GRADE_OVERRIDE = 'GRADE_OVERRIDE';
const REPORT_DOWNLOADED = 'REPORT_DOWNLOADED';
const UPLOAD_OVERRIDE = 'UPLOAD_OVERRIDE';
const UPLOAD_OVERRIDE_ERROR = 'UPLOAD_OVERRIDE_ERROR';
const GRADE_UPDATE_REQUEST = 'GRADE_UPDATE_REQUEST';
const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT';
const FILTER_BY_ASSIGNMENT_TYPE = 'FILTER_BY_ASSIGNMENT_TYPE';
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';
const GOT_BULK_HISTORY = 'GOT_BULK_HISTORY';
const BULK_HISTORY_ERR = 'BULK_HISTORY_ERR';
const BULK_GRADE_REPORT_DOWNLOADED = 'BULK_GRADE_REPORT_DOWNLOADED';
const INTERVENTION_REPORT_DOWNLOADED = 'INTERVENTION_REPORT_DOWNLOADED';
export {
STARTED_FETCHING_GRADES,
FINISHED_FETCHING_GRADES,
ERROR_FETCHING_GRADES,
GOT_GRADES,
GRADE_UPDATE_REQUEST,
GRADE_UPDATE_SUCCESS,
GRADE_UPDATE_FAILURE,
TOGGLE_GRADE_FORMAT,
FILTER_BY_ASSIGNMENT_TYPE,
OPEN_BANNER,
CLOSE_BANNER,
START_UPLOAD,
UPLOAD_COMPLETE,
UPLOAD_ERR,
GOT_BULK_HISTORY,
BULK_HISTORY_ERR,
DONE_VIEWING_ASSIGNMENT,
GOT_GRADE_OVERRIDE_HISTORY,
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
FILTER_SELECTED,
GRADE_OVERRIDE,
REPORT_DOWNLOADED,
UPLOAD_OVERRIDE,
UPLOAD_OVERRIDE_ERROR,
BULK_GRADE_REPORT_DOWNLOADED,
INTERVENTION_REPORT_DOWNLOADED,
};

View File

@@ -1,7 +0,0 @@
const GOT_ROLES = 'GOT_ROLES';
const ERROR_FETCHING_ROLES = 'ERROR_FETCHING_ROLES';
export {
GOT_ROLES,
ERROR_FETCHING_ROLES,
};

View File

@@ -1,9 +0,0 @@
const STARTED_FETCHING_TRACKS = 'STARTED_FETCHING_TRACKS';
const GOT_TRACKS = 'GOT_TRACKS';
const ERROR_FETCHING_TRACKS = 'ERROR_FETCHING_TRACKS';
export {
STARTED_FETCHING_TRACKS,
GOT_TRACKS,
ERROR_FETCHING_TRACKS,
};

View File

@@ -1,9 +1,4 @@
import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';
import * as actions from '../actions/assignmentTypes';
const initialState = {
results: [],
@@ -11,30 +6,30 @@ const initialState = {
errorFetching: false,
};
const assignmentTypes = (state = initialState, action) => {
switch (action.type) {
case GOT_ASSIGNMENT_TYPES:
const assignmentTypes = (state = initialState, { type, payload }) => {
switch (type) {
case actions.received.toString():
return {
...state,
results: action.assignmentTypes,
results: payload,
errorFetching: false,
finishedFetching: true,
};
case STARTED_FETCHING_ASSIGNMENT_TYPES:
case actions.startedFetching.toString():
return {
...state,
startedFetching: true,
};
case ERROR_FETCHING_ASSIGNMENT_TYPES:
case actions.errorFetching.toString():
return {
...state,
finishedFetching: true,
errorFetching: true,
};
case GOT_ARE_GRADES_FROZEN:
case actions.gotGradesFrozen.toString():
return {
...state,
areGradesFrozen: action.areGradesFrozen,
areGradesFrozen: payload,
errorFetching: false,
finishedFetching: true,
};

View File

@@ -1,8 +1,4 @@
import {
STARTED_FETCHING_COHORTS,
ERROR_FETCHING_COHORTS,
GOT_COHORTS,
} from '../constants/actionTypes/cohorts';
import * as actions from '../actions/cohorts';
const initialState = {
results: [],
@@ -12,19 +8,19 @@ const initialState = {
const cohorts = (state = initialState, action) => {
switch (action.type) {
case GOT_COHORTS:
case actions.fetching.received.toString():
return {
...state,
results: action.cohorts,
results: action.payload,
finishedFetching: true,
errorFetching: false,
};
case STARTED_FETCHING_COHORTS:
case actions.fetching.started.toString():
return {
...state,
startedFetching: true,
};
case ERROR_FETCHING_COHORTS:
case actions.fetching.error.toString():
return {
...state,
finishedFetching: true,

View File

@@ -1,11 +1,11 @@
import GOT_BULK_MANAGEMENT_CONFIG from '../constants/actionTypes/config';
import { gotBulkManagementConfig } from '../actions/config';
const reducer = (state = {}, action) => {
switch (action.type) {
case GOT_BULK_MANAGEMENT_CONFIG:
case gotBulkManagementConfig.toString():
return {
...state,
bulkManagementAvailable: action.data,
bulkManagementAvailable: action.payload,
};
default:
return state;

View File

@@ -1,82 +1,77 @@
import filterSelectors from 'data/selectors/filters';
import { GOT_GRADES, FILTER_BY_ASSIGNMENT_TYPE } from '../constants/actionTypes/grades';
import {
INITIALIZE_FILTERS,
UPDATE_ASSIGNMENT_FILTER,
UPDATE_ASSIGNMENT_LIMITS,
UPDATE_COURSE_GRADE_LIMITS,
RESET_FILTERS,
UPDATE_INCLUDE_COURSE_ROLE_MEMBERS,
} from '../constants/actionTypes/filters';
import * as actions from '../actions/filters';
import * as gradeActions from '../actions/grades';
import initialFilters from '../constants/filters';
const { getAssignmentsFromResultsSubstate, chooseRelevantAssignmentData } = filterSelectors;
const initialState = {};
const reducer = (state = initialState, action) => {
switch (action.type) {
case FILTER_BY_ASSIGNMENT_TYPE:
const reducer = (state = initialState, { type: actionType, payload }) => {
switch (actionType) {
case actions.update.assignmentType.toString():
return {
...state,
assignmentType: action.filterType,
assignmentType: payload.filterType,
assignment: (
action.filterType !== ''
&& (state.assignment || {}).type !== action.filterType)
payload.filterType !== ''
&& (state.assignment || {}).type !== payload.filterType)
? '' : state.assignment,
};
case INITIALIZE_FILTERS:
case actions.initialize.toString():
return {
...state,
...action.data,
...payload,
};
case GOT_GRADES: {
case gradeActions.received.toString(): {
const { assignment } = state;
const { id, type } = assignment || {};
if (!type) {
const relevantAssignment = getAssignmentsFromResultsSubstate(action.grades)
.map(chooseRelevantAssignmentData)
.find(assig => assig.id === id);
const relevantAssignment = getAssignmentsFromResultsSubstate(
payload.grades,
).map(
chooseRelevantAssignmentData,
).find(assig => assig.id === id);
return {
...state,
track: action.track,
cohort: action.cohort,
track: payload.track,
cohort: payload.cohort,
assignment: relevantAssignment,
};
}
return {
...state,
track: action.track,
cohort: action.cohort,
track: payload.track,
cohort: payload.cohort,
};
}
case RESET_FILTERS: {
case actions.reset.toString(): {
const result = { ...state };
action.filterNames.forEach((filterName) => {
payload.forEach((filterName) => {
result[filterName] = initialFilters[filterName];
});
return result;
}
case UPDATE_ASSIGNMENT_FILTER:
case actions.update.assignment.toString():
return {
...state,
assignment: action.data,
assignment: payload,
};
case UPDATE_ASSIGNMENT_LIMITS:
case actions.update.assignmentLimits.toString():
return {
...state,
assignmentGradeMin: action.data.minGrade,
assignmentGradeMax: action.data.maxGrade,
assignmentGradeMin: payload.minGrade,
assignmentGradeMax: payload.maxGrade,
};
case UPDATE_COURSE_GRADE_LIMITS:
case actions.update.courseGradeLimits.toString():
return {
...state,
courseGradeMin: action.data.courseGradeMin,
courseGradeMax: action.data.courseGradeMax,
courseGradeMin: payload.courseGradeMin,
courseGradeMax: payload.courseGradeMax,
};
case UPDATE_INCLUDE_COURSE_ROLE_MEMBERS:
case actions.update.includeCourseRoleMembers.toString():
return {
...state,
includeCourseRoleMembers: action.data.includeCourseRoleMembers,
includeCourseRoleMembers: payload,
};
default:
return state;

View File

@@ -1,19 +1,5 @@
import {
STARTED_FETCHING_GRADES,
ERROR_FETCHING_GRADES,
GOT_GRADES,
TOGGLE_GRADE_FORMAT,
FILTER_BY_ASSIGNMENT_TYPE,
OPEN_BANNER,
CLOSE_BANNER,
START_UPLOAD,
UPLOAD_COMPLETE,
UPLOAD_ERR,
GOT_BULK_HISTORY,
DONE_VIEWING_ASSIGNMENT,
GOT_GRADE_OVERRIDE_HISTORY,
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
} from '../constants/actionTypes/grades';
import * as actions from '../actions/grades';
import * as filterActions from '../actions/filters';
const initialState = {
results: [],
@@ -41,23 +27,23 @@ const initialState = {
filteredUsersCount: 0,
};
const grades = (state = initialState, action) => {
switch (action.type) {
case GOT_GRADES:
const grades = (state = initialState, { type, payload }) => {
switch (type) {
case actions.received.toString():
return {
...state,
results: action.grades,
headings: action.headings,
results: payload.grades,
headings: payload.headings,
finishedFetching: true,
errorFetching: false,
prevPage: action.prev,
nextPage: action.next,
prevPage: payload.prev,
nextPage: payload.next,
showSpinner: false,
courseId: action.courseId,
totalUsersCount: action.totalUsersCount,
filteredUsersCount: action.filteredUsersCount,
courseId: payload.courseId,
totalUsersCount: payload.totalUsersCount,
filteredUsersCount: payload.filteredUsersCount,
};
case DONE_VIEWING_ASSIGNMENT: {
case actions.doneViewingAssignment.toString(): {
const {
gradeOverrideHistoryResults,
gradeOverrideCurrentEarnedAllOverride,
@@ -72,63 +58,63 @@ const grades = (state = initialState, action) => {
} = state;
return rest;
}
case GOT_GRADE_OVERRIDE_HISTORY:
case actions.gradeOverride.received.toString():
return {
...state,
gradeOverrideHistoryResults: action.overrideHistory,
gradeOverrideCurrentEarnedAllOverride: action.currentEarnedAllOverride,
gradeOverrideCurrentPossibleAllOverride: action.currentPossibleAllOverride,
gradeOverrideCurrentEarnedGradedOverride: action.currentEarnedGradedOverride,
gradeOverrideCurrentPossibleGradedOverride: action.currentPossibleGradedOverride,
gradeOriginalEarnedAll: action.originalGradeEarnedAll,
gradeOriginalPossibleAll: action.originalGradePossibleAll,
gradeOriginalEarnedGraded: action.originalGradeEarnedGraded,
gradeOriginalPossibleGraded: action.originalGradePossibleGraded,
gradeOverrideHistoryResults: payload.overrideHistory,
gradeOverrideCurrentEarnedAllOverride: payload.currentEarnedAllOverride,
gradeOverrideCurrentPossibleAllOverride: payload.currentPossibleAllOverride,
gradeOverrideCurrentEarnedGradedOverride: payload.currentEarnedGradedOverride,
gradeOverrideCurrentPossibleGradedOverride: payload.currentPossibleGradedOverride,
gradeOriginalEarnedAll: payload.originalGradeEarnedAll,
gradeOriginalPossibleAll: payload.originalGradePossibleAll,
gradeOriginalEarnedGraded: payload.originalGradeEarnedGraded,
gradeOriginalPossibleGraded: payload.originalGradePossibleGraded,
overrideHistoryError: '',
};
case ERROR_FETCHING_GRADE_OVERRIDE_HISTORY:
case actions.overrideHistory.errorFetching.toString():
return {
...state,
finishedFetchingOverrideHistory: true,
overrideHistoryError: action.errorMessage,
overrideHistoryError: payload,
};
case STARTED_FETCHING_GRADES:
case actions.fetching.started.toString():
return {
...state,
startedFetching: true,
finishedFetching: false,
showSpinner: true,
};
case ERROR_FETCHING_GRADES:
case actions.fetching.error.toString():
return {
...state,
finishedFetching: true,
errorFetching: true,
};
case TOGGLE_GRADE_FORMAT:
case actions.toggleGradeFormat.toString():
return {
...state,
gradeFormat: action.formatType,
gradeFormat: payload,
};
case FILTER_BY_ASSIGNMENT_TYPE:
case filterActions.filterAssignmentType.toString():
return {
...state,
selectedAssignmentType: action.filterType,
headings: action.headings,
selectedAssignmentType: payload.filterType,
headings: payload.headings,
};
case OPEN_BANNER:
case actions.banner.open.toString():
return {
...state,
showSuccess: true,
};
case CLOSE_BANNER:
case actions.banner.close.toString():
return {
...state,
showSuccess: false,
};
case START_UPLOAD: {
case actions.csvUpload.started.toString(): {
const { errorMessages, uploadSuccess, ...rest } = state.bulkManagement;
return {
...state,
@@ -136,7 +122,7 @@ const grades = (state = initialState, action) => {
bulkManagement: rest,
};
}
case UPLOAD_COMPLETE: {
case actions.csvUpload.finished.toString():
return {
...state,
showSpinner: false,
@@ -145,22 +131,21 @@ const grades = (state = initialState, action) => {
uploadSuccess: true,
},
};
}
case UPLOAD_ERR:
case actions.csvUpload.error.toString():
return {
...state,
showSpinner: false,
bulkManagement: {
...state.bulkManagement,
...action.data,
...payload,
},
};
case GOT_BULK_HISTORY:
case actions.bulkHistory.received.toString():
return {
...state,
bulkManagement: {
...state.bulkManagement,
history: action.data,
history: payload,
},
};
default:

View File

@@ -1,7 +1,4 @@
import {
GOT_ROLES,
ERROR_FETCHING_ROLES,
} from '../constants/actionTypes/roles';
import * as actions from '../actions/roles';
const initialState = {
canUserViewGradebook: null,
@@ -9,12 +6,12 @@ const initialState = {
const roles = (state = initialState, action) => {
switch (action.type) {
case GOT_ROLES:
case actions.received.toString():
return {
...state,
canUserViewGradebook: action.canUserViewGradebook,
canUserViewGradebook: action.payload,
};
case ERROR_FETCHING_ROLES:
case actions.errorFetching.toString():
return {
...state,
canUserViewGradebook: false,

View File

@@ -1,8 +1,4 @@
import {
STARTED_FETCHING_TRACKS,
ERROR_FETCHING_TRACKS,
GOT_TRACKS,
} from '../constants/actionTypes/tracks';
import * as actions from '../actions/tracks';
const initialState = {
results: [],
@@ -12,19 +8,19 @@ const initialState = {
const tracks = (state = initialState, action) => {
switch (action.type) {
case GOT_TRACKS:
case actions.fetching.received.toString():
return {
...state,
results: action.tracks,
results: action.payload,
errorFetching: false,
finishedFetching: true,
};
case STARTED_FETCHING_TRACKS:
case actions.fetching.started.toString():
return {
...state,
startedFetching: true,
};
case ERROR_FETCHING_TRACKS:
case actions.fetching.error.toString():
return {
...state,
finishedFetching: true,

View File

@@ -102,6 +102,23 @@ const formatMinAssignmentGrade = composeFilters(
assignmentIdIsDefined,
);
const allGrades = ({ grades: { results } }) => results;
const uploadSuccess = ({ grades: { bulkManagement } }) => (!!bulkManagement && bulkManagement.uploadSuccess);
const bulkImportError = ({ grades: { bulkManagement } }) => (
(!!bulkManagement && bulkManagement.errorMessages)
? `Errors while processing: ${bulkManagement.errorMessages.join(', ')}`
: ''
);
const gradeOverrides = ({ grades }) => grades.gradeOverrideHistoryResults;
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 simpleSelectors = simpleSelectorFactory(
({ grades }) => grades,
[
@@ -117,18 +134,9 @@ const simpleSelectors = simpleSelectorFactory(
],
);
const allGrades = ({ grades: { results } }) => results;
const uploadSuccess = ({ grades: { bulkManagement } }) => (!!bulkManagement && bulkManagement.uploadSuccess);
const bulkImportError = ({ grades: { bulkManagement } }) => (
(!!bulkManagement && bulkManagement.errorMessages)
? `Errors while processing: ${bulkManagement.errorMessages.join(', ')}`
: ''
);
const gradeOverrides = ({ grades }) => grades.gradeOverrideHistoryResults;
const selectors = {
bulkImportError,
formatGradeOverrideForDisplay,
formatMinAssignmentGrade,
formatMaxAssignmentGrade,
formatMaxCourseGrade,

View File

@@ -4,12 +4,8 @@ import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProductio
import { createLogger } from 'redux-logger';
import { createMiddleware } from 'redux-beacon';
import Segment, { trackEvent, trackPageView } from '@redux-beacon/segment';
import { GOT_ROLES } from './constants/actionTypes/roles';
import {
GOT_GRADES, GRADE_UPDATE_SUCCESS, GRADE_UPDATE_FAILURE, UPLOAD_OVERRIDE,
UPLOAD_OVERRIDE_ERROR, BULK_GRADE_REPORT_DOWNLOADED, INTERVENTION_REPORT_DOWNLOADED,
} from './constants/actionTypes/grades';
import { UPDATE_COURSE_GRADE_LIMITS } from './constants/actionTypes/filters';
import * as actions from './actions';
import reducers from './reducers';
@@ -17,75 +13,79 @@ const loggerMiddleware = createLogger();
const trackingCategory = 'gradebook';
const eventsMap = {
[GOT_ROLES]: trackPageView(action => ({
[actions.roles.received.toString()]: trackPageView(({ payload }) => ({
category: trackingCategory,
page: action.courseId,
page: payload.courseId,
})),
[GOT_GRADES]: trackEvent(action => ({
[actions.grades.received.toString()]: trackEvent(({ payload }) => ({
name: 'edx.gradebook.grades.displayed',
properties: {
category: trackingCategory,
label: action.courseId,
track: action.track,
cohort: action.cohort,
assignmentType: action.assignmentType,
prev: action.prev,
next: action.next,
label: payload.courseId,
track: payload.track,
cohort: payload.cohort,
assignmentType: payload.assignmentType,
prev: payload.prev,
next: payload.next,
},
})),
[GRADE_UPDATE_SUCCESS]: trackEvent(action => ({
[actions.grades.update.success.toString()]: trackEvent(({ payload }) => ({
name: 'edx.gradebook.grades.grade_override.succeeded',
properties: {
category: trackingCategory,
label: action.courseId,
updatedGrades: action.payload.responseData,
label: payload.courseId,
updatedGrades: payload.responseData,
},
})),
[GRADE_UPDATE_FAILURE]: trackEvent(action => ({
[actions.grades.update.failure.toString()]: trackEvent(({ payload }) => ({
name: 'edx.gradebook.grades.grade_override.failed',
properties: {
category: trackingCategory,
label: action.courseId,
error: action.payload.error,
label: payload.courseId,
error: payload.error,
},
})),
[UPLOAD_OVERRIDE]: trackEvent(action => ({
[actions.grades.uploadOverride.success.toString()]: trackEvent(({ payload }) => ({
name: 'edx.gradebook.grades.upload.grades_overrides.succeeded',
properties: {
category: trackingCategory,
label: action.courseId,
label: payload.courseId,
},
})),
[UPLOAD_OVERRIDE_ERROR]: trackEvent(action => ({
[actions.grades.uploadOverride.failure.toString()]: trackEvent(({ payload }) => ({
name: 'edx.gradebook.grades.upload.grades_overrides.failed',
properties: {
category: trackingCategory,
label: action.courseId,
error: action.payload.error,
label: payload.courseId,
error: payload.error,
},
})),
[UPDATE_COURSE_GRADE_LIMITS]: trackEvent(action => ({
[actions.filters.update.courseGradeLimits]: trackEvent(({ payload }) => ({
name: 'edx.gradebook.grades.filter_applied',
label: action.courseId,
label: payload.courseId,
properties: {
category: trackingCategory,
label: action.courseId,
},
})),
[BULK_GRADE_REPORT_DOWNLOADED]: trackEvent(action => ({
name: 'edx.gradebook.reports.grade_export.downloaded',
properties: {
category: trackingCategory,
label: action.courseId,
},
})),
[INTERVENTION_REPORT_DOWNLOADED]: trackEvent(action => ({
name: 'edx.gradebook.reports.intervention.downloaded',
properties: {
category: trackingCategory,
label: action.courseId,
label: payload.courseId,
},
})),
[actions.grades.downloadReport.bulkGrade.toString()]: trackEvent(
({ payload }) => ({
name: 'edx.gradebook.reports.grade_export.downloaded',
properties: {
category: trackingCategory,
label: payload.courseId,
},
}),
),
[actions.grades.downloadReport.invervention.toString()]: trackEvent(
({ payload }) => ({
name: 'edx.gradebook.reports.intervention.downloaded',
properties: {
category: trackingCategory,
label: payload.courseId,
},
}),
),
};
const segmentMiddleware = createMiddleware(eventsMap, Segment());

View File

@@ -0,0 +1,24 @@
/* eslint-disable import/prefer-default-export */
import * as assignmentTypes from '../actions/assignmentTypes';
import * as config from '../actions/config';
import LmsApiService from '../services/LmsApiService';
const fetchAssignmentTypes = courseId => (
(dispatch) => {
dispatch(assignmentTypes.startedFetching());
return LmsApiService.fetchAssignmentTypes(courseId)
.then(response => response.data)
.then((data) => {
dispatch(assignmentTypes.received(Object.keys(data.assignment_types)));
dispatch(assignmentTypes.gotGradesFrozen(data.grades_frozen));
dispatch(config.gotBulkManagementConfig(data.can_see_bulk_management));
})
.catch(() => {
dispatch(assignmentTypes.errorFetching());
});
}
);
export { fetchAssignmentTypes };

View File

@@ -0,0 +1,21 @@
/* eslint-disable import/prefer-default-export */
import * as cohorts from '../actions/cohorts';
import LmsApiService from '../services/LmsApiService';
const fetchCohorts = courseId => (
(dispatch) => {
dispatch(cohorts.startedFetching());
return LmsApiService.fetchCohorts(courseId)
.then(response => response.data)
.then((data) => {
dispatch(cohorts.received(data.cohorts));
})
.catch(() => {
dispatch(cohorts.errorFetching());
});
}
);
export { fetchCohorts };

View File

@@ -0,0 +1,19 @@
/* eslint-disable import/prefer-default-export */
import * as filters from '../actions/filters';
import { getFilters } from '../selectors/filters';
import { fetchGrades } from './grades';
const updateIncludeCourseRoleMembers = (includeCourseRoleMembers) => (dispatch, getState) => {
dispatch(filters.update.includeCourseRoleMembers(includeCourseRoleMembers));
const state = getState();
const { cohort, track, assignmentType } = getFilters(state);
dispatch(fetchGrades(state.grades.courseId, cohort, track, assignmentType));
};
export {
updateIncludeCourseRoleMembers,
};

View File

@@ -0,0 +1,221 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as grades from '../actions/grades';
import { sortAlphaAsc } from '../actions/utils';
import { getFilters } from '../selectors/filters';
import {
formatMaxAssignmentGrade,
formatMinAssignmentGrade,
formatMaxCourseGrade,
formatMinCourseGrade,
formatGradeOverrideForDisplay,
} from '../selectors/grades';
import GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG from '../constants/errors';
import LmsApiService from '../services/LmsApiService';
const defaultAssignmentFilter = 'All';
const fetchBulkUpgradeHistory = courseId => (
// todo add loading effect
dispatch => LmsApiService.fetchGradeBulkOperationHistory(courseId).then(
(response) => { dispatch(grades.bulkHistory.received(response)); },
).catch(() => dispatch(grades.bulkHistory.error()))
);
const fetchGrades = (
courseId,
cohort,
track,
assignmentType,
options = {},
) => (
(dispatch, getState) => {
dispatch(grades.fetching.started());
const {
assignment,
assignmentGradeMax: assignmentMax,
assignmentGradeMin: assignmentMin,
courseGradeMin,
courseGradeMax,
includeCourseRoleMembers,
} = getFilters(getState());
const { id: assignmentId } = assignment || {};
const assignmentGradeMax = formatMaxAssignmentGrade(assignmentMax, { assignmentId });
const assignmentGradeMin = formatMinAssignmentGrade(assignmentMin, { assignmentId });
const courseGradeMinFormatted = formatMinCourseGrade(courseGradeMin);
const courseGradeMaxFormatted = formatMaxCourseGrade(courseGradeMax);
return LmsApiService.fetchGradebookData(
courseId,
options.searchText || null,
cohort,
track,
{
assignment: assignmentId,
assignmentGradeMax,
assignmentGradeMin,
courseGradeMin: courseGradeMinFormatted,
courseGradeMax: courseGradeMaxFormatted,
includeCourseRoleMembers,
},
)
.then(response => response.data)
.then((data) => {
dispatch(grades.received({
grades: data.results.sort(sortAlphaAsc),
cohort,
track,
assignmentType,
prev: data.previous,
next: data.next,
courseId,
totalUsersCount: data.total_users_count,
filteredUsersCount: data.filtered_users_count,
}));
dispatch(grades.fetching.finished());
if (options.showSuccess) {
dispatch(grades.banner.open());
}
})
.catch(() => {
dispatch(grades.fetching.error());
});
}
);
const fetchGradeOverrideHistory = (subsectionId, userId) => (
dispatch => LmsApiService.fetchGradeOverrideHistory(subsectionId, userId)
.then(response => response.data)
.then((data) => {
if (data.success) {
dispatch(grades.overrideHistory.received({
overrideHistory: formatGradeOverrideForDisplay(data.history),
currentEarnedAllOverride: data.override ? data.override.earned_all_override : null,
currentPossibleAllOverride: data.override ? data.override.possible_all_override : null,
currentEarnedGradedOverride: data.override ? data.override.earned_graded_override : null,
currentPossibleGradedOverride: data.override
? data.override.possible_graded_override : null,
originalGradeEarnedAll: data.original_grade ? data.original_grade.earned_all : null,
originalGradePossibleAll: data.original_grade ? data.original_grade.possible_all : null,
originalGradeEarnedGraded: data.original_grade
? data.original_grade.earned_graded : null,
originalGradePossibleGraded: data.original_grade
? data.original_grade.possible_graded : null,
}));
} else {
dispatch(grades.overrideHistory.errorFetching(data.error_message));
}
})
.catch(() => {
dispatch(grades.overrideHistory.errorFetching(GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG));
})
);
const fetchMatchingUserGrades = (
courseId,
searchText,
cohort,
track,
assignmentType,
showSuccess,
options = {},
) => {
const newOptions = { ...options, searchText, showSuccess };
return fetchGrades(courseId, cohort, track, assignmentType, newOptions);
};
const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
(dispatch) => {
dispatch(grades.fetching.started());
return getAuthenticatedHttpClient().get(endpoint)
.then(response => response.data)
.then((data) => {
dispatch(grades.fetching.results({
grades: data.results.sort(sortAlphaAsc),
cohort,
track,
assignmentType,
prev: data.previous,
next: data.next,
courseId,
totalUsersCount: data.total_users_count,
filteredUsersCount: data.filtered_users_count,
}));
dispatch(grades.fetching.finished());
})
.catch(() => {
dispatch(grades.fetching.error());
});
}
);
const submitFileUploadFormData = (courseId, formData) => (
(dispatch) => {
dispatch(grades.csvUpload.started());
return LmsApiService.uploadGradeCsv(courseId, formData).then(() => {
dispatch(grades.csvUpload.finished());
dispatch(grades.uploadOverride.success(courseId));
}).catch((err) => {
dispatch(grades.uploadOverride.failure(courseId, err));
if (err.status === 200 && err.data.error_messages.length) {
const { error_messages: errorMessages, saved, total } = err.data;
return dispatch(grades.csvUpload.error({ errorMessages, saved, total }));
}
return dispatch(grades.csvUpload.error({ errorMessages: ['Unknown error.'] }));
});
}
);
const updateGrades = (courseId, updateData, searchText, cohort, track) => (
(dispatch) => {
dispatch(grades.update.request());
return LmsApiService.updateGradebookData(courseId, updateData)
.then(response => response.data)
.then((data) => {
dispatch(grades.update.success({ courseId, data }));
dispatch(fetchMatchingUserGrades(
courseId,
searchText,
cohort,
track,
defaultAssignmentFilter,
true,
{ searchText },
));
})
.catch((error) => {
dispatch(grades.update.failure({ courseId, error }));
});
}
);
const updateGradesIfAssignmentGradeFiltersSet = (
courseId,
cohort,
track,
assignmentType,
) => (dispatch, getState) => {
const { filters: { assignmentGradeMin, assignmentGradeMax } } = getState();
const hasAssignmentGradeFiltersSet = assignmentGradeMax || assignmentGradeMin;
if (hasAssignmentGradeFiltersSet) {
dispatch(fetchGrades(
courseId,
cohort,
track,
assignmentType,
));
}
};
export {
fetchBulkUpgradeHistory,
fetchGrades,
fetchGradeOverrideHistory,
fetchMatchingUserGrades,
fetchPrevNextGrades,
submitFileUploadFormData,
updateGrades,
updateGradesIfAssignmentGradeFiltersSet,
};

View File

@@ -0,0 +1,41 @@
/* eslint-disable import/prefer-default-export */
import * as roles from '../actions/roles';
import { getFilters } from '../selectors/filters';
import { fetchCohorts } from './cohorts';
import {
fetchGrades,
} from './grades';
import { fetchTracks } from './tracks';
import { fetchAssignmentTypes } from './assignmentTypes';
import LmsApiService from '../services/LmsApiService';
const allowedRoles = ['staff', 'instructor', 'support'];
const getRoles = courseId => (
(dispatch, getState) => LmsApiService.fetchUserRoles(courseId)
.then(response => response.data)
.then((response) => {
const isAllowedRole = (role) => (
(role.course_id === courseId) && allowedRoles.includes(role.role)
);
const canUserViewGradebook = (response.is_staff || (response.roles.some(isAllowedRole)));
dispatch(roles.received({ canUserViewGradebook, courseId }));
const { cohort, track, assignmentType } = getFilters(getState());
if (canUserViewGradebook) {
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
dispatch(fetchTracks(courseId));
dispatch(fetchCohorts(courseId));
dispatch(fetchAssignmentTypes(courseId));
}
})
.catch(() => {
dispatch(roles.errorFetching());
}));
export {
getRoles,
};

View File

@@ -0,0 +1,31 @@
/* eslint-disable import/prefer-default-export */
import * as tracks from '../actions/tracks';
import { hasMastersTrack } from '../selectors/tracks';
import {
fetchBulkUpgradeHistory,
} from './grades';
import LmsApiService from '../services/LmsApiService';
const fetchTracks = courseId => (
(dispatch) => {
dispatch(tracks.fetching.started());
return LmsApiService.fetchTracks(courseId)
.then(response => response.data)
.then((data) => {
dispatch(tracks.fetching.received(data.course_modes));
if (hasMastersTrack(data.course_modes)) {
dispatch(fetchBulkUpgradeHistory(courseId));
}
})
.catch(() => {
dispatch(tracks.fetching.error());
});
}
);
export {
fetchTracks,
};