Compare commits

...

18 Commits

Author SHA1 Message Date
Simon Chen
dd82054bbc Merge pull request #39 from edx/schen/setup-test
feat(test): Setup unit testing
2018-12-05 14:20:58 -05:00
Jansen Kantor
6a4bc67841 Merge pull request #38 from edx/encoding
fix(UI) specify utf8 to avoid incorrect character rendering
2018-12-05 14:15:51 -05:00
Simon Chen
adfefac85d feat(test): Setup unit testing 2018-12-05 13:52:33 -05:00
jkantor
c92144c436 fix(UI) specify utf8 to avoid incorrect character rendering 2018-12-05 13:35:28 -05:00
Jansen Kantor
ca0156ea4c Merge pull request #36 from edx/rounded-percents
fix(UI) rounded percentages to two decimal places
2018-12-05 13:33:38 -05:00
jkantor
61c4bc11bd fix(UI) rounded percentages to two decimal places 2018-12-05 10:53:55 -05:00
Jansen Kantor
db25a18f9d Merge pull request #35 from edx/updateMessage-filter
fix(UI) box should appear after editing grade
2018-12-04 14:01:05 -05:00
jkantor
0d7fa18acd fix(UI) box should appear after editing grade 2018-12-04 13:38:27 -05:00
Simon Chen
012bb3a1f3 Merge pull request #34 from edx/schen/EDUCATOR-3754
Add hardcoded page size on frontend
2018-12-04 09:58:43 -05:00
Simon Chen
de233e0285 fix(pagination): Add hardcoded page size on frontend 2018-12-03 16:04:41 -05:00
Richard I Reilly
ae7544cd53 Merge pull request #31 from edx/rir/spinner
Show a spinner when waiting for the grades call to come back
2018-12-03 15:04:09 -05:00
Rick Reilly
14df81b312 Show a spinner when waiting for the grades call to come back 2018-12-03 14:54:07 -05:00
Jansen Kantor
4706cfcd94 Merge pull request #30 from edx/retain_filter
fix(filter) filter should remain active after we edit a grade
2018-11-30 15:52:45 -05:00
jkantor
1f5a2469b2 fix(filter) filter should remain active after we edit a grade 2018-11-30 12:53:18 -05:00
Simon Chen
e31c670938 Merge pull request #29 from edx/schen/percent
fix(UI): Update the percent number view so it is actually percent
2018-11-30 12:07:47 -05:00
Simon Chen
db9f683297 fix(UI): Update the percent number view so it is actually percent with symbols 2018-11-30 11:55:02 -05:00
Richard I Reilly
7a43cdcaea Merge pull request #27 from edx/rir/dynamic-assignments
Make it so the assignment time column filter is dynamic and api driven
2018-11-30 11:08:59 -05:00
Rick Reilly
d5637a4550 Make it so the assignment time column filter is dynamic and api driven 2018-11-30 10:56:08 -05:00
17 changed files with 312 additions and 105 deletions

View File

@@ -30,3 +30,6 @@ restart-detached:
validate-no-uncommitted-package-lock-changes:
git diff --exit-code package-lock.json
test:
docker exec -it edx.gradebook jest

11
package-lock.json generated
View File

@@ -3552,7 +3552,7 @@
},
"@types/object-assign": {
"version": "4.0.30",
"resolved": "http://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI="
},
"@types/tapable": {
@@ -4161,6 +4161,15 @@
"is-buffer": "^1.1.5"
}
},
"axios-mock-adapter": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.15.0.tgz",
"integrity": "sha1-+8BoJdgwLJXDM00hAju6mWJV1F0=",
"dev": true,
"requires": {
"deep-equal": "^1.0.1"
}
},
"axobject-query": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz",

View File

