Compare commits
11 Commits
v1.4.5
...
rir/lms-he
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eedc58e86 | ||
|
|
c4a4967659 | ||
|
|
75c4ad1f59 | ||
|
|
86092f22b3 | ||
|
|
c8cb07228f | ||
|
|
a1946e7bc4 | ||
|
|
01d80e0fff | ||
|
|
e6da087e83 | ||
|
|
ac5eaed5cb | ||
|
|
88997ca242 | ||
|
|
d5daf9086f |
41
package-lock.json
generated
41
package-lock.json
generated
@@ -2875,6 +2875,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
|
||||
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.12.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"regenerator-runtime": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
|
||||
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.0.0-beta.40",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.40.tgz",
|
||||
@@ -3295,7 +3310,7 @@
|
||||
},
|
||||
"globby": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "http://registry.npmjs.org/globby/-/globby-8.0.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-8.0.1.tgz",
|
||||
"integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -13559,6 +13574,14 @@
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
|
||||
"dev": true
|
||||
},
|
||||
"json2mq": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
|
||||
"integrity": "sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=",
|
||||
"requires": {
|
||||
"string-convert": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"json3": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
|
||||
@@ -20129,6 +20152,17 @@
|
||||
"integrity": "sha512-FlsPxavEyMuR6TjVbSSywovXSEyOg6ZDj5+Z8nbsRl9EkOzAhEIcS+GLoQDC5fz/t9suhUXWmUrOBrgeUvrMxw==",
|
||||
"dev": true
|
||||
},
|
||||
"react-media": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/react-media/-/react-media-1.9.2.tgz",
|
||||
"integrity": "sha512-JUYECMcJIm0V61LSVKd1e+II4ZTYO0GuR7xtlvKETlmThZ416BqZjZdJ1uGqgmMAGFeJ3TG4TX/3Kg4qbR3EJw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.2.0",
|
||||
"invariant": "^2.2.2",
|
||||
"json2mq": "^0.2.0",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"react-proptype-conditional-require": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz",
|
||||
@@ -22379,6 +22413,11 @@
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
||||
},
|
||||
"string-convert": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
|
||||
"integrity": "sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c="
|
||||
},
|
||||
"string-length": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"query-string": "^5.1.1",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-media": "^1.9.2",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
|
||||
@@ -8,3 +8,5 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
|
||||
@import "./components/Gradebook/gradebook";
|
||||
@import "./components/Gradebook/footer";
|
||||
|
||||
@import "./components/Header/header";
|
||||
|
||||
@@ -299,29 +299,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' }}>
|
||||
@@ -369,6 +365,7 @@ export default class Gradebook extends React.Component {
|
||||
<Modal
|
||||
open={this.state.modalOpen}
|
||||
title="Edit Grades"
|
||||
closeText="Cancel"
|
||||
body={(
|
||||
<div>
|
||||
<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' }]}
|
||||
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}
|
||||
/>,
|
||||
|
||||
25
src/components/Header/header.scss
Normal file
25
src/components/Header/header.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
.border-bottom-blue {
|
||||
border-bottom: 1px solid #0075b4;
|
||||
}
|
||||
|
||||
.border-bottom-gray {
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-weight: 600;
|
||||
|
||||
&:after {
|
||||
content: "\00BB";
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.color-gray {
|
||||
color: #767676;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
position: absolute;
|
||||
left: calc(50% - 30px);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { Hyperlink, Icon } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import Media from 'react-media';
|
||||
|
||||
import EdxLogo from '../../../assets/edx-sm.png';
|
||||
|
||||
@@ -20,10 +22,31 @@ export default class Header extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<header className="d-flex justify-content-center align-items-center p-3 border-bottom-blue">
|
||||
<Hyperlink content={this.renderLogo()} destination="https://www.edx.org" />
|
||||
<div />
|
||||
</header>
|
||||
<Media query="(max-width: 901px)">
|
||||
{matches =>
|
||||
(matches ? (
|
||||
<header className="border-bottom-blue">
|
||||
<div className="p-3 border-bottom-blue">
|
||||
<button className="border-0" onClick={() => this.setState({ mobileNavOpen: !this.state.mobileNavOpen })}>
|
||||
<Icon className={classNames('fa', 'fa-2x', 'color-gray', { 'fa-bars': !this.state.mobileNavOpen }, { 'fa-times': this.state.mobileNavOpen })} />
|
||||
</button>
|
||||
<Hyperlink className="header-logo" content={this.renderLogo()} destination="https://www.edx.org" />
|
||||
</div>
|
||||
{this.state.mobileNavOpen &&
|
||||
<div>
|
||||
<a className="nav-link border-bottom-gray" href="https://support.edx.org">Help</a>
|
||||
<a className="nav-link border-bottom-gray" href="https://www.google.com">Dashboard</a>
|
||||
<a className="nav-link border-bottom-gray" href="https://www.google.com">Profile</a>
|
||||
<a className="nav-link border-bottom-gray" href="https://www.google.com">Account</a>
|
||||
<a className="nav-link border-bottom-gray" href="https://www.google.com">Sign Out</a>
|
||||
</div>}
|
||||
</header>
|
||||
) : (
|
||||
<header>
|
||||
<h1>hello world</h1>
|
||||
</header>))
|
||||
}
|
||||
</Media>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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