Compare commits
1 Commits
rir/header
...
douglashal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d647f381 |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
110
package-lock.json
generated
110
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.1"
|
||||
},
|
||||
"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",
|
||||
@@ -2965,9 +2980,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-auth": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-1.2.1.tgz",
|
||||
"integrity": "sha512-NEc+rAJq5HJ9UACPirezpwTp5yhX9G2AQDPfk1hW4ceEF7BFRMIfzb+nJqmTJkEX/37yRVYUfQFLW+z2j1ZRcw==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-1.3.0.tgz",
|
||||
"integrity": "sha512-llAoGBvxwZCnGzUMFSHHMxGIrrYLuWlEmrSQs6rqdqT5RXHUkuwNde5iemohqGe/dLGcGjiNzUjr83iGUlN66Q==",
|
||||
"requires": {
|
||||
"axios": "^0.18.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
@@ -7968,6 +7983,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
|
||||
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "7.2.0"
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
|
||||
@@ -13793,6 +13816,16 @@
|
||||
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.isfunction": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
|
||||
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="
|
||||
},
|
||||
"lodash.isobject": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
|
||||
"integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0="
|
||||
},
|
||||
"lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
@@ -13856,6 +13889,11 @@
|
||||
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.tonumber": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.tonumber/-/lodash.tonumber-4.0.3.tgz",
|
||||
"integrity": "sha1-C5azGzVnJ5Prf1pj7nkfG56QJdk="
|
||||
},
|
||||
"lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
@@ -20129,6 +20167,27 @@
|
||||
"integrity": "sha512-FlsPxavEyMuR6TjVbSSywovXSEyOg6ZDj5+Z8nbsRl9EkOzAhEIcS+GLoQDC5fz/t9suhUXWmUrOBrgeUvrMxw==",
|
||||
"dev": true
|
||||
},
|
||||
"react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"react-popper": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.10.4.tgz",
|
||||
"integrity": "sha1-rypBXqIike3VBGeNev2opu4ylao=",
|
||||
"requires": {
|
||||
"popper.js": "1.14.6",
|
||||
"prop-types": "15.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"popper.js": {
|
||||
"version": "1.14.6",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.6.tgz",
|
||||
"integrity": "sha512-AGwHGQBKumlk/MDfrSOf0JHhJCImdDMcGNoqKmKkU+68GFazv3CQ6q9r7Ja1sKDZmYWTckY/uLyEznheTDycnA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -20217,6 +20276,51 @@
|
||||
"prop-types": "^15.6.0"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.1.tgz",
|
||||
"integrity": "sha512-8x/CxUL9SjYFmUdzsBPTgtKeCxt7QArjNSte0wwiLtF/Ix/o1nWNJooNy5o9XbHIKS31pz7J5VF2l41TwlvbHQ==",
|
||||
"requires": {
|
||||
"dom-helpers": "3.4.0",
|
||||
"loose-envify": "1.4.0",
|
||||
"prop-types": "15.6.2",
|
||||
"react-lifecycles-compat": "3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"requires": {
|
||||
"js-tokens": "3.0.2"
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
||||
"requires": {
|
||||
"loose-envify": "1.4.0",
|
||||
"object-assign": "4.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"reactstrap": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-6.5.0.tgz",
|
||||
"integrity": "sha512-dWb3fB/wBAiQloteKlf+j9Nl2VLe6BMZgTEt6hpeTt0t9TwtkeU+2v2NBYONZaF4FZATfMiIKozhWpc2HmLW1g==",
|
||||
"requires": {
|
||||
"classnames": "2.2.5",
|
||||
"lodash.isfunction": "3.0.9",
|
||||
"lodash.isobject": "3.0.2",
|
||||
"lodash.tonumber": "4.0.3",
|
||||
"prop-types": "15.6.1",
|
||||
"react-lifecycles-compat": "3.0.4",
|
||||
"react-popper": "0.10.4",
|
||||
"react-transition-group": "2.5.1"
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/edx-bootstrap": "^0.4.3",
|
||||
"@edx/frontend-auth": "^1.2.1",
|
||||
"@edx/frontend-auth": "^1.3.0",
|
||||
"@edx/paragon": "^3.7.2",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"classnames": "^2.2.5",
|
||||
@@ -40,6 +40,7 @@
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"reactstrap": "^6.5.0",
|
||||
"redux": "^3.7.2",
|
||||
"redux-devtools-extension": "^2.13.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<html>
|
||||
<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>
|
||||
|
||||
@@ -5,8 +5,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
@import "~@edx/paragon/src/SearchField/SearchField";
|
||||
@import "./components/Gradebook/gradebook";
|
||||
|
||||
@import "./components/Gradebook/gradebook";
|
||||
@import "./components/Gradebook/footer";
|
||||
@import "./components/Header/header";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Hyperlink, Icon } from '@edx/paragon';
|
||||
|
||||
import EdXFooterLogo from '../../../assets/edx-footer.png';
|
||||
import EdXLogo from '../../../assets/edx-sm.png';
|
||||
|
||||
export default function Footer() {
|
||||
function renderLogo() {
|
||||
return (
|
||||
<img src={EdXFooterLogo} alt="edX logo" />
|
||||
<img src={EdXLogo} alt="edX logo" height="30" width="60" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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()} aria-label="edX Home" />
|
||||
<Hyperlink destination="https://www.edx.org/" content={renderLogo()} />
|
||||
</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>
|
||||
|
||||
@@ -28,7 +28,14 @@ export default class Gradebook extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
const urlQuery = queryString.parse(this.props.location.search);
|
||||
this.props.getRoles(this.props.match.params.courseId, urlQuery);
|
||||
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);
|
||||
}
|
||||
|
||||
setNewModalState = (userEntry, subsection) => {
|
||||
@@ -252,11 +259,6 @@ 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>
|
||||
@@ -269,7 +271,6 @@ 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>
|
||||
@@ -299,25 +300,29 @@ export default class Gradebook extends React.Component {
|
||||
/>
|
||||
</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>
|
||||
{(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>
|
||||
<div>
|
||||
<div style={{ marginLeft: '10px', marginBottom: '10px' }}>
|
||||
@@ -365,7 +370,6 @@ 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>
|
||||
@@ -373,12 +377,11 @@ 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="Save Grade"
|
||||
label="Edit Grade"
|
||||
buttonType="primary"
|
||||
onClick={this.handleAdjustedGradeClick}
|
||||
/>,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,40 +1,94 @@
|
||||
import React from 'react';
|
||||
import { Hyperlink, Icon } from '@edx/paragon';
|
||||
import { configuration } from '../../config';
|
||||
import {
|
||||
Collapse,
|
||||
Navbar,
|
||||
NavbarToggler,
|
||||
NavbarBrand,
|
||||
Nav,
|
||||
NavItem,
|
||||
NavLink,
|
||||
UncontrolledDropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
} from 'reactstrap';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import apiClient from '../../data/apiClient';
|
||||
import { configuration } from '../../config';
|
||||
import EdxLogo from '../../../assets/edx-sm.png';
|
||||
|
||||
export default class Header extends React.Component {
|
||||
class Header extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.state = {
|
||||
mobileNavOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
renderLogo() {
|
||||
return (
|
||||
<img src={EdxLogo} alt="edX logo" height="30" width="60" />
|
||||
);
|
||||
toggle() {
|
||||
this.setState({
|
||||
mobileNavOpen: !this.state.mobileNavOpen,
|
||||
});
|
||||
}
|
||||
|
||||
getUserProfileImageIcon() {
|
||||
const screenReaderText = `Profile image for ${this.props.username}`;
|
||||
|
||||
if (this.props.userProfileImageUrl) {
|
||||
return <img src={this.props.userProfileImageUrl} alt={screenReaderText} />;
|
||||
}
|
||||
return <Icon className={['fa', 'fa-user', 'px-3']} screenReaderText={screenReaderText} />;
|
||||
}
|
||||
|
||||
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>
|
||||
{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>
|
||||
<Navbar light expand="md" className="border-bottom">
|
||||
<NavbarBrand href={configuration.LMS_BASE_URL}>
|
||||
<img src={EdxLogo} alt="edX logo" height="30" width="60" />
|
||||
</NavbarBrand>
|
||||
<NavbarToggler onClick={this.toggle} />
|
||||
<Collapse isOpen={this.state.mobileNavOpen} navbar>
|
||||
<Nav className="ml-auto" navbar>
|
||||
<UncontrolledDropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>
|
||||
{this.getUserProfileImageIcon()}
|
||||
{this.props.username}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu right>
|
||||
<DropdownItem href={`${configuration.LMS_BASE_URL}/dashboard`}>
|
||||
Dashboard
|
||||
</DropdownItem>
|
||||
<DropdownItem href={`${configuration.LMS_BASE_URL}/u/${this.props.username}`}>
|
||||
Profile
|
||||
</DropdownItem>
|
||||
<DropdownItem href={`${configuration.LMS_BASE_URL}/account/settings`}>
|
||||
Account
|
||||
</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
<DropdownItem onClick={() => apiClient.logout()}>
|
||||
Logout
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Header.defaultProps = {
|
||||
username: null,
|
||||
userProfileImageUrl: null,
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
username: PropTypes.string,
|
||||
userProfileImageUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
@@ -13,7 +13,6 @@ 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 => (
|
||||
{
|
||||
@@ -29,21 +28,10 @@ const mapStateToProps = state => (
|
||||
nextPage: state.grades.nextPage,
|
||||
assignmnetTypes: state.assignmentTypes.results,
|
||||
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
|
||||
showSpinner: shouldShowSpinner(state),
|
||||
canUserViewGradebook: state.roles.canUserViewGradebook
|
||||
showSpinner: state.grades.showSpinner,
|
||||
}
|
||||
);
|
||||
|
||||
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) => {
|
||||
@@ -76,9 +64,6 @@ const mapDispatchToProps = dispatch => (
|
||||
updateBanner: (showSuccess) => {
|
||||
dispatch(updateBanner(showSuccess));
|
||||
},
|
||||
getRoles: (matchParams, urlQuery) => {
|
||||
dispatch(getRoles(matchParams, urlQuery));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
11
src/containers/Header/index.jsx
Normal file
11
src/containers/Header/index.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchUserProfile } from '@edx/frontend-auth';
|
||||
|
||||
import Header from '../../components/Header';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
username: state.userProfile.username,
|
||||
userProfileImageUrl: state.userProfile.userProfileImageUrl,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Header);
|
||||
@@ -1,38 +0,0 @@
|
||||
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 allowedRoles = ['staff', 'instructor', 'support'];
|
||||
|
||||
const gotRoles = canUserViewGradebook => ({ type: GOT_ROLES, canUserViewGradebook });
|
||||
const errorFetchingRoles = () => ({ type: ERROR_FETCHING_ROLES });
|
||||
|
||||
const getRoles = (courseId, urlQuery) => (
|
||||
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,
|
||||
};
|
||||
@@ -1,144 +0,0 @@
|
||||
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 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,
|
||||
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(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);
|
||||
});
|
||||
});
|
||||
|
||||
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(makeRoleListObj([course1DummyRole, course2StaffRole], false)));
|
||||
|
||||
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(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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { configuration } from '../config';
|
||||
|
||||
const apiClient = getAuthenticatedAPIClient({
|
||||
appBaseUrl: configuration.BASE_URL,
|
||||
authBaseUrl: configuration.LMS_BASE_URL,
|
||||
loginUrl: configuration.LOGIN_URL,
|
||||
logoutUrl: configuration.LOGOUT_URL,
|
||||
csrfTokenApiPath: process.env.CSRF_TOKEN_API_PATH,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
const GOT_ROLES = 'GOT_ROLES';
|
||||
const ERROR_FETCHING_ROLES = 'ERROR_FETCHING_ROLES'
|
||||
|
||||
export {
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES,
|
||||
};
|
||||
@@ -1,17 +1,25 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { userProfile } from '@edx/frontend-auth';
|
||||
|
||||
import cohorts from './cohorts';
|
||||
import grades from './grades';
|
||||
import tracks from './tracks';
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
import roles from './roles';
|
||||
|
||||
const identityReducer = (state) => {
|
||||
const newState = { ...state };
|
||||
return newState;
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
// The authentication state is added as initialState when
|
||||
// creating the store in data/store.js.
|
||||
authentication: identityReducer,
|
||||
userProfile,
|
||||
grades,
|
||||
cohorts,
|
||||
tracks,
|
||||
assignmentTypes,
|
||||
roles,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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;
|
||||
@@ -1,47 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -59,11 +59,6 @@ class LmsApiService {
|
||||
const assignmentTypesUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`;
|
||||
return apiClient.get(assignmentTypesUrl);
|
||||
}
|
||||
|
||||
static fetchUserRoles(courseId) {
|
||||
const rolesUrl = `${LmsApiService.baseUrl}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(courseId)}`;
|
||||
return apiClient.get(rolesUrl);
|
||||
}
|
||||
}
|
||||
|
||||
export default LmsApiService;
|
||||
|
||||
@@ -3,12 +3,15 @@ import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import apiClient from './apiClient';
|
||||
import reducers from './reducers';
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
const initialState = apiClient.getAuthenticationState();
|
||||
|
||||
const store = createStore(
|
||||
reducers,
|
||||
initialState,
|
||||
composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware)),
|
||||
);
|
||||
|
||||
|
||||
@@ -3,31 +3,37 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { fetchUserProfile } from '@edx/frontend-auth';
|
||||
|
||||
import apiClient from './data/apiClient';
|
||||
import Footer from './components/Gradebook/footer';
|
||||
import GradebookPage from './containers/GradebookPage';
|
||||
import Header from './components/Header';
|
||||
import Header from './containers/Header';
|
||||
import store from './data/store';
|
||||
import './App.scss';
|
||||
|
||||
var courseId = window.location.pathname.substring(1);
|
||||
class App extends React.Component {
|
||||
componentDidMount() {
|
||||
const username = store.getState().authentication.username;
|
||||
store.dispatch(fetchUserProfile(apiClient, username));
|
||||
}
|
||||
|
||||
const App = () => (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path="/:courseId" component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
render() {
|
||||
return <Provider store={store}>
|
||||
<Router>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path="/:courseId" component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>;
|
||||
}
|
||||
}
|
||||
|
||||
if (apiClient.ensurePublicOrAuthencationAndCookies(window.location.pathname)) {
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
|
||||
Reference in New Issue
Block a user