Merge pull request #55 from edx/jkantor/role-error

render error message if user is not allowed to view gradebook
This commit is contained in:
Jansen Kantor
2018-12-19 15:15:34 -05:00
committed by GitHub
10 changed files with 252 additions and 9 deletions

View File

@@ -28,14 +28,7 @@ export default class Gradebook extends React.Component {
componentDidMount() {
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);
this.props.getAssignmentTypes(this.props.match.params.courseId);
this.props.getRoles(this.props.match.params.courseId, urlQuery);
}
setNewModalState = (userEntry, subsection) => {
@@ -259,6 +252,11 @@ 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 &&
<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.
</div>
}
<hr />
<div className="d-flex justify-content-between" >
<div>

View File

@@ -13,6 +13,7 @@ import {
import { fetchCohorts } from '../../data/actions/cohorts';
import { fetchTracks } from '../../data/actions/tracks';
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
import { getRoles } from '../../data/actions/roles';
const mapStateToProps = state => (
{
@@ -28,10 +29,21 @@ const mapStateToProps = state => (
nextPage: state.grades.nextPage,
assignmnetTypes: state.assignmentTypes.results,
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
showSpinner: state.grades.showSpinner,
showSpinner: shouldShowSpinner(state),
canUserViewGradebook: state.roles.canUserViewGradebook
}
);
function shouldShowSpinner (state) {
if (state.roles.canUserViewGradebook === true){
return state.grades.showSpinner;
} else if (state.roles.canUserViewGradebook === false){
return false;
} else { // canUserViewGradebook === null
return true;
}
}
const mapDispatchToProps = dispatch => (
{
getUserGrades: (courseId, cohort, track) => {
@@ -64,6 +76,9 @@ const mapDispatchToProps = dispatch => (
updateBanner: (showSuccess) => {
dispatch(updateBanner(showSuccess));
},
getRoles: (matchParams, urlQuery) => {
dispatch(getRoles(matchParams, urlQuery));
},
}
);

38
src/data/actions/roles.js Normal file
View File

@@ -0,0 +1,38 @@
import {
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 gotRoles = canUserViewGradebook => ({ type: GOT_ROLES, canUserViewGradebook });
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())
});
});
export {
getRoles,
errorFetchingRoles,
};

View File

@@ -0,0 +1,103 @@
import configureMockStore from 'redux-mock-store';
import MockAdapter from 'axios-mock-adapter';
import thunk from 'redux-thunk';
import apiClient from '../apiClient';
import { configuration } from '../../config';
import { getRoles } from './roles';
import {
GOT_ROLES,
ERROR_FETCHING_ROLES,
} from '../constants/actionTypes/roles';
import { STARTED_FETCHING_GRADES } from '../constants/actionTypes/grades';
import { STARTED_FETCHING_TRACKS } from '../constants/actionTypes/tracks';
import { STARTED_FETCHING_COHORTS } from '../constants/actionTypes/cohorts';
import { STARTED_FETCHING_ASSIGNMENT_TYPES } from '../constants/actionTypes/assignmentTypes';
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';
function makeRoleObj(courseId, role) {
return {
course_id: courseId,
role: role,
}
};
const course1StaffRole = makeRoleObj(course1Id, "staff");
const course1DummyRole = makeRoleObj(course1Id, "dummy");
const course2StaffRole = makeRoleObj(course2Id, "staff");
const course2DummyRole = makeRoleObj(course2Id, "dummy");
const urlParams = { cohort: null, track: null };
describe('actions', () => {
afterEach(() => {
axiosMock.reset();
});
describe('getRoles', () => {
it('dispatches got_roles action and subsequent actions after fetching role that allows gradebook', () => {
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([course1StaffRole, course2DummyRole]));
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it('dispatches got_roles action and no other actions after fetching role that disallows gradebook', () => {
const expectedActions = [
{ type: GOT_ROLES, canUserViewGradebook: false },
];
const store = mockStore();
axiosMock.onGet(rolesUrl)
.replyOnce(200, JSON.stringify([course1DummyRole, course2StaffRole]));
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it('dispatches got_roles action and no other actions after fetching empty roles', () => {
const expectedActions = [
{ type: GOT_ROLES, canUserViewGradebook: false },
];
const store = mockStore();
axiosMock.onGet(rolesUrl)
.replyOnce(200, JSON.stringify([]));
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it('dispatches error action after getting an error when trying to get roles', () => {
const expectedActions = [
{ type: ERROR_FETCHING_ROLES },
];
const store = mockStore();
axiosMock.onGet(rolesUrl).replyOnce(400);
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});

View File

@@ -0,0 +1,7 @@
const GOT_ROLES = 'GOT_ROLES';
const ERROR_FETCHING_ROLES = 'ERROR_FETCHING_ROLES'
export {
GOT_ROLES,
ERROR_FETCHING_ROLES,
};

View File

@@ -4,12 +4,14 @@ import cohorts from './cohorts';
import grades from './grades';
import tracks from './tracks';
import assignmentTypes from './assignmentTypes';
import roles from './roles';
const rootReducer = combineReducers({
grades,
cohorts,
tracks,
assignmentTypes,
roles,
});
export default rootReducer;

View File

@@ -0,0 +1,26 @@
import {
GOT_ROLES,
ERROR_FETCHING_ROLES,
} from '../constants/actionTypes/roles';
const initialState = {
canUserViewGradebook: null,
};
const roles = (state = initialState, action) => {
switch (action.type) {
case GOT_ROLES:
return {
...state,
canUserViewGradebook: action.canUserViewGradebook,
};
case ERROR_FETCHING_ROLES:
return {
...state,
canUserViewGradebook: false,
};
default:
return state;
}};
export default roles;

View File

@@ -0,0 +1,47 @@
import roles from './roles';
import {
ERROR_FETCHING_ROLES,
GOT_ROLES,
} from '../constants/actionTypes/roles';
const initialState = {
canUserViewGradebook: null,
};
describe('tracks reducer', () => {
it('has initial state', () => {
expect(roles(undefined, {})).toEqual(initialState);
});
it('updates canUserViewGradebook to true', () => {
const expected = {
...initialState,
canUserViewGradebook: true
};
expect(roles(undefined, {
type: GOT_ROLES,
canUserViewGradebook: true,
})).toEqual(expected);
});
it('updates canUserViewGradebook to false', () => {
const expected = {
...initialState,
canUserViewGradebook: false
};
expect(roles(undefined, {
type: GOT_ROLES,
canUserViewGradebook: false,
})).toEqual(expected);
});
it('updates fetch roles failure state', () => {
const expected = {
...initialState,
canUserViewGradebook: false,
};
expect(roles(undefined, {
type: ERROR_FETCHING_ROLES,
})).toEqual(expected);
});
});

View File

@@ -59,6 +59,11 @@ class LmsApiService {
const assignmentTypesUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`;
return apiClient.get(assignmentTypesUrl);
}
static fetchUserRoles(){
var rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/`;
return apiClient.get(rolesUrl)
}
}
export default LmsApiService;

View File

@@ -11,6 +11,8 @@ import Header from './components/Header';
import store from './data/store';
import './App.scss';
var courseId = window.location.pathname.substring(1);
const App = () => (
<Provider store={store}>
<Router>