Compare commits

...

10 Commits

Author SHA1 Message Date
Rick Reilly
c5b025aef5 wip 2019-01-02 14:20:39 -05:00
Rick Reilly
0b507c558a wip 2019-01-02 14:20:21 -05:00
Jansen Kantor
86092f22b3 Merge pull request #65 from edx/jkantor/disable-student-groups
disable rather than hide empty groups and cohorts
2019-01-02 09:22:19 -05:00
Zachary Hancock
c8cb07228f Merge pull request #63 from edx/zhancock/edit-modal
Gradebook edit modal updates
2018-12-27 10:04:57 -05:00
Jansen Kantor
a1946e7bc4 Merge pull request #64 from edx/jkantor/roles-filter
request filtered roles
2018-12-21 15:08:09 -05:00
jansenk
01d80e0fff disable rather than hide empty groups and cohorts
EDUCATOR-3824
2018-12-21 14:23:19 -05:00
jansenk
e6da087e83 request filtered roles 2018-12-21 12:53:07 -05:00
Jansen Kantor
ac5eaed5cb Merge pull request #62 from edx/jkantor/staff
fix(auth) allow global staff to view gradebook
2018-12-21 11:46:02 -05:00
jansenk
88997ca242 fix(auth) allow global staff to view gradebook 2018-12-21 11:34:28 -05:00
Zach Hancock
d5daf9086f gradebook edit modal message 2018-12-21 10:09:35 -05:00
8 changed files with 131 additions and 56 deletions

View File

@@ -5,6 +5,8 @@ $fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome"; @import "~font-awesome/scss/font-awesome";
@import "~@edx/paragon/src/SearchField/SearchField"; @import "~@edx/paragon/src/SearchField/SearchField";
@import "./components/Gradebook/gradebook";
@import "./components/Gradebook/gradebook"; @import "./components/Gradebook/gradebook";
@import "./components/Gradebook/footer"; @import "./components/Gradebook/footer";
@import "./components/Header/header";

View File

@@ -299,29 +299,25 @@ export default class Gradebook extends React.Component {
/> />
</div> </div>
} }
{(this.props.tracks.length > 0 || this.props.cohorts.length > 0) && <div className="student-filters">
<div className="student-filters"> <span className="label">
<span className="label"> Student Groups:
Student Groups: </span>
</span> <InputSelect
{this.props.tracks.length > 0 && name="Tracks"
<InputSelect disabled={this.props.tracks.length === 0}
name="Tracks" value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
value={this.mapSelectedTrackEntry(this.props.selectedTrack)} options={this.mapTracksEntries(this.props.tracks)}
options={this.mapTracksEntries(this.props.tracks)} onChange={this.updateTracks}
onChange={this.updateTracks} />
/> <InputSelect
} name="Cohorts"
{this.props.cohorts.length > 0 && disabled={this.props.cohorts.length === 0}
<InputSelect value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
name="Cohorts" options={this.mapCohortsEntries(this.props.cohorts)}
value={this.mapSelectedCohortEntry(this.props.selectedCohort)} onChange={this.updateCohorts}
options={this.mapCohortsEntries(this.props.cohorts)} />
onChange={this.updateCohorts} </div>
/>
}
</div>
}
</div> </div>
<div> <div>
<div style={{ marginLeft: '10px', marginBottom: '10px' }}> <div style={{ marginLeft: '10px', marginBottom: '10px' }}>
@@ -369,6 +365,7 @@ export default class Gradebook extends React.Component {
<Modal <Modal
open={this.state.modalOpen} open={this.state.modalOpen}
title="Edit Grades" title="Edit Grades"
closeText="Cancel"
body={( body={(
<div> <div>
<h3>{this.state.modalModel[0].assignmentName}</h3> <h3>{this.state.modalModel[0].assignmentName}</h3>
@@ -376,11 +373,12 @@ export default class Gradebook extends React.Component {
columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]} columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
data={this.state.modalModel} data={this.state.modalModel}
/> />
<div>Note: Once you save, your changes will be visible to students.</div>
</div> </div>
)} )}
buttons={[ buttons={[
<Button <Button
label="Edit Grade" label="Save Grade"
buttonType="primary" buttonType="primary"
onClick={this.handleAdjustedGradeClick} onClick={this.handleAdjustedGradeClick}
/>, />,

View File

@@ -0,0 +1,24 @@
.color-gray-dark {
color: #767676;
}
.weight-bold {
font-weight: 600;
}
.size-16 {
font-size: 16px;
}
.border-bottom-blue {
border-bottom: 1px solid #0075b4;
}
.border-bottom-gray {
border-bottom: 1px solid #e7e7e7;
}
.nav-link::after {
content: '\00BB';
padding-left: 4px;
}

View File

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Hyperlink } from '@edx/paragon'; import { Hyperlink, Icon } from '@edx/paragon';
import { configuration } from '../../config';
import EdxLogo from '../../../assets/edx-sm.png'; import EdxLogo from '../../../assets/edx-sm.png';
@@ -24,6 +25,15 @@ export default class Header extends React.Component {
<Hyperlink content={this.renderLogo()} destination="https://www.edx.org" /> <Hyperlink content={this.renderLogo()} destination="https://www.edx.org" />
<div /> <div />
</header> </header>
{this.state.mobileNavOpen &&
<nav className="d-flex flex-column weight-bold size-16">
<a href="https://www.google.com" className="nav-link border-bottom-gray">Rick</a>
<a href="https://www.google.com" className="nav-link border-bottom-gray">Alex</a>
<a href="https://www.google.com" className="nav-link border-bottom-gray">Jasen</a>
<a href="https://www.google.com" className="nav-link border-bottom-gray">Doug</a>
<a href="https://www.google.com" className="nav-link border-bottom-gray">Simon</a>
</nav>
}
</div> </div>
); );
} }

