Add new filter for assignments
JIRA:EDUCATOR-4514 - Filter applies both to gradebook view and to CSV export used for bulk management. - Fixes a bug with the cohort filter being applied to bulk management CSVs - Sets us up to add new filters more easily - New filter interoperates with existing assignment type filter to limit options
This commit is contained in:
@@ -41,7 +41,8 @@ export default class Gradebook extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
const urlQuery = queryString.parse(this.props.location.search);
|
||||
this.props.getRoles(this.props.courseId, urlQuery);
|
||||
this.props.initializeFilters(urlQuery);
|
||||
this.props.getRoles(this.props.courseId);
|
||||
this.overrideReasonInput.focus();
|
||||
}
|
||||
|
||||
@@ -87,6 +88,17 @@ export default class Gradebook extends React.Component {
|
||||
return ['Grades'];
|
||||
};
|
||||
|
||||
getAssignmentFilterOptions = () => [
|
||||
{ label: 'All', value: '' },
|
||||
...this.props.assignmentFilterOptions.map((assignment) => {
|
||||
const { label, subsectionLabel } = assignment;
|
||||
return {
|
||||
label: `${label}: ${subsectionLabel}`,
|
||||
value: label,
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
handleAdjustedGradeClick = () => {
|
||||
this.props.updateGrades(
|
||||
this.props.courseId, [
|
||||
@@ -113,9 +125,23 @@ export default class Gradebook extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleAssignmentFilterChange = (assignment) => {
|
||||
const selectedFilterOption = this.props.assignmentFilterOptions.find(assig =>
|
||||
assig.label === assignment);
|
||||
const { type, id } = selectedFilterOption || {};
|
||||
const typedValue = { label: assignment, type, id };
|
||||
this.props.updateAssignmentFilter(typedValue);
|
||||
const updatedQueryStrings = this.updateQueryParams('assignment', assignment);
|
||||
this.props.history.push(updatedQueryStrings);
|
||||
}
|
||||
|
||||
updateQueryParams = (queryKey, queryValue) => {
|
||||
const parsed = queryString.parse(this.props.location.search);
|
||||
parsed[queryKey] = queryValue;
|
||||
if (queryValue) {
|
||||
parsed[queryKey] = queryValue;
|
||||
} else {
|
||||
delete parsed[queryKey];
|
||||
}
|
||||
return `?${queryString.stringify(parsed)}`;
|
||||
};
|
||||
|
||||
@@ -124,7 +150,7 @@ export default class Gradebook extends React.Component {
|
||||
id: entry,
|
||||
label: entry,
|
||||
}));
|
||||
mapped.unshift({ id: 0, label: 'All' });
|
||||
mapped.unshift({ id: 0, label: 'All', value: '' });
|
||||
return mapped;
|
||||
};
|
||||
|
||||
@@ -182,9 +208,9 @@ export default class Gradebook extends React.Component {
|
||||
return { resultsSummary, filename, ...rest };
|
||||
};
|
||||
|
||||
updateAssignmentTypes = (event) => {
|
||||
this.props.filterColumns(event, this.props.grades[0]);
|
||||
const updatedQueryStrings = this.updateQueryParams('assignmentType', event);
|
||||
updateAssignmentTypes = (assignmentType) => {
|
||||
this.props.filterAssignmentType(assignmentType);
|
||||
const updatedQueryStrings = this.updateQueryParams('assignmentType', assignmentType);
|
||||
this.props.history.push(updatedQueryStrings);
|
||||
}
|
||||
|
||||
@@ -216,7 +242,8 @@ export default class Gradebook extends React.Component {
|
||||
this.props.selectedTrack,
|
||||
this.props.selectedAssignmentType,
|
||||
);
|
||||
this.updateQueryParams('cohort', selectedCohortId);
|
||||
const updatedQueryStrings = this.updateQueryParams('cohort', selectedCohortId);
|
||||
this.props.history.push(updatedQueryStrings);
|
||||
};
|
||||
|
||||
handleClickExportGrades = () => {
|
||||
@@ -379,20 +406,32 @@ export default class Gradebook extends React.Component {
|
||||
options={[{ label: 'Percent', value: 'percent' }, { label: 'Absolute', value: 'absolute' }]}
|
||||
onChange={this.props.toggleFormat}
|
||||
/>
|
||||
{this.props.assignmentTypes.length > 0 &&
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Assignment Types:
|
||||
</span>
|
||||
<InputSelect
|
||||
name="assignment-types"
|
||||
ariaLabel="Assignment Types"
|
||||
value={this.props.selectedAssignmentType}
|
||||
options={this.mapAssignmentTypeEntries(this.props.assignmentTypes)}
|
||||
onChange={this.updateAssignmentTypes}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Assignment Types:
|
||||
</span>
|
||||
<InputSelect
|
||||
name="assignment-types"
|
||||
ariaLabel="Assignment Types"
|
||||
value={this.props.selectedAssignmentType}
|
||||
options={this.mapAssignmentTypeEntries(this.props.assignmentTypes)}
|
||||
onChange={this.updateAssignmentTypes}
|
||||
disabled={this.props.assignmentFilterOptions.length === 0}
|
||||
/>
|
||||
</div>
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Assignment:
|
||||
</span>
|
||||
<InputSelect
|
||||
name="assignment"
|
||||
ariaLabel="Assignment"
|
||||
value={this.props.selectedAssignment}
|
||||
options={this.getAssignmentFilterOptions()}
|
||||
onChange={this.handleAssignmentFilterChange}
|
||||
disabled={this.props.assignmentFilterOptions.length === 0}
|
||||
/>
|
||||
</div>
|
||||
<div className="student-filters">
|
||||
<span className="label">
|
||||
Student Groups:
|
||||
@@ -643,6 +682,7 @@ export default class Gradebook extends React.Component {
|
||||
Gradebook.defaultProps = {
|
||||
areGradesFrozen: false,
|
||||
assignmentTypes: [],
|
||||
assignmentFilterOptions: [],
|
||||
canUserViewGradebook: false,
|
||||
cohorts: [],
|
||||
grades: [],
|
||||
@@ -655,7 +695,8 @@ Gradebook.defaultProps = {
|
||||
courseId: '',
|
||||
selectedCohort: null,
|
||||
selectedTrack: null,
|
||||
selectedAssignmentType: 'All',
|
||||
selectedAssignmentType: '',
|
||||
selectedAssignment: '',
|
||||
showSpinner: false,
|
||||
tracks: [],
|
||||
bulkImportError: '',
|
||||
@@ -671,9 +712,17 @@ Gradebook.defaultProps = {
|
||||
Gradebook.propTypes = {
|
||||
areGradesFrozen: PropTypes.bool,
|
||||
assignmentTypes: PropTypes.arrayOf(PropTypes.string),
|
||||
assignmentFilterOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
subsectionLabel: PropTypes.string,
|
||||
})),
|
||||
canUserViewGradebook: PropTypes.bool,
|
||||
cohorts: PropTypes.arrayOf(PropTypes.string),
|
||||
filterColumns: PropTypes.func.isRequired,
|
||||
cohorts: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
id: PropTypes.number,
|
||||
})),
|
||||
filterAssignmentType: PropTypes.func.isRequired,
|
||||
updateAssignmentFilter: PropTypes.func.isRequired,
|
||||
format: PropTypes.string.isRequired,
|
||||
getRoles: PropTypes.func.isRequired,
|
||||
getUserGrades: PropTypes.func.isRequired,
|
||||
@@ -711,9 +760,8 @@ Gradebook.propTypes = {
|
||||
courseId: PropTypes.string,
|
||||
searchForUser: PropTypes.func.isRequired,
|
||||
selectedAssignmentType: PropTypes.string,
|
||||
selectedCohort: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
}),
|
||||
selectedAssignment: PropTypes.string,
|
||||
selectedCohort: PropTypes.number,
|
||||
selectedTrack: PropTypes.string,
|
||||
showSpinner: PropTypes.bool,
|
||||
showSuccess: PropTypes.bool.isRequired,
|
||||
@@ -744,4 +792,5 @@ Gradebook.propTypes = {
|
||||
totalUsersCount: PropTypes.number,
|
||||
filteredUsersCount: PropTypes.number,
|
||||
showDownloadButtons: PropTypes.bool,
|
||||
initializeFilters: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -8,14 +8,17 @@ import {
|
||||
fetchPrevNextGrades,
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
filterColumns,
|
||||
filterAssignmentType,
|
||||
closeBanner,
|
||||
submitFileUploadFormData,
|
||||
} from '../../data/actions/grades';
|
||||
import { fetchCohorts } from '../../data/actions/cohorts';
|
||||
import { fetchTracks } from '../../data/actions/tracks';
|
||||
import { initializeFilters, updateAssignmentFilter } from '../../data/actions/filters';
|
||||
import stateHasMastersTrack from '../../data/selectors/tracks';
|
||||
import getBulkManagementHistory from '../../data/selectors/grades';
|
||||
import { getBulkManagementHistory, getHeadings } from '../../data/selectors/grades';
|
||||
import { selectableAssignmentLabels } from '../../data/selectors/filters';
|
||||
import { getCohortNameById } from '../../data/selectors/cohorts';
|
||||
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
||||
import { getRoles } from '../../data/actions/roles';
|
||||
import LmsApiService from '../../data/services/LmsApiService';
|
||||
@@ -40,24 +43,28 @@ const mapStateToProps = (state, ownProps) => (
|
||||
gradeOverrideCurrentPossibleGradedOverride:
|
||||
state.grades.gradeOverrideCurrentPossibleGradedOverride,
|
||||
gradeOriginalEarnedGraded: state.grades.gradeOriginalEarnedGraded,
|
||||
headings: state.grades.headings,
|
||||
headings: getHeadings(state),
|
||||
tracks: state.tracks.results,
|
||||
cohorts: state.cohorts.results,
|
||||
selectedTrack: state.grades.selectedTrack,
|
||||
selectedCohort: state.grades.selectedCohort,
|
||||
selectedAssignmentType: state.grades.selectedAssignmentType,
|
||||
selectedTrack: state.filters.track,
|
||||
selectedCohort: state.filters.cohort,
|
||||
selectedAssignmentType: state.filters.assignmentType,
|
||||
selectedAssignment: (state.filters.assignment || {}).label,
|
||||
format: state.grades.gradeFormat,
|
||||
showSuccess: state.grades.showSuccess,
|
||||
errorFetchingGradeOverrideHistory: state.grades.errorFetchingOverrideHistory,
|
||||
prevPage: state.grades.prevPage,
|
||||
nextPage: state.grades.nextPage,
|
||||
assignmentTypes: state.assignmentTypes.results,
|
||||
assignmentFilterOptions: selectableAssignmentLabels(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,
|
||||
cohort: getCohortNameById(state, state.filters.cohort),
|
||||
track: state.filters.track,
|
||||
assignment: (state.filters.assignment || {}).id,
|
||||
assignmentType: state.filters.assignmentType,
|
||||
}),
|
||||
interventionExportUrl:
|
||||
LmsApiService.getInterventionExportCsvUrl(ownProps.match.params.courseId),
|
||||
@@ -85,10 +92,12 @@ const mapDispatchToProps = {
|
||||
getAssignmentTypes: fetchAssignmentTypes,
|
||||
updateGrades,
|
||||
toggleFormat: toggleGradeFormat,
|
||||
filterColumns,
|
||||
filterAssignmentType,
|
||||
closeBanner,
|
||||
getRoles,
|
||||
submitFileUploadFormData,
|
||||
initializeFilters,
|
||||
updateAssignmentFilter,
|
||||
};
|
||||
|
||||
const GradebookPage = connect(
|
||||
|
||||
23
src/data/actions/filters.js
Normal file
23
src/data/actions/filters.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { INITIALIZE_FILTERS, UPDATE_ASSIGNMENT_FILTER } from '../constants/actionTypes/filters';
|
||||
|
||||
const initializeFilters = ({
|
||||
assignment = '',
|
||||
assignmentType = '',
|
||||
track = '',
|
||||
cohort = '',
|
||||
}) => ({
|
||||
type: INITIALIZE_FILTERS,
|
||||
data: {
|
||||
assignment: { label: assignment },
|
||||
assignmentType,
|
||||
track,
|
||||
cohort,
|
||||
},
|
||||
});
|
||||
|
||||
const updateAssignmentFilter = assignment => ({
|
||||
type: UPDATE_ASSIGNMENT_FILTER,
|
||||
data: assignment,
|
||||
});
|
||||
|
||||
export { initializeFilters, updateAssignmentFilter };
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_COLUMNS,
|
||||
FILTER_BY_ASSIGNMENT_TYPE,
|
||||
OPEN_BANNER,
|
||||
CLOSE_BANNER,
|
||||
START_UPLOAD,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
ERROR_FETCHING_GRADE_OVERRIDE_HISTORY,
|
||||
} from '../constants/actionTypes/grades';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import { headingMapper, sortAlphaAsc, formatDateForDisplay } from './utils';
|
||||
import { sortAlphaAsc, formatDateForDisplay } from './utils';
|
||||
import apiClient from '../apiClient';
|
||||
|
||||
const defaultAssignmentFilter = 'All';
|
||||
@@ -85,10 +85,10 @@ const gradeUpdateFailure = (courseId, error) => ({
|
||||
|
||||
const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType });
|
||||
|
||||
const filterColumns = (filterType, exampleUser) => (
|
||||
const filterAssignmentType = filterType => (
|
||||
dispatch => dispatch({
|
||||
type: FILTER_COLUMNS,
|
||||
headings: headingMapper(filterType)(exampleUser),
|
||||
type: FILTER_BY_ASSIGNMENT_TYPE,
|
||||
filterType,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -112,7 +112,6 @@ const fetchGrades = (
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
headings: headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
|
||||
prev: data.previous,
|
||||
next: data.next,
|
||||
courseId,
|
||||
@@ -185,7 +184,6 @@ const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType)
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
headings: headingMapper(assignmentType || defaultAssignmentFilter)(data.results[0]),
|
||||
prev: data.previous,
|
||||
next: data.next,
|
||||
courseId,
|
||||
@@ -259,7 +257,7 @@ export {
|
||||
gradeUpdateFailure,
|
||||
updateGrades,
|
||||
toggleGradeFormat,
|
||||
filterColumns,
|
||||
filterAssignmentType,
|
||||
closeBanner,
|
||||
submitFileUploadFormData,
|
||||
fetchBulkUpgradeHistory,
|
||||
|
||||
@@ -97,11 +97,6 @@ describe('actions', () => {
|
||||
cohort: expectedCohort,
|
||||
track: expectedTrack,
|
||||
assignmentType: expectedAssignmentType,
|
||||
headings: [
|
||||
'Username',
|
||||
'Email',
|
||||
'Total',
|
||||
],
|
||||
prev: responseData.previous,
|
||||
next: responseData.next,
|
||||
courseId,
|
||||
@@ -159,7 +154,6 @@ describe('actions', () => {
|
||||
cohort: expectedCohort,
|
||||
track: expectedTrack,
|
||||
assignmentType: expectedAssignmentType,
|
||||
headings: [],
|
||||
prev: responseData.previous,
|
||||
next: responseData.next,
|
||||
courseId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fetchGrades } from './grades';
|
||||
import { fetchTracks } from './tracks';
|
||||
import { fetchCohorts } from './cohorts';
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
import { getFilters } from '../selectors/filters';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const allowedRoles = ['staff', 'instructor', 'support'];
|
||||
@@ -17,16 +18,17 @@ const gotRoles = (canUserViewGradebook, courseId) => ({
|
||||
});
|
||||
const errorFetchingRoles = () => ({ type: ERROR_FETCHING_ROLES });
|
||||
|
||||
const getRoles = (courseId, urlQuery) => (
|
||||
dispatch => LmsApiService.fetchUserRoles(courseId)
|
||||
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 } = getFilters(getState());
|
||||
if (canUserViewGradebook) {
|
||||
dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track, urlQuery.assignmentType));
|
||||
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
||||
dispatch(fetchTracks(courseId));
|
||||
dispatch(fetchCohorts(courseId));
|
||||
dispatch(fetchAssignmentTypes(courseId));
|
||||
|
||||
@@ -26,29 +26,5 @@ const sortAlphaAsc = (gradeRowA, gradeRowB) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
const headingMapper = (filterKey) => {
|
||||
const filters = {
|
||||
all: section => section.label,
|
||||
some: section => section.label && section.category === filterKey,
|
||||
};
|
||||
|
||||
const filter = filterKey === 'All' ? 'all' : 'some';
|
||||
|
||||
return (entry) => {
|
||||
if (entry) {
|
||||
const results = ['Username', 'Email'];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
.filter(filters[filter])
|
||||
.map(s => s.label);
|
||||
|
||||
const totals = ['Total'];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
};
|
||||
|
||||
export { headingMapper, sortAlphaAsc, formatDateForDisplay };
|
||||
export { sortAlphaAsc, formatDateForDisplay };
|
||||
|
||||
|
||||
4
src/data/constants/actionTypes/filters.js
Normal file
4
src/data/constants/actionTypes/filters.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const INITIALIZE_FILTERS = 'INITIALIZE_FILTERS';
|
||||
const UPDATE_ASSIGNMENT_FILTER = 'UPDATE_ASSIGNMENT_FILTER';
|
||||
|
||||
export { INITIALIZE_FILTERS, UPDATE_ASSIGNMENT_FILTER };
|
||||
@@ -10,7 +10,7 @@ const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
|
||||
const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
|
||||
|
||||
const TOGGLE_GRADE_FORMAT = 'TOGGLE_GRADE_FORMAT';
|
||||
const FILTER_COLUMNS = 'FILTER_COLUMNS';
|
||||
const FILTER_BY_ASSIGNMENT_TYPE = 'FILTER_BY_ASSIGNMENT_TYPE';
|
||||
const CLOSE_BANNER = 'CLOSE_BANNER';
|
||||
const OPEN_BANNER = 'OPEN_BANNER';
|
||||
|
||||
@@ -29,7 +29,7 @@ export {
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_COLUMNS,
|
||||
FILTER_BY_ASSIGNMENT_TYPE,
|
||||
OPEN_BANNER,
|
||||
CLOSE_BANNER,
|
||||
START_UPLOAD,
|
||||
|
||||
55
src/data/reducers/filters.js
Normal file
55
src/data/reducers/filters.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { GOT_GRADES, FILTER_BY_ASSIGNMENT_TYPE } from '../constants/actionTypes/grades';
|
||||
|
||||
import { INITIALIZE_FILTERS, UPDATE_ASSIGNMENT_FILTER } from '../constants/actionTypes/filters';
|
||||
|
||||
import { getAssignmentsFromResultsSubstate, chooseRelevantAssignmentData } from '../selectors/filters';
|
||||
|
||||
const initialState = {};
|
||||
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case FILTER_BY_ASSIGNMENT_TYPE:
|
||||
return {
|
||||
...state,
|
||||
assignmentType: action.filterType,
|
||||
assignment: (
|
||||
action.filterType !== '' &&
|
||||
(state.assignment || {}).type !== action.filterType)
|
||||
? '' : state.assignment,
|
||||
};
|
||||
case INITIALIZE_FILTERS:
|
||||
return {
|
||||
...state,
|
||||
...action.data,
|
||||
};
|
||||
case GOT_GRADES: {
|
||||
const { assignment } = state;
|
||||
const { label, type } = assignment || {};
|
||||
if (!type) {
|
||||
const relevantAssignment = getAssignmentsFromResultsSubstate(action.grades)
|
||||
.map(chooseRelevantAssignmentData)
|
||||
.find(assig => assig.label === label);
|
||||
return {
|
||||
...state,
|
||||
track: action.track,
|
||||
cohort: action.cohort,
|
||||
assignment: relevantAssignment,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
track: action.track,
|
||||
cohort: action.cohort,
|
||||
};
|
||||
}
|
||||
case UPDATE_ASSIGNMENT_FILTER:
|
||||
return {
|
||||
...state,
|
||||
assignment: action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_COLUMNS,
|
||||
FILTER_BY_ASSIGNMENT_TYPE,
|
||||
OPEN_BANNER,
|
||||
CLOSE_BANNER,
|
||||
START_UPLOAD,
|
||||
@@ -48,9 +48,6 @@ const grades = (state = initialState, action) => {
|
||||
headings: action.headings,
|
||||
finishedFetching: true,
|
||||
errorFetching: false,
|
||||
selectedTrack: action.track,
|
||||
selectedCohort: action.cohort,
|
||||
selectedAssignmentType: action.assignmentType,
|
||||
prevPage: action.prev,
|
||||
nextPage: action.next,
|
||||
showSpinner: false,
|
||||
@@ -98,9 +95,10 @@ const grades = (state = initialState, action) => {
|
||||
...state,
|
||||
gradeFormat: action.formatType,
|
||||
};
|
||||
case FILTER_COLUMNS:
|
||||
case FILTER_BY_ASSIGNMENT_TYPE:
|
||||
return {
|
||||
...state,
|
||||
selectedAssignmentType: action.filterType,
|
||||
headings: action.headings,
|
||||
};
|
||||
case OPEN_BANNER:
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
TOGGLE_GRADE_FORMAT,
|
||||
FILTER_COLUMNS,
|
||||
FILTER_BY_ASSIGNMENT_TYPE,
|
||||
OPEN_BANNER,
|
||||
} from '../constants/actionTypes/grades';
|
||||
|
||||
@@ -94,8 +94,6 @@ describe('grades reducer', () => {
|
||||
headings: headingsData,
|
||||
errorFetching: false,
|
||||
finishedFetching: true,
|
||||
selectedTrack: expectedTrack,
|
||||
selectedCohort: expectedCohortId,
|
||||
prevPage: expectedPrev,
|
||||
nextPage: expectedNext,
|
||||
showSpinner: false,
|
||||
@@ -137,7 +135,7 @@ describe('grades reducer', () => {
|
||||
headings: expectedHeadings,
|
||||
};
|
||||
expect(grades(undefined, {
|
||||
type: FILTER_COLUMNS,
|
||||
type: FILTER_BY_ASSIGNMENT_TYPE,
|
||||
headings: expectedHeadings,
|
||||
})).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import grades from './grades';
|
||||
import tracks from './tracks';
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
import roles from './roles';
|
||||
import filters from './filters';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
grades,
|
||||
@@ -12,6 +13,7 @@ const rootReducer = combineReducers({
|
||||
tracks,
|
||||
assignmentTypes,
|
||||
roles,
|
||||
filters,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
|
||||
12
src/data/selectors/cohorts.js
Normal file
12
src/data/selectors/cohorts.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
const getCohorts = state => state.cohorts.results || [];
|
||||
|
||||
const getCohortById = (state, selectedCohortId) => {
|
||||
const cohort = getCohorts(state).find(coh => coh.id === selectedCohortId);
|
||||
return cohort;
|
||||
};
|
||||
|
||||
const getCohortNameById = (state, selectedCohortId) =>
|
||||
(getCohortById(state, selectedCohortId) || {}).name;
|
||||
|
||||
export { getCohortById, getCohortNameById, getCohorts };
|
||||
41
src/data/selectors/filters.js
Normal file
41
src/data/selectors/filters.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const getFilters = state => state.filters || {};
|
||||
|
||||
const getAssignmentsFromResultsSubstate = results =>
|
||||
(results[0] || {}).section_breakdown || [];
|
||||
|
||||
const selectableAssignments = (state) => {
|
||||
const selectedAssignmentType = getFilters(state).assignmentType;
|
||||
const needToFilter = selectedAssignmentType && selectedAssignmentType !== 'All';
|
||||
const allAssignments = getAssignmentsFromResultsSubstate(state.grades.results);
|
||||
if (needToFilter) {
|
||||
return allAssignments.filter(assignment => assignment.category === selectedAssignmentType);
|
||||
}
|
||||
return allAssignments;
|
||||
};
|
||||
|
||||
const chooseRelevantAssignmentData = assignment => ({
|
||||
label: assignment.label,
|
||||
subsectionLabel: assignment.subsection_name,
|
||||
type: assignment.category,
|
||||
id: assignment.module_id,
|
||||
});
|
||||
|
||||
const selectableAssignmentLabels = state =>
|
||||
selectableAssignments(state).map(chooseRelevantAssignmentData);
|
||||
|
||||
const typeOfSelectedAssignment = (state) => {
|
||||
const selectedAssignmentLabel = getFilters(state).assignment;
|
||||
const sectionBreakdown = (state.grades.results[0] || {}).section_breakdown || [];
|
||||
const selectedAssignment = sectionBreakdown.find(section =>
|
||||
section.label === selectedAssignmentLabel);
|
||||
return selectedAssignment && selectedAssignment.category;
|
||||
};
|
||||
|
||||
export {
|
||||
selectableAssignmentLabels,
|
||||
selectableAssignments,
|
||||
getFilters,
|
||||
typeOfSelectedAssignment,
|
||||
chooseRelevantAssignmentData,
|
||||
getAssignmentsFromResultsSubstate,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatDateForDisplay } from '../../data/actions/utils';
|
||||
import { formatDateForDisplay } from '../actions/utils';
|
||||
import { getFilters } from './filters';
|
||||
|
||||
const getRowsProcessed = (data) => {
|
||||
const {
|
||||
@@ -37,4 +38,46 @@ const getBulkManagementHistoryFromState = state =>
|
||||
const getBulkManagementHistory = state =>
|
||||
getBulkManagementHistoryFromState(state).map(transformHistoryEntry);
|
||||
|
||||
export default getBulkManagementHistory;
|
||||
const headingMapper = (category, label = 'All') => {
|
||||
const filters = {
|
||||
all: section => section.label,
|
||||
byCategory: section => section.label && section.category === category,
|
||||
byLabel: section => section.label && section.label === label,
|
||||
};
|
||||
|
||||
let filter;
|
||||
if (label === 'All') {
|
||||
filter = category === 'All' ? 'all' : 'byCategory';
|
||||
} else {
|
||||
filter = 'byLabel';
|
||||
}
|
||||
|
||||
return (entry) => {
|
||||
if (entry) {
|
||||
const results = ['Username', 'Email'];
|
||||
|
||||
const assignmentHeadings = entry
|
||||
.filter(filters[filter])
|
||||
.map(s => s.label);
|
||||
|
||||
const totals = ['Total'];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
};
|
||||
|
||||
const getHeadings = (state) => {
|
||||
const filters = getFilters(state) || {};
|
||||
const {
|
||||
assignmentType: selectedAssignmentType,
|
||||
assignment: selectedAssignment,
|
||||
} = filters;
|
||||
const assignments = (state.grades.results[0] || {}).section_breakdown || [];
|
||||
const type = selectedAssignmentType || 'All';
|
||||
const assignment = (selectedAssignment || {}).label || 'All';
|
||||
return headingMapper(type, assignment)(assignments);
|
||||
};
|
||||
|
||||
export { getBulkManagementHistory, getHeadings };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import getBulkManagementHistory from './grades';
|
||||
import { getBulkManagementHistory } from './grades';
|
||||
|
||||
const genericHistoryRow = {
|
||||
id: 5,
|
||||
|
||||
@@ -70,11 +70,12 @@ class LmsApiService {
|
||||
}
|
||||
|
||||
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;
|
||||
const queryParams = ['track', 'cohort', 'assignment', 'assignmentType']
|
||||
.filter(opt => options[opt] &&
|
||||
options[opt] !== 'All')
|
||||
.map(opt => `${opt}=${encodeURIComponent(options[opt])}`)
|
||||
.join('&');
|
||||
return `${LmsApiService.baseUrl}/api/bulk_grades/course/${courseId}/?${queryParams}`;
|
||||
}
|
||||
|
||||
static getInterventionExportCsvUrl(courseId) {
|
||||
|
||||
Reference in New Issue
Block a user