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:
Matt Hughes
2019-08-02 10:29:38 -04:00
parent 5b16a5dbb2
commit 9ed5c6cb34
18 changed files with 302 additions and 97 deletions

View 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 };

View File

@@ -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,

View File

@@ -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,

View File

@@ -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));

View File

@@ -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 };

View File

@@ -0,0 +1,4 @@
const INITIALIZE_FILTERS = 'INITIALIZE_FILTERS';
const UPDATE_ASSIGNMENT_FILTER = 'UPDATE_ASSIGNMENT_FILTER';
export { INITIALIZE_FILTERS, UPDATE_ASSIGNMENT_FILTER };

View File

@@ -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,

View 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;

View File

@@ -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:

View File

@@ -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);
});

View File

@@ -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;

View 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 };

View 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,
};

View File

@@ -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 };

View File

@@ -1,4 +1,4 @@
import getBulkManagementHistory from './grades';
import { getBulkManagementHistory } from './grades';
const genericHistoryRow = {
id: 5,

View File

@@ -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) {