feat(filter): Create the cohorts and track dropdown for filtering students

This is to provide the feature to filter students on the gradebook by
cohorts or enrollment tracks
This commit is contained in:
Simon Chen
2018-11-09 13:09:36 -05:00
committed by Simon Chen
parent 2c890e53f8
commit a4dc135129
15 changed files with 351 additions and 40 deletions

29
package-lock.json generated
View File

@@ -7254,8 +7254,7 @@
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"decompress-response": {
"version": "3.3.0",
@@ -13485,6 +13484,16 @@
"cast-array": "~1.0.0",
"object-filter": "~1.0.2",
"query-string": "~2.4.1"
},
"dependencies": {
"query-string": {
"version": "2.4.2",
"resolved": "http://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
"integrity": "sha1-fbBmZCCAS6qSrp8miWKFWnYUPfs=",
"requires": {
"strict-uri-encode": "^1.0.0"
}
}
}
},
"make-dir": {
@@ -19187,11 +19196,19 @@
"dev": true
},
"query-string": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz",
"integrity": "sha1-fbBmZCCAS6qSrp8miWKFWnYUPfs=",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-6.2.0.tgz",
"integrity": "sha512-5wupExkIt8RYL4h/FE+WTg3JHk62e6fFPWtAZA9J5IWK1PfTfKkMS93HBUHcFpeYi9KsY5pFbh+ldvEyaz5MyA==",
"requires": {
"strict-uri-encode": "^1.0.0"
"decode-uri-component": "^0.2.0",
"strict-uri-encode": "^2.0.0"
},
"dependencies": {
"strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
}
}
},
"querystring": {

View File

@@ -33,6 +33,7 @@
"font-awesome": "^4.7.0",
"history": "^4.7.2",
"prop-types": "^15.5.10",
"query-string": "^6.2.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-redux": "^5.0.7",

View File

@@ -1,3 +1,12 @@
.student-filters{
display: flex;
.label{
padding-top: 30px;
}
.form-group{
margin-left: 10px;
}
}
.gbook {
overflow-x: scroll;

View File

@@ -1,7 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import emailPropType from 'email-prop-type';
import { Button, Modal, SearchField, Table } from '@edx/paragon';
import { Button, Modal, SearchField, Table, InputSelect } from '@edx/paragon';
import queryString from 'query-string';
export default class Gradebook extends React.Component {
@@ -21,7 +22,14 @@ export default class Gradebook extends React.Component {
}
componentDidMount() {
this.props.getUserGrades(this.props.match.params.courseId);
const urlQuery = queryString.parse(this.props.location.search);
this.props.getUserGrades(
this.props.match.params.courseId,
urlQuery.cohort,
urlQuery.track,
);
this.props.getTracks(this.props.match.params.courseId);
this.props.getCohorts(this.props.match.params.courseId);
}
sortAlphaDesc = (gradeRowA, gradeRowB) => {
@@ -223,6 +231,75 @@ export default class Gradebook extends React.Component {
},
]);
}
updateQueryParams = (queryKey, queryValue) => {
const parsed = queryString.parse(this.props.location.search);
parsed[queryKey] = queryValue;
return `?${queryString.stringify(parsed)}`;
};
mapCohortsEntries = (entries) => {
let mapped = entries.map(entry => ({
id: entry.id,
label: entry.name,
}));
mapped.unshift({id:0, label:'Cohorts'});
return mapped;
};
mapTracksEntries = (entries) => {
let mapped = entries.map(entry => ({
id: entry.slug,
label: entry.name,
}));
mapped.unshift({ label:'Tracks' });
return mapped;
};
updateTracks = (event) => {
const selectedTrackItem = this.props.tracks.find(x=>x.name===event);
let selectedTrackSlug = null;
if(selectedTrackItem) {
selectedTrackSlug = selectedTrackItem.slug;
}
this.props.getUserGrades(
this.props.match.params.courseId,
this.props.selectedCohort,
selectedTrackSlug,
);
const updatedQueryStrings = this.updateQueryParams('track', selectedTrackSlug)
this.props.history.push(updatedQueryStrings);
};
updateCohorts = (event) => {
const selectedCohortItem = this.props.cohorts.find(x=>x.name===event);
let selectedCohortId = null;
if(selectedCohortItem) {
selectedCohortId = selectedCohortItem.id;
}
this.props.getUserGrades(
this.props.match.params.courseId,
selectedCohortId,
this.props.selectedTrack,
);
const updatedQueryStrings = this.updateQueryParams('cohort', selectedCohortId)
this.props.history.push(updatedQueryStrings);
};
mapSelectedCohortEntry = (entry) => {
const selectedCohortEntry = this.props.cohorts.find(x => x.id === parseInt(entry, 10));
if (selectedCohortEntry) {
return selectedCohortEntry.name;
}
return 'Cohorts';
};
mapSelectedTrackEntry = (entry) => {
const selectedTrackEntry = this.props.tracks.find(x => x.slug === entry);
if (selectedTrackEntry) {
return selectedTrackEntry.name;
}
return 'Tracks';
};
render() {
return (
@@ -294,21 +371,44 @@ export default class Gradebook extends React.Component {
type="radio"
name="category"
value="exam"
onClick={() => this.setState({ headings: this.mapHeadingsExam(this.props.results[0]) })}
onClick={() => this.setState({ headings: this.mapHeadingsExam(this.props.results[0]) })}
/>
Exam
</label>
</span>
</div>
{(this.props.tracks.length > 0 || this.props.cohorts.length > 0) &&
<div className="student-filters">
<span className="label">
Student Groups:
</span>
{this.props.tracks.length > 0 &&
<InputSelect
name='Tracks'
value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
options={this.mapTracksEntries(this.props.tracks)}
onChange={this.updateTracks}
/>
}
{this.props.cohorts.length > 0 &&
<InputSelect
name='Cohorts'
value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
options={this.mapCohortsEntries(this.props.cohorts)}
onChange={this.updateCohorts}
/>
}
</div>
}
</div>
<div>
<div style={{ marginLeft: '10px', marginBottom: '10px' }}>
<a href="https://www.google./com">Download Grade Report</a>
</div>
<SearchField
onSubmit={value => this.props.searchForUser(this.props.match.params.courseId, value)}
onSubmit={value => this.props.searchForUser(this.props.match.params.courseId, value, this.props.selectedCohort, this.props.selectedTrack)}
onChange={filterValue => this.setState({ filterValue })}
onClear={() => this.props.getUserGrades(this.props.match.params.courseId)}
onClear={() => this.props.getUserGrades(this.props.match.params.courseId, this.props.selectedCohort, this.props.selectedTrack)}
value={this.state.filterValue}
/>
</div>
@@ -360,19 +460,3 @@ export default class Gradebook extends React.Component {
}
}
// CommentDetails.defaultProps = {
// id: null,
// postId: null,
// name: '',
// email: 'example@example.com',
// body: '',
// };
// CommentDetails.propTypes = {
// id: PropTypes.number,
// postId: PropTypes.number,
// name: PropTypes.string,
// email: emailPropType,
// body: PropTypes.string,
// };

View File

@@ -2,20 +2,32 @@ import { connect } from 'react-redux';
import Gradebook from '../../components/Gradebook';
import { fetchGrades, fetchMatchingUserGrades, updateGrades } from '../../data/actions/grades';
import { fetchCohorts } from '../../data/actions/cohorts';
import { fetchTracks } from '../../data/actions/tracks';
const mapStateToProps = state => (
{
grades: state.grades.results,
tracks: state.tracks.results,
cohorts: state.cohorts.results,
selectedTrack: state.grades.selectedTrack,
selectedCohort: state.grades.selectedCohort,
}
);
const mapDispatchToProps = dispatch => (
{
getUserGrades: (courseId) => {
dispatch(fetchGrades(courseId));
getUserGrades: (courseId, cohort, track) => {
dispatch(fetchGrades(courseId, cohort, track));
},
searchForUser: (courseId, searchText) => {
dispatch(fetchMatchingUserGrades(courseId, searchText));
searchForUser: (courseId, searchText, cohort, track) => {
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track));
},
getCohorts: (courseId) => {
dispatch(fetchCohorts(courseId));
},
getTracks: (courseId) => {
dispatch(fetchTracks(courseId));
},
updateGrades: (courseId, updateData) => {
dispatch(updateGrades(courseId, updateData));

View File

@@ -0,0 +1,31 @@
import {
STARTED_FETCHING_COHORTS,
GOT_COHORTS,
ERROR_FETCHING_COHORTS,
} from '../constants/actionTypes/cohorts';
import LmsApiService from '../services/LmsApiService';
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((error) => {
dispatch(errorFetchingCohorts());
});
}
);
export {
fetchCohorts,
startedFetchingCohorts,
gotCohorts,
errorFetchingCohorts,
};

View File

@@ -12,7 +12,12 @@ import LmsApiService from '../services/LmsApiService';
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
const gotGrades = grades => ({ type: GOT_GRADES, grades });
const gotGrades = (grades, cohort, track) => ({
type: GOT_GRADES,
grades,
cohort,
track,
});
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
const gradeUpdateSuccess = responseData => ({
@@ -24,13 +29,13 @@ const gradeUpdateFailure = error => ({
payload: { error },
});
const fetchGrades = courseId => (
const fetchGrades = (courseId, cohort, track) => (
(dispatch) => {
dispatch(startedFetchingGrades());
return LmsApiService.fetchGradebookData(courseId)
return LmsApiService.fetchGradebookData(courseId, null, cohort, track)
.then(response => response.data)
.then((data) => {
dispatch(gotGrades(data.results));
dispatch(gotGrades(data.results, cohort, track));
dispatch(finishedFetchingGrades());
})
.catch((error) => {
@@ -39,13 +44,13 @@ const fetchGrades = courseId => (
}
);
const fetchMatchingUserGrades = (courseId, searchText) => (
const fetchMatchingUserGrades = (courseId, searchText, cohort, track) => (
(dispatch) => {
dispatch(startedFetchingGrades());
return LmsApiService.fetchGradebookData(courseId, searchText)
return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track)
.then(response => response.data)
.then((data) => {
dispatch(gotGrades(data.results));
dispatch(gotGrades(data.results, cohort, track));
dispatch(finishedFetchingGrades());
})
.catch((error) => {

View File

@@ -0,0 +1,31 @@
import {
STARTED_FETCHING_TRACKS,
GOT_TRACKS,
ERROR_FETCHING_TRACKS,
} from '../constants/actionTypes/tracks';
import LmsApiService from '../services/LmsApiService';
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(response => response.data)
.then((data) => {
dispatch(gotTracks(data.course_modes));
})
.catch((error) => {
dispatch(errorFetchingTracks());
});
}
);
export {
fetchTracks,
startedFetchingTracks,
gotTracks,
errorFetchingTracks,
};

View File

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,39 @@
import {
STARTED_FETCHING_COHORTS,
ERROR_FETCHING_COHORTS,
GOT_COHORTS,
} from '../constants/actionTypes/cohorts';
const initialState = {
results: [],
startedFetching: false,
errorFetching: false,
};
const cohorts = (state = initialState, action) => {
switch (action.type) {
case GOT_COHORTS:
return {
...state,
results: action.cohorts,
errorFetching: false,
};
case STARTED_FETCHING_COHORTS:
return {
...state,
startedFetching: true,
};
case ERROR_FETCHING_COHORTS:
return {
...state,
finishedFetching: true,
errorFetching: true,
};
default:
return state;
}
};
export default cohorts;

View File

@@ -19,6 +19,8 @@ const grades = (state = initialState, action) => {
results: action.grades,
finishedFetching: true,
errorFetching: false,
selectedTrack: action.track,
selectedCohort: action.cohort,
};
case STARTED_FETCHING_GRADES:
return {

View File

@@ -1,9 +1,13 @@
import { combineReducers } from 'redux';
import cohorts from './cohorts';
import grades from './grades';
import tracks from './tracks';
const rootReducer = combineReducers({
grades,
cohorts,
tracks,
});
export default rootReducer;

View File

@@ -0,0 +1,39 @@
import {
STARTED_FETCHING_TRACKS,
ERROR_FETCHING_TRACKS,
GOT_TRACKS,
} from '../constants/actionTypes/tracks';
const initialState = {
results: [],
startedFetching: false,
errorFetching: false,
};
const tracks = (state = initialState, action) => {
switch (action.type) {
case GOT_TRACKS:
return {
...state,
results: action.tracks,
errorFetching: false,
};
case STARTED_FETCHING_TRACKS:
return {
...state,
startedFetching: true,
};
case ERROR_FETCHING_TRACKS:
return {
...state,
finishedFetching: true,
errorFetching: true,
};
default:
return state;
}
};
export default tracks;

View File

@@ -4,10 +4,19 @@ import { configuration } from '../../config';
class LmsApiService {
static baseUrl = configuration.LMS_BASE_URL;
static fetchGradebookData(courseId, searchText) {
static fetchGradebookData(courseId, searchText, cohort, track) {
let gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/`;
if (searchText || track || cohort) {
gradebookUrl += '?';
}
if (searchText) {
gradebookUrl += `?username_contains=${searchText}`;
gradebookUrl += `username_contains=${searchText}&`;
}
if (cohort) {
gradebookUrl += `cohort_id=${cohort}&`;
}
if (track) {
gradebookUrl += `enrollment_mode=${track}`;
}
return apiClient.get(gradebookUrl);
}
@@ -35,6 +44,16 @@ class LmsApiService {
const gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/bulk-update`;
return apiClient.post(gradebookUrl, updateData);
}
static fetchTracks(courseId) {
const trackUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/course/${courseId}`;
return apiClient.get(trackUrl);
}
static fetchCohorts(courseId) {
const cohortsUrl = `${LmsApiService.baseUrl}/courses/${courseId}/cohorts/`;
return apiClient.get(cohortsUrl);
}
}
export default LmsApiService;