Compare commits
14 Commits
v1.4.5
...
jkantor/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
286e194414 | ||
|
|
3deb592d89 | ||
|
|
67493d1e9e | ||
|
|
e5bca7e526 | ||
|
|
52c5357ce7 | ||
|
|
d469cc2de7 | ||
|
|
86092f22b3 | ||
|
|
c8cb07228f | ||
|
|
a1946e7bc4 | ||
|
|
01d80e0fff | ||
|
|
e6da087e83 | ||
|
|
ac5eaed5cb | ||
|
|
88997ca242 | ||
|
|
d5daf9086f |
@@ -10,6 +10,7 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import queryString from 'query-string';
|
||||
import { configuration } from '../../config';
|
||||
import PageButtons from '../PageButtons';
|
||||
|
||||
const DECIMAL_PRECISION = 2;
|
||||
|
||||
@@ -252,9 +253,9 @@ export default class Gradebook extends React.Component {
|
||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||
</div>
|
||||
}
|
||||
{ !this.props.canUserViewGradebook &&
|
||||
{ (this.props.canUserViewGradebook === false) &&
|
||||
<div className="alert alert-warning" role="alert" >
|
||||
You are not authorized to view the gradebook for this course. If you have a global role, please enroll in this course and try again.
|
||||
You are not authorized to view the gradebook for this course.
|
||||
</div>
|
||||
}
|
||||
<hr />
|
||||
@@ -299,29 +300,25 @@ export default class Gradebook extends React.Component {
|
||||
/>
|
||||
</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 className="student-filters">
|
||||
<span className="label">
|
||||
Student Groups:
|
||||
</span>
|
||||
<InputSelect
|
||||
name="Tracks"
|
||||
disabled={this.props.tracks.length === 0}
|
||||
value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
|
||||
options={this.mapTracksEntries(this.props.tracks)}
|
||||
onChange={this.updateTracks}
|
||||
/>
|
||||
<InputSelect
|
||||
name="Cohorts"
|
||||
disabled={this.props.cohorts.length === 0}
|
||||
value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
|
||||
options={this.mapCohortsEntries(this.props.cohorts)}
|
||||
onChange={this.updateCohorts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginLeft: '10px', marginBottom: '10px' }}>
|
||||
@@ -333,21 +330,6 @@ 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' }}>
|
||||
<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' }} />
|
||||
<Button
|
||||
label="Next"
|
||||
buttonType="primary"
|
||||
style={{ visibility: (!this.props.nextPage ? 'hidden' : 'visible') }}
|
||||
onClick={() => this.props.getPrevNextGrades(this.props.nextPage, this.props.selectedCohort, this.props.selectedTrack)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
@@ -357,18 +339,18 @@ export default class Gradebook extends React.Component {
|
||||
onClose={() => this.props.updateBanner(false)}
|
||||
open={this.props.showSuccess}
|
||||
/>
|
||||
{PageButtons(this.props)}
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={this.props.headings}
|
||||
data={this.formatter[this.props.format](this.props.grades, this.props.areGradesFrozen)}
|
||||
tableSortable
|
||||
defaultSortDirection="asc"
|
||||
defaultSortedColumn="username"
|
||||
/>
|
||||
</div>
|
||||
{PageButtons(this.props)}
|
||||
<Modal
|
||||
open={this.state.modalOpen}
|
||||
title="Edit Grades"
|
||||
closeText="Cancel"
|
||||
body={(
|
||||
<div>
|
||||
<h3>{this.state.modalModel[0].assignmentName}</h3>
|
||||
@@ -376,11 +358,12 @@ export default class Gradebook extends React.Component {
|
||||
columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
|
||||
data={this.state.modalModel}
|
||||
/>
|
||||
<div>Note: Once you save, your changes will be visible to students.</div>
|
||||
</div>
|
||||
)}
|
||||
buttons={[
|
||||
<Button
|
||||
label="Edit Grade"
|
||||
label="Save Grade"
|
||||
buttonType="primary"
|
||||
onClick={this.handleAdjustedGradeClick}
|
||||
/>,
|
||||
|
||||
36
src/components/PageButtons/PageButtons.test.jsx
Normal file
36
src/components/PageButtons/PageButtons.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import renderer from 'react-test-renderer';
|
||||
import PageButtons from '.';
|
||||
|
||||
const createInput = function createInput(prevPage, nextPage) {
|
||||
return {
|
||||
prevPage,
|
||||
nextPage,
|
||||
selectedTrack: 't',
|
||||
selectedCohort: 'c',
|
||||
getPrevNextGrades() {},
|
||||
};
|
||||
};
|
||||
|
||||
describe('PageButtons', () => {
|
||||
const assertPageButtonsSnapshot = function assertPageButtonsSnapshot(input) {
|
||||
const pb = renderer.create(PageButtons(input));
|
||||
const tree = pb.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
};
|
||||
|
||||
it('prev null, next null', () => {
|
||||
assertPageButtonsSnapshot(createInput(null, null));
|
||||
});
|
||||
|
||||
it('prev null, next not null', () => {
|
||||
assertPageButtonsSnapshot(createInput(null, 'np'));
|
||||
});
|
||||
|
||||
it('prev not null, next null', () => {
|
||||
assertPageButtonsSnapshot(createInput('pp', null));
|
||||
});
|
||||
|
||||
it('prev not null, next not null', () => {
|
||||
assertPageButtonsSnapshot(createInput('pp', 'np'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PageButtons prev not null, next not null 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center"
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": "20px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"margin": "20px",
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"margin": "20px",
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Next Page
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PageButtons prev not null, next null 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center"
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": "20px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"margin": "20px",
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"margin": "20px",
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Next Page
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PageButtons prev null, next not null 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center"
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": "20px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"margin": "20px",
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"margin": "20px",
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Next Page
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PageButtons prev null, next null 1`] = `
|
||||
<div
|
||||
className="d-flex justify-content-center"
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": "20px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"margin": "20px",
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"margin": "20px",
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Next Page
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
30
src/components/PageButtons/index.jsx
Normal file
30
src/components/PageButtons/index.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
|
||||
|
||||
export default function PageButtons({prevPage, nextPage, selectedTrack, selectedCohort, getPrevNextGrades}) {
|
||||
return (
|
||||
<div
|
||||
className="d-flex justify-content-center"
|
||||
style={{ paddingBottom: '20px' }}
|
||||
>
|
||||
<Button
|
||||
label="Previous Page"
|
||||
style={{ margin: '20px' }}
|
||||
buttonType="primary"
|
||||
disabled={!prevPage}
|
||||
onClick={() => getPrevNextGrades(prevPage, selectedCohort, selectedTrack)}
|
||||
/>
|
||||
<Button
|
||||
label="Next Page"
|
||||
style={{ margin: '20px' }}
|
||||
buttonType="primary"
|
||||
disabled={!nextPage}
|
||||
onClick={() => getPrevNextGrades(nextPage, selectedCohort, selectedTrack)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,16 +96,12 @@ describe('actions', () => {
|
||||
track: expectedTrack,
|
||||
headings: [
|
||||
{
|
||||
columnSortable: true,
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
onSort: expect.anything(),
|
||||
},
|
||||
{
|
||||
columnSortable: true,
|
||||
key: 'total',
|
||||
label: 'Total',
|
||||
onSort: expect.anything(),
|
||||
},
|
||||
],
|
||||
prev: responseData.previous,
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import {
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES
|
||||
} from '../constants/actionTypes/roles';
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES,
|
||||
} from '../constants/actionTypes/roles';
|
||||
import { fetchGrades } from './grades';
|
||||
import { fetchTracks } from './tracks';
|
||||
import { fetchCohorts } from './cohorts';
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const allowed_roles = ['staff', 'instructor', 'support'];
|
||||
const allowedRoles = ['staff', 'instructor', 'support'];
|
||||
|
||||
const gotRoles = canUserViewGradebook => ({ type: GOT_ROLES, canUserViewGradebook });
|
||||
const errorFetchingRoles = () => ({type: ERROR_FETCHING_ROLES });
|
||||
const errorFetchingRoles = () => ({ type: ERROR_FETCHING_ROLES });
|
||||
|
||||
const getRoles = (courseId, urlQuery) => (
|
||||
(dispatch) => {
|
||||
return LmsApiService.fetchUserRoles(courseId)
|
||||
.then(response => response.data)
|
||||
.then(roles => {
|
||||
var canUserViewGradebook = roles.some(role => (role.course_id === courseId) && allowed_roles.includes(role.role));
|
||||
dispatch(gotRoles(canUserViewGradebook));
|
||||
if(canUserViewGradebook){
|
||||
dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track));
|
||||
dispatch(fetchTracks(courseId));
|
||||
dispatch(fetchCohorts(courseId));
|
||||
dispatch(fetchAssignmentTypes(courseId));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingRoles())
|
||||
});
|
||||
});
|
||||
dispatch => 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));
|
||||
if (canUserViewGradebook) {
|
||||
dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track));
|
||||
dispatch(fetchTracks(courseId));
|
||||
dispatch(fetchCohorts(courseId));
|
||||
dispatch(fetchAssignmentTypes(courseId));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(errorFetchingRoles());
|
||||
}));
|
||||
|
||||
export {
|
||||
getRoles,
|
||||
errorFetchingRoles,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,11 +18,16 @@ import { STARTED_FETCHING_ASSIGNMENT_TYPES } from '../constants/actionTypes/assi
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const axiosMock = new MockAdapter(apiClient);
|
||||
|
||||
const rolesUrl = `${configuration.LMS_BASE_URL}/api/enrollment/v1/roles/`;
|
||||
|
||||
const course1Id = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const course2Id = 'course-v1:edX+DemoX+Demo_Course_2';
|
||||
const rolesUrl = `${configuration.LMS_BASE_URL}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(course1Id)}`;
|
||||
|
||||
function makeRoleListObj(roles, isGlobalStaff){
|
||||
return {
|
||||
roles: roles,
|
||||
is_staff: isGlobalStaff,
|
||||
}
|
||||
}
|
||||
function makeRoleObj(courseId, role) {
|
||||
return {
|
||||
course_id: courseId,
|
||||
@@ -52,7 +57,25 @@ describe('actions', () => {
|
||||
];
|
||||
const store = mockStore();
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(200, JSON.stringify([course1StaffRole, course2DummyRole]));
|
||||
.replyOnce(200, JSON.stringify(makeRoleListObj([course1StaffRole, course2DummyRole], false)));
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and other actions after fetching irrelevent roles but user is global staff', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: true },
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(200, JSON.stringify(makeRoleListObj([course1DummyRole, course2DummyRole], true)));
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
@@ -66,7 +89,7 @@ describe('actions', () => {
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(200, JSON.stringify([course1DummyRole, course2StaffRole]));
|
||||
.replyOnce(200, JSON.stringify(makeRoleListObj([course1DummyRole, course2StaffRole], false)));
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
@@ -80,7 +103,25 @@ describe('actions', () => {
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(200, JSON.stringify([]));
|
||||
.replyOnce(200, JSON.stringify(makeRoleListObj([], false)));
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and other actions after fetching empty roles but user is global staff', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: true },
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(200, JSON.stringify(makeRoleListObj([], true)));
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
|
||||
@@ -66,8 +66,6 @@ const headingMapper = (filterKey) => {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { dispatch(sortGrades('username', direction)); },
|
||||
}];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
@@ -75,15 +73,11 @@ const headingMapper = (filterKey) => {
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: true,
|
||||
onSort: direction => dispatch(sortGrades(s.label, direction)),
|
||||
}));
|
||||
|
||||
const totals = [{
|
||||
label: 'Total',
|
||||
key: 'total',
|
||||
columnSortable: true,
|
||||
onSort: direction => dispatch(sortGrades('total', direction)),
|
||||
}];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
@@ -95,8 +89,6 @@ const headingMapper = (filterKey) => {
|
||||
const results = [{
|
||||
label: 'Username',
|
||||
key: 'username',
|
||||
columnSortable: true,
|
||||
onSort: (direction) => { dispatch(sortGrades('username', direction)); },
|
||||
}];
|
||||
|
||||
const assignmentHeadings = entry.section_breakdown
|
||||
@@ -104,15 +96,11 @@ const headingMapper = (filterKey) => {
|
||||
.map(s => ({
|
||||
label: s.label,
|
||||
key: s.label,
|
||||
columnSortable: false,
|
||||
onSort: (direction) => { this.sortNumerically(s.label, direction); },
|
||||
}));
|
||||
|
||||
const totals = [{
|
||||
label: 'Total',
|
||||
key: 'total',
|
||||
columnSortable: true,
|
||||
onSort: direction => dispatch(sortGrades('total', direction)),
|
||||
}];
|
||||
|
||||
return results.concat(assignmentHeadings).concat(totals);
|
||||
|
||||
@@ -60,9 +60,9 @@ class LmsApiService {
|
||||
return apiClient.get(assignmentTypesUrl);
|
||||
}
|
||||
|
||||
static fetchUserRoles(){
|
||||
var rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/`;
|
||||
return apiClient.get(rolesUrl)
|
||||
static fetchUserRoles(courseId) {
|
||||
const rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(courseId)}`;
|
||||
return apiClient.get(rolesUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user