@@ -47,6 +47,7 @@
"whatwg-fetch": "^2.0.3"
},
"devDependencies": {
"axios-mock-adapter": "^1.15.0",
"babel-cli": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-jest": "^22.4.0",

View File

@@ -1,6 +1,8 @@
<!doctype html>
<html>
<head></head>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="root"></div>
</body>

View File

@@ -1,3 +1,20 @@
.spinner-overlay {
position: absolute;
height: 100%;
width: 100%;
background-color: #999;
opacity: 0.5;
z-index: 99999;
display:flex;
align-items: flex-start;
justify-content: center;
padding: 200px;
}
.color-black {
color: black;
}
.gradebook-container{
width: 500px;
@media only screen and (min-width: 640px) {
@@ -10,6 +27,7 @@
width: 1024px;
}
}
.back-link{
float:right;
}

View File

@@ -1,8 +1,18 @@
import React from 'react';
import { Button, InputSelect, Modal, SearchField, StatusAlert, Table } from '@edx/paragon';
import {
Button,
InputSelect,
Modal,
SearchField,
StatusAlert,
Table,
Icon,
} from '@edx/paragon';
import queryString from 'query-string';
import { configuration } from '../../config';
const DECIMAL_PRECISION = 2;
export default class Gradebook extends React.Component {
constructor(props) {
super(props);
@@ -25,6 +35,7 @@ export default class Gradebook extends React.Component {
);
this.props.getTracks(this.props.match.params.courseId);
this.props.getCohorts(this.props.match.params.courseId);
this.props.getAssignmentTypes(this.props.match.params.courseId);
}
setNewModalState = (userEntry, subsection) => {
@@ -50,15 +61,20 @@ export default class Gradebook extends React.Component {
}
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,
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,
},
},
},
]);
],
this.state.filterValue,
this.props.selectedCohort,
this.props.selectedTrack,
);
this.setState({
modalModel: [{}],
@@ -74,6 +90,15 @@ export default class Gradebook extends React.Component {
return `?${queryString.stringify(parsed)}`;
};
mapAssignmentTypeEntries = (entries) => {
const mapped = entries.map(entry => ({
id: entry,
label: entry,
}));
mapped.unshift({ id: 0, label: 'All' });
return mapped;
};
mapCohortsEntries = (entries) => {
const mapped = entries.map(entry => ({
id: entry.id,
@@ -92,6 +117,10 @@ export default class Gradebook extends React.Component {
return mapped;
};
updateAssignmentTypes = (event) => {
this.props.filterColumns(event, this.props.grades[0]);
}
updateTracks = (event) => {
const selectedTrackItem = this.props.tracks.find(x => x.name === event);
let selectedTrackSlug = null;
@@ -122,6 +151,15 @@ export default class Gradebook extends React.Component {
this.props.history.push(updatedQueryStrings);
};
mapSelectedAssignmentTypeEntry = (entry) => {
const selectedAssignmentTypeEntry = this.props.assignmentTypes
.find(x => x.id === parseInt(entry, 10));
if (selectedAssignmentTypeEntry) {
return selectedAssignmentTypeEntry.name;
}
return 'All';
};
mapSelectedCohortEntry = (entry) => {
const selectedCohortEntry = this.props.cohorts.find(x => x.id === parseInt(entry, 10));
if (selectedCohortEntry) {
@@ -138,6 +176,8 @@ export default class Gradebook extends React.Component {
return 'Tracks';
};
roundPercentageGrade = percent => parseFloat(percent.toFixed(DECIMAL_PRECISION));
formatter = {
percent: entries => entries.map((entry) => {
const results = { username: entry.username };
@@ -149,11 +189,11 @@ export default class Gradebook extends React.Component {
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{subsection.percent}
{this.roundPercentageGrade(subsection.percent * 100)}%
</button>);
return acc;
}, {});
const totals = { total: entry.percent * 100 };
const totals = { total: `${this.roundPercentageGrade(entry.percent * 100)}%` };
return Object.assign(results, assignments, totals);
}),
@@ -172,7 +212,7 @@ export default class Gradebook extends React.Component {
return acc;
}, {});
const totals = { total: entry.percent * 100 };
const totals = { total: `${entry.percent * 100}/100` };
return Object.assign(results, assignments, totals);
}),
};
@@ -182,6 +222,7 @@ export default class Gradebook extends React.Component {
render() {
return (
<div className="d-flex justify-content-center">
{ this.props.showSpinner && <div className="spinner-overlay"><Icon className={['fa', 'fa-spinner', 'fa-spin', 'fa-5x', 'color-black']} /></div>}
<div className="card gradebook-container">
<div className="card-body">
<a
@@ -220,46 +261,19 @@ export default class Gradebook extends React.Component {
<label htmlFor="score-view-absolute">Absolute</label>
</span>
</div>
<div>
Category:
<span>
<input
id="category-all"
className="ml-2 mr-1"
type="radio"
name="category"
value="all"
onClick={() => this.props.filterColumns('all', this.props.grades[0])}
{ this.props.assignmnetTypes.length > 0 &&
<div className="student-filters">
<span className="label">
Assignment Types:
</span>
<InputSelect
name="assignment-types"
value={this.mapSelectedTrackEntry(this.props.selectedAssignmentType)}
options={this.mapAssignmentTypeEntries(this.props.assignmnetTypes)}
onChange={this.updateAssignmentTypes}
/>
<label className="mr-2" htmlFor="category-all">
All
</label>
</span>
<span>
<input
id="category-homework"
className="mr-1"
type="radio"
name="category"
value="homework"
onClick={() => this.props.filterColumns('hw', this.props.grades[0])}
/>
<label className="mr-2" htmlFor="category-homework">Homework</label>
</span>
<span>
<input
id="category-exam"
type="radio"
name="category"
value="exam"
className="ml-2 mr-1"
onClick={() => this.props.filterColumns('exam', this.props.grades[0])}
/>
<label htmlFor="category-exam">
Exam
</label>
</span>
</div>
</div>
}
{(this.props.tracks.length > 0 || this.props.cohorts.length > 0) &&
<div className="student-filters">
<span className="label">
@@ -294,14 +308,14 @@ export default class Gradebook extends React.Component {
onClear={() => this.props.getUserGrades(this.props.match.params.courseId, this.props.selectedCohort, this.props.selectedTrack)}
value={this.state.filterValue}
/>
<div className="d-flex justify-content-end" style={{ marginTop : '20px'}}>
<div className="d-flex justify-content-end" style={{ marginTop: '20px' }}>
<Button
label="Previous"
buttonType="primary"
style={{ visibility: (!this.props.prevPage ? 'hidden' : 'visible') }}
onClick={() => this.props.getPrevNextGrades(this.props.prevPage, this.props.selectedCohort, this.props.selectedTrack)}
/>
<div style={{width: '10px'}} />
<div style={{ width: '10px' }} />
<Button
label="Next"
buttonType="primary"

View File

@@ -12,6 +12,7 @@ import {
} from '../../data/actions/grades';
import { fetchCohorts } from '../../data/actions/cohorts';
import { fetchTracks } from '../../data/actions/tracks';
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
const mapStateToProps = state => (
{
@@ -25,6 +26,8 @@ const mapStateToProps = state => (
showSuccess: state.grades.showSuccess,
prevPage: state.grades.prevPage,
nextPage: state.grades.nextPage,
assignmnetTypes: state.assignmentTypes.results,
showSpinner: state.grades.showSpinner,
}
);
@@ -34,9 +37,9 @@ const mapDispatchToProps = dispatch => (
dispatch(fetchGrades(courseId, cohort, track));
},
searchForUser: (courseId, searchText, cohort, track) => {
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track));
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, false));
},
getPrevNextGrades : (endpoint, cohort, track) => {
getPrevNextGrades: (endpoint, cohort, track) => {
dispatch(fetchPrevNextGrades(endpoint, cohort, track));
},
getCohorts: (courseId) => {
@@ -45,8 +48,11 @@ const mapDispatchToProps = dispatch => (
getTracks: (courseId) => {
dispatch(fetchTracks(courseId));
},
updateGrades: (courseId, updateData) => {
dispatch(updateGrades(courseId, updateData));
getAssignmentTypes: (courseId) => {
dispatch(fetchAssignmentTypes(courseId));
},
updateGrades: (courseId, updateData, searchText, cohort, track) => {
dispatch(updateGrades(courseId, updateData, searchText, cohort, track));
},
toggleFormat: (formatType) => {
dispatch(toggleGradeFormat(formatType));

View File

@@ -0,0 +1,32 @@
import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
} from '../constants/actionTypes/assignmentTypes';
import LmsApiService from '../services/LmsApiService';
const startedFetchingAssignmentTypes = () => ({ type: STARTED_FETCHING_ASSIGNMENT_TYPES });
const errorFetchingAssignmentTypes = () => ({ type: ERROR_FETCHING_ASSIGNMENT_TYPES });
const gotAssignmentTypes = assignmentTypes => ({ type: GOT_ASSIGNMENT_TYPES, assignmentTypes });
const fetchAssignmentTypes = courseId => (
(dispatch) => {
dispatch(startedFetchingAssignmentTypes());
return LmsApiService.fetchAssignmentTypes(courseId)
.then(response => response.data)
.then((data) => {
dispatch(gotAssignmentTypes(Object.keys(data.assignment_types)));
})
.catch((error) => {
dispatch(errorFetchingAssignmentTypes());
});
}
);
export {
fetchAssignmentTypes,
startedFetchingAssignmentTypes,
gotAssignmentTypes,
errorFetchingAssignmentTypes,
};

View File

@@ -0,0 +1,73 @@
import configureMockStore from 'redux-mock-store';
import MockAdapter from 'axios-mock-adapter';
import thunk from 'redux-thunk';
import apiClient from '../apiClient';
import { fetchCohorts } from './cohorts';
import {
STARTED_FETCHING_COHORTS,
GOT_COHORTS,
ERROR_FETCHING_COHORTS,
} from '../constants/actionTypes/cohorts';
const mockStore = configureMockStore([thunk]);
const axiosMock = new MockAdapter(apiClient);
describe('actions', () => {
afterEach(() => {
axiosMock.reset();
});
describe('fetchCohorts', () => {
const courseId = 'course-v1:edX+DemoX+Demo_Course';
it('dispatches success action after fetching cohorts', () => {
const responseData = {
cohorts: [
{
assignment_type: 'manual',
group_id: null,
id: 1,
name: 'default_group',
user_count: 2,
user_partition_id: null,
},
{
assignment_type: 'auto',
group_id: null,
id: 2,
name: 'auto_group',
user_count: 5,
user_partition_id: null,
}],
};
const expectedActions = [
{ type: STARTED_FETCHING_COHORTS },
{ type: GOT_COHORTS, cohorts: responseData.cohorts },
];
const store = mockStore();
axiosMock.onGet(`http://localhost:18000/courses/${courseId}/cohorts/`)
.replyOnce(200, JSON.stringify(responseData));
return store.dispatch(fetchCohorts(courseId)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it('dispatches failure action after fetching cohorts', () => {
const expectedActions = [
{ type: STARTED_FETCHING_COHORTS },
{ type: ERROR_FETCHING_COHORTS },
];
const store = mockStore();
axiosMock.onGet(`http://localhost:18000/courses/${courseId}/cohorts/`)
.replyOnce(500, JSON.stringify({}));
return store.dispatch(fetchCohorts(courseId)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});

View File

@@ -14,8 +14,9 @@ import {
import LmsApiService from '../services/LmsApiService';
import store from '../store';
import { headingMapper, gradeSortMap, sortAlphaAsc } from './utils';
import apiClient from "../apiClient";
import apiClient from '../apiClient';
const defaultAssignmentFilter = 'All';
const sortGrades = (columnName, direction) => {
const sortFn = gradeSortMap(columnName, direction);
@@ -55,10 +56,11 @@ const gradeUpdateFailure = error => ({
const toggleGradeFormat = formatType => ({ type: TOGGLE_GRADE_FORMAT, formatType });
const filterColumns = (filterType, exampleUser) => (
dispatch => ({
dispatch => dispatch({
type: FILTER_COLUMNS,
headings: headingMapper[filterType](dispatch, exampleUser),
}));
headings: headingMapper(filterType)(dispatch, exampleUser),
})
);
const updateBanner = showSuccess => ({ type: UPDATE_BANNER, showSuccess });
@@ -72,7 +74,7 @@ const fetchGrades = (courseId, cohort, track, showSuccess) => (
data.results.sort(sortAlphaAsc),
cohort,
track,
headingMapper.all(dispatch, data.results[0]),
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]),
data.previous,
data.next,
));
@@ -85,7 +87,7 @@ const fetchGrades = (courseId, cohort, track, showSuccess) => (
}
);
const fetchMatchingUserGrades = (courseId, searchText, cohort, track) => (
const fetchMatchingUserGrades = (courseId, searchText, cohort, track, showSuccess) => (
(dispatch) => {
dispatch(startedFetchingGrades());
return LmsApiService.fetchGradebookData(courseId, searchText, cohort, track)
@@ -95,11 +97,12 @@ const fetchMatchingUserGrades = (courseId, searchText, cohort, track) => (
data.results.sort(sortAlphaAsc),
cohort,
track,
headingMapper.all(dispatch, data.results[0]),
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]),
data.previous,
data.next,
));
dispatch(finishedFetchingGrades());
dispatch(updateBanner(showSuccess));
})
.catch(() => {
dispatch(errorFetchingGrades());
@@ -117,7 +120,7 @@ const fetchPrevNextGrades = (endpoint, cohort, track) => (
data.results.sort(sortAlphaAsc),
cohort,
track,
headingMapper.all(dispatch, data.results[0]),
headingMapper(defaultAssignmentFilter)(dispatch, data.results[0]),
data.previous,
data.next,
));
@@ -130,14 +133,14 @@ const fetchPrevNextGrades = (endpoint, cohort, track) => (
);
const updateGrades = (courseId, updateData) => (
const updateGrades = (courseId, updateData, searchText, cohort, track) => (
(dispatch) => {
dispatch(gradeUpdateRequest());
return LmsApiService.updateGradebookData(courseId, updateData)
.then(response => response.data)
.then((data) => {
dispatch(gradeUpdateSuccess(data));
dispatch(fetchGrades(courseId, null, null, true));
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track, true));
})
.catch((error) => {
dispatch(gradeUpdateFailure(error));

View File

@@ -60,8 +60,8 @@ function gradeSortMap(columnName, direction) {
return sortNumerically(columnName, direction);
}
const headingMapper = {
all: (dispatch, entry) => {
const headingMapper = (filterKey) => {
function all(dispatch, entry) {
if (entry) {
const results = [{
label: 'Username',
@@ -89,21 +89,18 @@ const headingMapper = {
return results.concat(assignmentHeadings).concat(totals);
}
return [];
},
hw: (dispatch, entry) => {
}
function some(dispatch, entry) {
const results = [{
label: 'Username',
key: 'username',
columnSortable: true,
onSort: (direction) => {
this.setState({
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc),
});
},
onSort: (direction) => { dispatch(sortGrades('username', direction)); },
}];
const assignmentHeadings = entry.section_breakdown
.filter(section => section.is_graded && section.label && section.category == 'Homework')
.filter(section => section.is_graded && section.label && section.category === filterKey)
.map(s => ({
label: s.label,
key: s.label,
@@ -111,31 +108,17 @@ const headingMapper = {
onSort: (direction) => { this.sortNumerically(s.label, direction); },
}));
return results.concat(assignmentHeadings);
},
exam: (dispatch, entry) => {
const results = [{
label: 'Username',
key: 'username',
columnSortable: false,
onSort: (direction) => {
this.setState({
grades: [...this.state.grades].sort(direction === 'desc' ? this.sortAlphaDesc : this.sortAlphaAsc),
});
},
const totals = [{
label: 'Total',
key: 'total',
columnSortable: true,
onSort: direction => dispatch(sortGrades('total', direction)),
}];
const assignmentHeadings = entry.section_breakdown
.filter(section => section.is_graded && section.label && section.category == 'Exam')
.map(s => ({
label: s.label,
key: s.label,
columnSortable: false,
onSort: (direction) => { this.sortNumerically(s.label, direction); },
}));
return results.concat(assignmentHeadings).concat(totals);
}
return results.concat(assignmentHeadings);
},
return filterKey === 'All' ? all : some;
};
export { headingMapper, gradeSortMap, sortAlphaAsc };

View File

@@ -0,0 +1,10 @@
const STARTED_FETCHING_ASSIGNMENT_TYPES = 'STARTED_FETCHING_ASSIGNMENT_TYPES';
const GOT_ASSIGNMENT_TYPES = 'GOT_ASSIGNMENT_TYPES';
const ERROR_FETCHING_ASSIGNMENT_TYPES = 'ERROR_FETCHING_ASSIGNMENT_TYPES';
export {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
};

View File

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

View File

@@ -19,6 +19,7 @@ const initialState = {
showSuccess: false,
prevPage: null,
nextPage: null,
showSpinner: true,
};
const grades = (state = initialState, action) => {
@@ -34,12 +35,14 @@ const grades = (state = initialState, action) => {
selectedCohort: action.cohort,
prevPage: action.prev,
nextPage: action.next,
showSpinner: false,
};
case STARTED_FETCHING_GRADES:
return {
...state,
startedFetching: true,
finishedFetching: false,
showSpinner: true,
};
case ERROR_FETCHING_GRADES:
return {

View File

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

View File

@@ -3,12 +3,12 @@ import { configuration } from '../../config';
class LmsApiService {
static baseUrl = configuration.LMS_BASE_URL;
static pageSize = 10
static fetchGradebookData(courseId, searchText, cohort, track) {
let gradebookUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/`;
if (searchText || track || cohort) {
gradebookUrl += '?';
}
gradebookUrl += `?page_size=${LmsApiService.pageSize}&`;
if (searchText) {
gradebookUrl += `username_contains=${searchText}&`;
}
@@ -44,7 +44,7 @@ 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);
@@ -54,6 +54,11 @@ class LmsApiService {
const cohortsUrl = `${LmsApiService.baseUrl}/courses/${courseId}/cohorts/`;
return apiClient.get(cohortsUrl);
}
static fetchAssignmentTypes(courseId) {
const assignmentTypesUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`;
return apiClient.get(assignmentTypesUrl);
}
}
export default LmsApiService;

View File

@@ -4,3 +4,7 @@ import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
// These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing
process.env.LMS_BASE_URL = 'http://localhost:18000';