diff --git a/package-lock.json b/package-lock.json
index 646b42d..886c345 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": {
diff --git a/package.json b/package.json
index 76e2dcb..3fae152 100755
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/Gradebook/gradebook.scss b/src/components/Gradebook/gradebook.scss
index 35f6189..d4c535e 100644
--- a/src/components/Gradebook/gradebook.scss
+++ b/src/components/Gradebook/gradebook.scss
@@ -1,3 +1,12 @@
+.student-filters{
+ display: flex;
+ .label{
+ padding-top: 30px;
+ }
+ .form-group{
+ margin-left: 10px;
+ }
+}
.gbook {
overflow-x: scroll;
diff --git a/src/components/Gradebook/index.jsx b/src/components/Gradebook/index.jsx
index 56d2dd6..bd72088 100644
--- a/src/components/Gradebook/index.jsx
+++ b/src/components/Gradebook/index.jsx
@@ -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
+ {(this.props.tracks.length > 0 || this.props.cohorts.length > 0) &&
+
+
+ Student Groups:
+
+ {this.props.tracks.length > 0 &&
+
+ }
+ {this.props.cohorts.length > 0 &&
+
+ }
+
+ }
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}
/>
@@ -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,
-// };
-
diff --git a/src/containers/GradebookPage/index.jsx b/src/containers/GradebookPage/index.jsx
index cbc052f..7c34b10 100644
--- a/src/containers/GradebookPage/index.jsx
+++ b/src/containers/GradebookPage/index.jsx
@@ -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));
diff --git a/src/data/actions/cohorts.js b/src/data/actions/cohorts.js
new file mode 100644
index 0000000..915c4f6
--- /dev/null
+++ b/src/data/actions/cohorts.js
@@ -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,
+};
diff --git a/src/data/actions/grades.js b/src/data/actions/grades.js
index 03caa63..ea1bdca 100644
--- a/src/data/actions/grades.js
+++ b/src/data/actions/grades.js
@@ -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) => {
diff --git a/src/data/actions/tracks.js b/src/data/actions/tracks.js
new file mode 100644
index 0000000..9b3bc28
--- /dev/null
+++ b/src/data/actions/tracks.js
@@ -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,
+};
diff --git a/src/data/constants/actionTypes/cohorts.js b/src/data/constants/actionTypes/cohorts.js
new file mode 100644
index 0000000..2027c26
--- /dev/null
+++ b/src/data/constants/actionTypes/cohorts.js
@@ -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,
+};
diff --git a/src/data/constants/actionTypes/tracks.js b/src/data/constants/actionTypes/tracks.js
new file mode 100644
index 0000000..aa89e98
--- /dev/null
+++ b/src/data/constants/actionTypes/tracks.js
@@ -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,
+};
diff --git a/src/data/reducers/cohorts.js b/src/data/reducers/cohorts.js
new file mode 100644
index 0000000..f53e8cd
--- /dev/null
+++ b/src/data/reducers/cohorts.js
@@ -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;
+
diff --git a/src/data/reducers/grades.js b/src/data/reducers/grades.js
index 45bec53..056c214 100644
--- a/src/data/reducers/grades.js
+++ b/src/data/reducers/grades.js
@@ -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 {
diff --git a/src/data/reducers/index.js b/src/data/reducers/index.js
index 041f3e6..f4b0ef4 100755
--- a/src/data/reducers/index.js
+++ b/src/data/reducers/index.js
@@ -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;
diff --git a/src/data/reducers/tracks.js b/src/data/reducers/tracks.js
new file mode 100644
index 0000000..1c1836e
--- /dev/null
+++ b/src/data/reducers/tracks.js
@@ -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;
+
diff --git a/src/data/services/LmsApiService.js b/src/data/services/LmsApiService.js
index ce41870..fef622b 100644
--- a/src/data/services/LmsApiService.js
+++ b/src/data/services/LmsApiService.js
@@ -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;