Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85cf3e35e4 | ||
|
|
85fa6bca72 | ||
|
|
231685e78d | ||
|
|
a4dc135129 | ||
|
|
2c890e53f8 | ||
|
|
33556fd749 | ||
|
|
8a62e8b710 |
@@ -3,6 +3,7 @@
|
||||
const Merge = require('webpack-merge');
|
||||
const commonConfig = require('./webpack.common.config.js');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
@@ -91,5 +92,20 @@ module.exports = Merge.smart(commonConfig, {
|
||||
inject: true, // Appends script tags linking to the webpack bundles at the end of the body
|
||||
template: path.resolve(__dirname, '../public/index.html'),
|
||||
}),
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: 'production',
|
||||
BASE_URL: null,
|
||||
LMS_BASE_URL: null,
|
||||
LOGIN_URL: null,
|
||||
LOGOUT_URL: null,
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: null,
|
||||
DATA_API_BASE_URL: null,
|
||||
SEGMENT_KEY: null,
|
||||
FEATURE_FLAGS: {},
|
||||
ACCESS_TOKEN_COOKIE_NAME: null,
|
||||
CSRF_COOKIE_NAME: 'csrftoken',
|
||||
NEW_RELIC_APP_ID: null,
|
||||
NEW_RELIC_LICENSE_KEY: null,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
.student-filters{
|
||||
display: flex;
|
||||
.label{
|
||||
padding-top: 30px;
|
||||
}
|
||||
.form-group{
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.gbook {
|
||||
overflow-x: scroll;
|
||||
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import emailPropType from 'email-prop-type';
|
||||
import { SearchField, Table, Modal } from '@edx/paragon';
|
||||
import { Button, Modal, SearchField, Table, InputSelect } from '@edx/paragon';
|
||||
import queryString from 'query-string';
|
||||
|
||||
|
||||
export default class Gradebook extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
grades: [], // this.mapUserEnteriesPercent(this.props.grades).sort(this.sortAlphaDesc),
|
||||
grades: [], // this.mapUserEntriesPercent(this.props.grades).sort(this.sortAlphaDesc),
|
||||
headings: [], // this.mapHeadings(this.props.grades[0]),
|
||||
filterValue: '',
|
||||
modalContent: (<h1>Hello, World!</h1>),
|
||||
modalOpen: false,
|
||||
modalModel: [{}],
|
||||
updateVal: 0,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
};
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -150,25 +160,40 @@ export default class Gradebook extends React.Component {
|
||||
return results.concat(assignmentHeadings);
|
||||
};
|
||||
|
||||
mapUserEnteriesPercent = entries => entries.map((entry) => {
|
||||
setNewModalState = (userEntry, subsection) => {
|
||||
this.setState({
|
||||
modalModel: [{
|
||||
username: userEntry.username,
|
||||
currentGrade: `${subsection.score_earned}/${subsection.score_possible}`,
|
||||
adjustedGrade: (
|
||||
<span>
|
||||
<input
|
||||
style={{ width: '25px' }}
|
||||
type="text"
|
||||
onChange={(event) => this.setState({updateVal: event.target.value})}
|
||||
/> / {subsection.score_possible}
|
||||
</span>
|
||||
),
|
||||
assignmentName: `${subsection.subsection_name}`,
|
||||
}],
|
||||
modalOpen: true,
|
||||
updateModuleId: subsection.module_id,
|
||||
updateUserId: userEntry.user_id,
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
mapUserEntriesPercent = entries => entries.map((entry) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, s) => {
|
||||
acc[s.label] = (
|
||||
.reduce((acc, subsection) => {
|
||||
acc[subsection.label] = (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={() => this.setState({
|
||||
modalModel: [{
|
||||
username: entry.username,
|
||||
autoGrade: `${s.score_earned}/${s.score_possible}`,
|
||||
adjustedGrade: (<span><input style={{ width: '25px' }} type="text" value={this.updateVal} /> / {s.score_possible}</span>),
|
||||
assignmentName: `${s.subsection_name}`,
|
||||
}],
|
||||
modalOpen: true,
|
||||
})}
|
||||
onClick={() => this.setNewModalState(entry, subsection)}
|
||||
>
|
||||
{s.percent}
|
||||
{subsection.percent}
|
||||
</button>);
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -176,25 +201,17 @@ export default class Gradebook extends React.Component {
|
||||
return Object.assign(results, assignments, totals);
|
||||
});
|
||||
|
||||
mapUserEnteriesAbsolute = entries => entries.map((entry) => {
|
||||
mapUserEntriesAbsolute = entries => entries.map((entry) => {
|
||||
const results = { username: entry.username };
|
||||
const assignments = entry.section_breakdown
|
||||
.filter(section => section.is_graded)
|
||||
.reduce((acc, s) => {
|
||||
acc[s.label] = (
|
||||
.reduce((acc, subsection) => {
|
||||
acc[subsection.label] = (
|
||||
<button
|
||||
className="btn btn-header link-style"
|
||||
onClick={() => this.setState({
|
||||
modalModel: [{
|
||||
username: entry.username,
|
||||
autoGrade: `${s.score_earned}/${s.score_possible}`,
|
||||
adjustedGrade: (<span><input style={{ width: '25px' }} type="text" value={this.updateVal} /> / {s.score_possible}</span>),
|
||||
assignmentName: `${s.subsection_name}`,
|
||||
}],
|
||||
modalOpen: true,
|
||||
})}
|
||||
onClick={() => this.setNewModalState(entry, subsection)}
|
||||
>
|
||||
{s.score_earned}/{s.score_possible}
|
||||
{subsection.score_earned}/{subsection.score_possible}
|
||||
</button>);
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -203,6 +220,87 @@ export default class Gradebook extends React.Component {
|
||||
return Object.assign(results, assignments, totals);
|
||||
});
|
||||
|
||||
handleAdjustedGradeClick = () => {
|
||||
this.props.updateGrades(this.props.match.params.courseId, [
|
||||
{
|
||||
'user_id': this.state.updateUserId,
|
||||
'usage_id': this.state.updateModuleId,
|
||||
'grade': {
|
||||
'earned_graded_override': this.state.updateVal,
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
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 (
|
||||
<div className="d-flex justify-content-center">
|
||||
@@ -221,7 +319,7 @@ export default class Gradebook extends React.Component {
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="percent"
|
||||
onClick={() => this.setState({ grades: this.mapUserEnteriesPercent(this.props.results).sort(this.sortAlphaDesc) })}
|
||||
onClick={() => this.setState({ grades: this.mapUserEntriesPercent(this.props.results).sort(this.sortAlphaDesc) })}
|
||||
/>
|
||||
<label className="ml-2 mr-2" htmlFor="score-view-percent">Percent</label>
|
||||
</span>
|
||||
@@ -231,7 +329,7 @@ export default class Gradebook extends React.Component {
|
||||
type="radio"
|
||||
name="score-view"
|
||||
value="absolute"
|
||||
onClick={() => this.setState({ grades: this.mapUserEnteriesAbsolute(this.props.results).sort(this.sortAlphaDesc) })}
|
||||
onClick={() => this.setState({ grades: this.mapUserEntriesAbsolute(this.props.results).sort(this.sortAlphaDesc) })}
|
||||
/>
|
||||
<label className="ml-2 mr-2" htmlFor="score-view-absolute">Absolute</label>
|
||||
</span>
|
||||
@@ -273,24 +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={() => this.setState({ grades: this.mapUserEnteriesPercent(this.props.results).filter(entry => entry.username === '' || entry.username.includes(this.state.filterValue)) }) }
|
||||
onSubmit={value => this.props.searchForUser(this.props.match.params.courseId, value, this.props.selectedCohort, this.props.selectedTrack)}
|
||||
onChange={filterValue => this.setState({ filterValue })}
|
||||
onClear={() => this.setState({
|
||||
grades: this.mapUserEnteriesPercent(this.props.results)
|
||||
.sort(this.sortAlphaDesc),
|
||||
})}
|
||||
onClear={() => this.props.getUserGrades(this.props.match.params.courseId, this.props.selectedCohort, this.props.selectedTrack)}
|
||||
value={this.state.filterValue}
|
||||
/>
|
||||
</div>
|
||||
@@ -299,7 +417,7 @@ export default class Gradebook extends React.Component {
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={this.mapHeadings(this.props.grades[0])}
|
||||
data={this.mapUserEnteriesPercent(this.props.grades)}
|
||||
data={this.mapUserEntriesPercent(this.props.grades)}
|
||||
tableSortable
|
||||
defaultSortDirection="desc"
|
||||
defaultSortedColumn="username"
|
||||
@@ -312,15 +430,28 @@ export default class Gradebook extends React.Component {
|
||||
<div>
|
||||
<h3>{this.state.modalModel[0].assignmentName}</h3>
|
||||
<Table
|
||||
columns={[{ label: 'Username', key: 'username' }, { label: 'Auto grade', key: 'autoGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
|
||||
columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
|
||||
data={this.state.modalModel}
|
||||
tableSortable
|
||||
defaultSortDirection="desc"
|
||||
defaultSortedColumn="username"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onClose={() => this.setState({ modalOpen: false })}
|
||||
)}
|
||||
buttons={[
|
||||
<Button
|
||||
label="Edit Grade"
|
||||
buttonType="primary"
|
||||
onClick={this.handleAdjustedGradeClick}
|
||||
/>
|
||||
]}
|
||||
onClose={() => this.setState({
|
||||
modalOpen: false,
|
||||
modalModel: [{}],
|
||||
updateVal: 0,
|
||||
updateModuleId: null,
|
||||
updateUserId: null,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,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,
|
||||
// };
|
||||
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Gradebook from '../../components/Gradebook';
|
||||
import { fetchGrades } from '../../data/actions/grades';
|
||||
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, 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));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
31
src/data/actions/cohorts.js
Normal file
31
src/data/actions/cohorts.js
Normal 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,
|
||||
};
|
||||
@@ -3,25 +3,72 @@ import {
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
GRADE_UPDATE_REQUEST,
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
} from '../constants/actionTypes/grades';
|
||||
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 fetchGrades = courseId => (
|
||||
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
|
||||
const gradeUpdateSuccess = responseData => ({
|
||||
type: GRADE_UPDATE_SUCCESS,
|
||||
payload: { responseData },
|
||||
})
|
||||
const gradeUpdateFailure = error => ({
|
||||
type: GRADE_UPDATE_FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
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) => {
|
||||
dispatch(errorFetchingGrades())
|
||||
dispatch(errorFetchingGrades());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const fetchMatchingUserGrades = (courseId, searchText, cohort, track) => (
|
||||
(dispatch) => {
|
||||
dispatch(startedFetchingGrades());
|
||||
return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gotGrades(data.results, cohort, track));
|
||||
dispatch(finishedFetchingGrades());
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(errorFetchingGrades());
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const updateGrades = (courseId, updateData) => (
|
||||
(dispatch) => {
|
||||
dispatch(gradeUpdateRequest());
|
||||
return LmsApiService.updateGradebookData(courseId, updateData)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
dispatch(gradeUpdateSuccess(data))
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(gradeUpdateFailure(error));
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -32,4 +79,9 @@ export {
|
||||
errorFetchingGrades,
|
||||
gotGrades,
|
||||
fetchGrades,
|
||||
fetchMatchingUserGrades,
|
||||
gradeUpdateRequest,
|
||||
gradeUpdateSuccess,
|
||||
gradeUpdateFailure,
|
||||
updateGrades,
|
||||
};
|
||||
|
||||
31
src/data/actions/tracks.js
Normal file
31
src/data/actions/tracks.js
Normal 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,
|
||||
};
|
||||
9
src/data/constants/actionTypes/cohorts.js
Normal file
9
src/data/constants/actionTypes/cohorts.js
Normal 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,
|
||||
};
|
||||
@@ -3,9 +3,16 @@ const FINISHED_FETCHING_GRADES = 'FINISHED_FETCHING_GRADES';
|
||||
const ERROR_FETCHING_GRADES = 'ERROR_FETCHING_GRADES';
|
||||
const GOT_GRADES = 'GOT_GRADES';
|
||||
|
||||
const GRADE_UPDATE_REQUEST = 'GRADE_UPDATE_REQUEST';
|
||||
const GRADE_UPDATE_SUCCESS = 'GRADE_UPDATE_SUCCESS';
|
||||
const GRADE_UPDATE_FAILURE = 'GRADE_UPDATE_FAILURE';
|
||||
|
||||
export {
|
||||
STARTED_FETCHING_GRADES,
|
||||
FINISHED_FETCHING_GRADES,
|
||||
ERROR_FETCHING_GRADES,
|
||||
GOT_GRADES,
|
||||
GRADE_UPDATE_REQUEST,
|
||||
GRADE_UPDATE_SUCCESS,
|
||||
GRADE_UPDATE_FAILURE,
|
||||
};
|
||||
|
||||
9
src/data/constants/actionTypes/tracks.js
Normal file
9
src/data/constants/actionTypes/tracks.js
Normal 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,
|
||||
};
|
||||
39
src/data/reducers/cohorts.js
Normal file
39
src/data/reducers/cohorts.js
Normal 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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
39
src/data/reducers/tracks.js
Normal file
39
src/data/reducers/tracks.js
Normal 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;
|
||||
|
||||
@@ -4,11 +4,56 @@ import { configuration } from '../../config';
|
||||
class LmsApiService {
|
||||
static baseUrl = configuration.LMS_BASE_URL;
|
||||
|
||||
static fetchGradebookData(courseId) {
|
||||
const fixedCourseId = 'course-v1:edX+DemoX+Demo_Course'; // TODO: get rid of this in favor of courseId
|
||||
const gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${fixedCourseId}/`;
|
||||
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}&`;
|
||||
}
|
||||
if (cohort) {
|
||||
gradebookUrl += `cohort_id=${cohort}&`;
|
||||
}
|
||||
if (track) {
|
||||
gradebookUrl += `enrollment_mode=${track}`;
|
||||
}
|
||||
return apiClient.get(gradebookUrl);
|
||||
}
|
||||
|
||||
static updateGradebookData(courseId, updateData) {
|
||||
/*
|
||||
updateData is expected to be a list of objects with the keys 'user_id' (an integer),
|
||||
'usage_id' (a string) and 'grade', which is an object with the keys:
|
||||
'earned_all_override', 'possible_all_override', 'earned_graded_override', and 'possible_graded_override',
|
||||
each of which should be an integer.
|
||||
Example:
|
||||
[
|
||||
{
|
||||
"user_id": 9,
|
||||
"usage_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions",
|
||||
"grade": {
|
||||
"earned_all_override": 11,
|
||||
"possible_all_override": 11,
|
||||
"earned_graded_override": 11,
|
||||
"possible_graded_override": 11
|
||||
}
|
||||
}
|
||||
]
|
||||
*/
|
||||
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;
|
||||
|
||||
@@ -21,32 +21,3 @@ const App = () => (
|
||||
);
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
|
||||
// const App = () => (
|
||||
// <Provider store={store}>
|
||||
// <Router>
|
||||
// <div>
|
||||
// <header>
|
||||
// <nav>
|
||||
// <ul className="nav">
|
||||
// <li className="nav-item"><Link className="nav-link" to="/">Home</Link></li>
|
||||
// <li className="nav-item"><Link className="nav-link" to="/posts">Posts</Link></li>
|
||||
// <li className="nav-item"><Link className="nav-link" to="/disclosure">Disclosure</Link></li>
|
||||
// <li className="nav-item"><Link className="nav-link" to="/comment-search">Comment Search</Link></li>
|
||||
// </ul>
|
||||
// </nav>
|
||||
// </header>
|
||||
// <main>
|
||||
// <Switch>
|
||||
// <Route exact path="/" component={() => <span>Hello World</span>} />
|
||||
// <Route path="/posts" component={PostsPage} />
|
||||
// <Route path="/disclosure" component={DisclosurePage} />
|
||||
// <Route path="/comment-search" component={CommentSearchPage} />
|
||||
// </Switch>
|
||||
// </main>
|
||||
// </div>
|
||||
// </Router>
|
||||
// </Provider>
|
||||
// );
|
||||
|
||||
// ReactDOM.render(<App />, document.getElementById('root'));
|
||||
Reference in New Issue
Block a user