View File

@@ -1,38 +1,38 @@
import { import {
GOT_ROLES, GOT_ROLES,
ERROR_FETCHING_ROLES ERROR_FETCHING_ROLES,
} from '../constants/actionTypes/roles'; } from '../constants/actionTypes/roles';
import { fetchGrades } from './grades'; import { fetchGrades } from './grades';
import { fetchTracks } from './tracks'; import { fetchTracks } from './tracks';
import { fetchCohorts } from './cohorts'; import { fetchCohorts } from './cohorts';
import { fetchAssignmentTypes } from './assignmentTypes'; import { fetchAssignmentTypes } from './assignmentTypes';
import LmsApiService from '../services/LmsApiService'; import LmsApiService from '../services/LmsApiService';
const allowed_roles = ['staff', 'instructor', 'support']; const allowedRoles = ['staff', 'instructor', 'support'];
const gotRoles = canUserViewGradebook => ({ type: GOT_ROLES, canUserViewGradebook }); const gotRoles = canUserViewGradebook => ({ type: GOT_ROLES, canUserViewGradebook });
const errorFetchingRoles = () => ({type: ERROR_FETCHING_ROLES }); const errorFetchingRoles = () => ({ type: ERROR_FETCHING_ROLES });
const getRoles = (courseId, urlQuery) => ( const getRoles = (courseId, urlQuery) => (
(dispatch) => { dispatch => LmsApiService.fetchUserRoles(courseId)
return LmsApiService.fetchUserRoles(courseId) .then(response => response.data)
.then(response => response.data) .then((response) => {
.then(roles => { const canUserViewGradebook = response.is_staff
var canUserViewGradebook = roles.some(role => (role.course_id === courseId) && allowed_roles.includes(role.role)); || (response.roles.some(role => (role.course_id === courseId)
dispatch(gotRoles(canUserViewGradebook)); && allowedRoles.includes(role.role)));
if(canUserViewGradebook){ dispatch(gotRoles(canUserViewGradebook));
dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track)); if (canUserViewGradebook) {
dispatch(fetchTracks(courseId)); dispatch(fetchGrades(courseId, urlQuery.cohort, urlQuery.track));
dispatch(fetchCohorts(courseId)); dispatch(fetchTracks(courseId));
dispatch(fetchAssignmentTypes(courseId)); dispatch(fetchCohorts(courseId));
} dispatch(fetchAssignmentTypes(courseId));
}) }
.catch(() => { })
dispatch(errorFetchingRoles()) .catch(() => {
}); dispatch(errorFetchingRoles());
}); }));
export { export {
getRoles, getRoles,
errorFetchingRoles, errorFetchingRoles,
}; };

View File

@@ -18,11 +18,16 @@ import { STARTED_FETCHING_ASSIGNMENT_TYPES } from '../constants/actionTypes/assi
const mockStore = configureMockStore([thunk]); const mockStore = configureMockStore([thunk]);
const axiosMock = new MockAdapter(apiClient); const axiosMock = new MockAdapter(apiClient);
const rolesUrl = `${configuration.LMS_BASE_URL}/api/enrollment/v1/roles/`;
const course1Id = 'course-v1:edX+DemoX+Demo_Course'; const course1Id = 'course-v1:edX+DemoX+Demo_Course';
const course2Id = 'course-v1:edX+DemoX+Demo_Course_2'; 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) { function makeRoleObj(courseId, role) {
return { return {
course_id: courseId, course_id: courseId,
@@ -52,7 +57,25 @@ describe('actions', () => {
]; ];
const store = mockStore(); const store = mockStore();
axiosMock.onGet(rolesUrl) 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(() => { return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
@@ -66,7 +89,7 @@ describe('actions', () => {
const store = mockStore(); const store = mockStore();
axiosMock.onGet(rolesUrl) axiosMock.onGet(rolesUrl)
.replyOnce(200, JSON.stringify([course1DummyRole, course2StaffRole])); .replyOnce(200, JSON.stringify(makeRoleListObj([course1DummyRole, course2StaffRole], false)));
return store.dispatch(getRoles(course1Id, urlParams)).then(() => { return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
@@ -80,7 +103,25 @@ describe('actions', () => {
const store = mockStore(); const store = mockStore();
axiosMock.onGet(rolesUrl) 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(() => { return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);

View File

@@ -60,9 +60,9 @@ class LmsApiService {
return apiClient.get(assignmentTypesUrl); return apiClient.get(assignmentTypesUrl);
} }
static fetchUserRoles(){ static fetchUserRoles(courseId) {
var rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/`; const rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(courseId)}`;
return apiClient.get(rolesUrl) return apiClient.get(rolesUrl);
} }
} }