Compare commits

...

11 Commits

Author SHA1 Message Date
Simon Chen
8a01a60d63 Merge pull request #61 from edx/schen/default_select
fix(functionality): Make sure we default select radio button
2018-12-20 10:06:27 -05:00
Simon Chen
66cdcc7f2a fix(functionality): Make sure we default select radio button 2018-12-20 09:57:14 -05:00
Robert Raposa
0c73d66666 Merge pull request #59 from edx/robrap/footer-logo
Update footer logo.
2018-12-19 16:13:55 -05:00
albemarle
28e3e6d0e6 Merge pull request #60 from edx/home-link
add aria-label for edX Home
2018-12-19 15:29:57 -05:00
albemarle
6473bafa3d edx -> edX 2018-12-19 15:16:37 -05:00
Jansen Kantor
167901e665 Merge pull request #55 from edx/jkantor/role-error
render error message if user is not allowed to view gradebook
2018-12-19 15:15:34 -05:00
albemarle
50a0d6e579 add aria-label for edX Home 2018-12-19 15:09:58 -05:00
Robert Raposa
a284c286f5 Update footer logo.
ARCH-322
2018-12-19 14:04:31 -05:00
jansenk
dd967e703c render error message if user is not allowed to view gradebook 2018-12-19 14:00:27 -05:00
Richard I Reilly
725dc071e3 Merge pull request #58 from edx/rir/base-html-fixes
Add necessary meta tag to the base html file, language attribute for …
2018-12-19 13:37:45 -05:00
Rick Reilly
3da7730f23 Add necessary meta tag to the base html file, language attribute for a11y, and title 2018-12-19 11:20:33 -05:00
13 changed files with 260 additions and 14 deletions

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,7 +1,9 @@
<!doctype html>
<html>
<html lang="en-us">
<head>
<title>Gradebook | edX</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="root"></div>

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { Hyperlink, Icon } from '@edx/paragon';
import EdXLogo from '../../../assets/edx-sm.png';
import EdXFooterLogo from '../../../assets/edx-footer.png';
export default function Footer() {
function renderLogo() {
return (
<img src={EdXLogo} alt="edX logo" height="30" width="60" />
<img src={EdXFooterLogo} alt="edX logo" />
);
}
@@ -18,10 +18,10 @@ export default function Footer() {
>
<div className="max-width-1180 d-grid">
<div className="area-1">
<Hyperlink destination="https://www.edx.org/" content={renderLogo()} />
<Hyperlink destination="https://www.edx.org/" content={renderLogo()} aria-label="edX Home" />
</div>
<div className="area-2">
<h2>edx</h2>
<h2>edX</h2>
<ul className="list-unstyled p-0 m-0">
<li><a href="https://www.edx.org/about-us">About</a></li>
<li><a href="https://www.edx.org/enterprise">edX for Business</a></li>

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>
@@ -271,6 +269,7 @@ export default class Gradebook extends React.Component {
type="radio"
name="score-view"
value="percent"
defaultChecked
onClick={() => this.props.toggleFormat('percent')}
/>
<label className="mr-2" htmlFor="score-view-percent">Percent</label>

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>