Compare commits

..

18 Commits

Author SHA1 Message Date
Douglas Hall
66d647f381 feat(header): add new responsive header 2018-12-19 09:20:52 -05:00
Richard I Reilly
bea36fb387 Merge pull request #57 from edx/kill-the-cats
Minor clean-up.
2018-12-18 18:29:09 -05:00
Robert Raposa
1408b0ae7e Minor clean-up. 2018-12-18 17:35:37 -05:00
Robert Raposa
7b817a4234 Merge pull request #56 from edx/dynamic-copyright
ARCH-321: Dynamic copyright in footer.
2018-12-18 17:13:42 -05:00
Richard I Reilly
a762c47d77 Merge pull request #37 from edx/add-footer
ARCH-308: Reimplement LMS footer in React in Gradebook.
2018-12-18 16:55:08 -05:00
Robert Raposa
aecb93c252 Dynamic copyright in footer.
ARCH-321
2018-12-18 16:51:27 -05:00
Robert Raposa
5a489b1bd5 Add footer matching LMS courses footer.
Note: There are still some follow-up tasks in ARCH-308
for analytics, i18n, etc. This gets the base functionality
in place.

ARCH-308
2018-12-18 14:19:01 -05:00
Simon Chen
5c642a1be5 Merge pull request #54 from edx/schen/alert
Update the color of the alert from red to yellow
2018-12-13 15:30:20 -05:00
Simon Chen
9a0e0e0ece Update the color of the alert from red to yellow 2018-12-13 14:50:38 -05:00
Alex Dusenbery
7486a342e2 Merge pull request #53 from edx/aed/show-attempted
Distinguish unattempted subsections.
2018-12-13 14:20:18 -05:00
Alex Dusenbery
fd807c54f8 Update README.md 2018-12-12 16:12:15 -05:00
Alex Dusenbery
9b894b502f Distinguish unattempted subsections. 2018-12-12 15:03:42 -05:00
Douglas Hall
afd9692f6b Merge pull request #51 from edx/douglashall/fix_prod_webpack_config
fix(webpack): use the appropriate css loader in prod webpack config
2018-12-12 10:56:04 -05:00
Douglas Hall
8f77dea222 fix(webpack): use the appropriate css loader in prod webpack config 2018-12-12 09:48:15 -05:00
Simon Chen
3bf3acaaec Merge pull request #50 from edx/schen/frozen_grades
fix(feat): Prevent editing if course grades are frozen
2018-12-11 13:45:23 -05:00
Simon Chen
f1ab3d0330 fix(feat): Prevent editing if course grades are frozen 2018-12-11 13:29:00 -05:00
Simon Chen
8d89bc16b1 Merge pull request #48 from edx/schen/update_wording
Update link wording
2018-12-10 15:29:01 -05:00
Simon Chen
b46d47286b Update link wording 2018-12-10 14:22:49 -05:00
23 changed files with 6668 additions and 6912 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.DS_Store
.eslintcache
.idea
node_modules
npm-debug.log
coverage

View File

@@ -23,7 +23,7 @@ npm i --save @edx/gradebook
After cloning the repository, run `make up-detached` in the `gradebook` directory - this will build and start the `gradebook` web application in a docker container.
The web application runs on port **1991**, so when you go to `http://localhost:1991` you should see the UI.
The web application runs on port **1991**, so when you go to `http://localhost:1991/course-v1:edX+DemoX+Demo_Course` you should see the UI (assuming you have such a Demo Course in your devstack). Note that you always have to provide a course id to actually see a gradebook.
If you don't, you can see the log messages for the docker container by executing `make logs` in the `gradebook` directory.

BIN
assets/logo-footer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -4,8 +4,8 @@ const Merge = require('webpack-merge');
const commonConfig = require('./webpack.common.config.js');
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = Merge.smart(commonConfig, {
mode: 'production',
@@ -38,29 +38,27 @@ module.exports = Merge.smart(commonConfig, {
// increases build time.
{
test: /(.scss|.css)$/,
use: ExtractTextPlugin.extract({
// creates style nodes from JS strings, only used if extracting fails
fallback: 'style-loader',
use: [
{
loader: 'css-loader', // translates CSS into CommonJS
options: {
sourceMap: true,
minimize: true,
},
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader', // translates CSS into CommonJS
options: {
sourceMap: true,
minimize: true,
},
{
loader: 'sass-loader', // compiles Sass to CSS
options: {
sourceMap: true,
includePaths: [
path.join(__dirname, '../node_modules'),
path.join(__dirname, '../src'),
],
},
},
'postcss-loader',
{
loader: 'sass-loader', // compiles Sass to CSS
options: {
sourceMap: true,
includePaths: [
path.join(__dirname, '../node_modules'),
path.join(__dirname, '../src'),
],
},
],
}),
},
],
},
// Webpack, by default, uses the url-loader for images and fonts that are required/included by
// files it processes, which just base64 encodes them and inlines them in the javascript
@@ -105,9 +103,8 @@ module.exports = Merge.smart(commonConfig, {
// Specify additional processing or side-effects done on the Webpack output bundles as a whole.
plugins: [
// Writes the extracted CSS from each entry to a file in the output directory.
new ExtractTextPlugin({
filename: '[name].min.css',
allChunks: true,
new MiniCssExtractPlugin({
filename: '[name].[chunkhash].css',
}),
// Generates an HTML file in the output directory.
new HtmlWebpackPlugin({

12919
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",
@@ -33,13 +33,14 @@
"font-awesome": "^4.7.0",
"history": "^4.7.2",
"prop-types": "^15.5.10",
"query-string": "^6.2.0",
"query-string": "^5.1.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-redux": "^5.0.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",
@@ -47,6 +48,7 @@
"whatwg-fetch": "^2.0.3"
},
"devDependencies": {
"autoprefixer": "^9.4.2",
"axios-mock-adapter": "^1.15.0",
"babel-cli": "^6.26.0",
"babel-eslint": "^8.2.2",
@@ -62,7 +64,6 @@
"enzyme-adapter-react-16": "^1.1.1",
"es-check": "^2.0.2",
"eslint-config-edx": "^4.0.3",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"fetch-mock": "^6.3.0",
"file-loader": "^1.1.9",
"html-webpack-harddisk-plugin": "^0.2.0",
@@ -71,7 +72,9 @@
"identity-obj-proxy": "^3.0.0",
"image-webpack-loader": "^4.2.0",
"jest": "^22.4.0",
"mini-css-extract-plugin": "^0.4.0",
"node-sass": "^4.7.2",
"postcss-loader": "^3.0.0",
"react-dev-utils": "^5.0.0",
"react-test-renderer": "^16.2.0",
"redux-mock-store": "^1.5.1",
@@ -79,8 +82,8 @@
"semantic-release": "^15.10.7",
"style-loader": "^0.20.2",
"travis-deploy-once": "^5.0.9",
"webpack": "^4.1.0",
"webpack-cli": "^2.0.10",
"webpack": "^4.25.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.0",
"webpack-merge": "^4.1.1"
},

View File

@@ -7,3 +7,4 @@ $fa-font-path: "~font-awesome/fonts";
@import "~@edx/paragon/src/SearchField/SearchField";
@import "./components/Gradebook/gradebook";
@import "./components/Gradebook/footer";

View File

@@ -0,0 +1,122 @@
import React from 'react';
import { Hyperlink, Icon } from '@edx/paragon';
import EdXLogo from '../../../assets/edx-sm.png';
export default function Footer() {
function renderLogo() {
return (
<img src={EdXLogo} alt="edX logo" height="30" width="60" />
);
}
return (
<footer
role="contentinfo"
aria-label="Page Footer"
className="footer d-flex justify-content-center border-top py-3 px-4"
>
<div className="max-width-1180 d-grid">
<div className="area-1">
<Hyperlink destination="https://www.edx.org/" content={renderLogo()} />
</div>
<div className="area-2">
<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>
<li><a href="https://www.edx.org/affiliate-program">Affiliates</a></li>
<li><a href="http://open.edx.org">Open edX</a></li>
<li><a href="https://www.edx.org/careers">Careers</a></li>
<li><a href="https://www.edx.org/news-announcements">News</a></li>
</ul>
</div>
<div className="area-3">
<h2>Legal</h2>
<ul className="list-unstyled p-0 m-0">
<li><a href="https://www.edx.org/edx-terms-service">Terms of Service &amp; Honor Code</a></li>
<li><a href="https://www.edx.org/edx-privacy-policy">Privacy Policy</a></li>
<li><a href="https://www.edx.org/accessibility">Accessibility Policy</a></li>
<li><a href="https://www.edx.org/trademarks">Trademark Policy</a></li>
<li><a href="https://www.edx.org/sitemap">Sitemap</a></li>
</ul>
</div>
<div className="area-4">
<h2>Connect</h2>
<ul className="list-unstyled p-0 m-0">
<li><a href="https://www.edx.org/blog">Blog</a></li>
<li><a href="https://courses.edx.org/support/contact_us">Contact Us</a></li>
<li><a href="https://support.edx.org">Help Center</a></li>
<li><a href="https://www.edx.org/media-kit">Media Kit</a></li>
<li><a href="https://www.edx.org/donate">Donate</a></li>
</ul>
</div>
<div className="area-5">
<ul
className="d-flex flex-row justify-content-between list-unstyled max-width-222 p-0 mb-4"
>
{/* TODO: Use Paragon HyperLink with Icon. */}
{/* Would need to add rel to paragon if we still need it. */}
<li>
<a href="http://www.facebook.com/EdxOnline" title="Facebook" rel="noopener noreferrer" target="_blank">
<Icon className={['fa', 'fa-facebook-square', 'fa-2x']} screenReaderText="Like edX on Facebook" />
</a>
</li>
<li>
<a href="https://twitter.com/edXOnline" title="Twitter" rel="noopener noreferrer" target="_blank">
<Icon className={['fa', 'fa-twitter-square', 'fa-2x']} screenReaderText="Follow edX on Twitter" />
</a>
</li>
<li>
<a href="https://www.youtube.com/user/edxonline" title="Youtube" rel="noopener noreferrer" target="_blank">
<Icon className={['fa', 'fa-youtube-square', 'fa-2x']} screenReaderText="Subscribe to the edX YouTube channel" />
</a>
</li>
<li>
<a href="https://www.linkedin.com/company/edx" title="LinkedIn" rel="noopener noreferrer" target="_blank">
<Icon className={['fa', 'fa-linkedin-square', 'fa-2x']} screenReaderText="Follow edX on LinkedIn" />
</a>
</li>
<li>
<a href="https://plus.google.com/+edXOnline" title="Google+" rel="noopener noreferrer" target="_blank">
<Icon className={['fa', 'fa-google-plus-square', 'fa-2x']} screenReaderText="Follow edX on Google+" />
</a>
</li>
<li>
<a href="https://www.reddit.com/r/edx" title="Reddit" rel="noopener noreferrer" target="_blank">
<Icon className={['fa', 'fa-reddit-square', 'fa-2x']} screenReaderText="Subscribe to the edX subreddit" />
</a>
</li>
</ul>
<ul className="d-flex flex-row justify-content-between list-unstyled max-width-264 p-0 mb-5">
<li>
<a href="https://itunes.apple.com/us/app/edx/id945480667?mt=8" rel="noopener noreferrer" target="_blank">
<img
className="max-height-39"
alt="Download the edX mobile app from the Apple App Store"
src="https://prod-edxapp.edx-cdn.org/static/images/app/app_store_badge_135x40.d0558d910630.svg"
/>
</a>
</li>
<li>
<a href="https://play.google.com/store/apps/details?id=org.edx.mobile" rel="noopener noreferrer" target="_blank">
<img
className="max-height-39"
alt="Download the edX mobile app from Google Play"
src="https://prod-edxapp.edx-cdn.org/static/images/app/google_play_badge_45.6ea466e328da.png"
/>
</a>
</li>
</ul>
<p>
© 2012{(new Date().getFullYear())} edX Inc.
<br />
EdX, Open edX, and MicroMasters are registered trademarks of edX Inc.
| 粤ICP备17044299号-2
</p>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,165 @@
.max-width-222 {
max-width: 222px;
}
.max-width-264 {
max-width: 264px;
}
.max-width-1180 {
max-width: 1180px;
}
.max-height-39 {
max-height: 39px;
}
.d-grid {
display: grid;
}
$gray-footer: #fcfcfc;
$border-1: 1px solid $gray-200;
.footer {
background-color: $gray-footer;
.area-1 {
grid-column: 1;
grid-row: 1;
border-bottom: $border-1;
padding-bottom: 1rem;
}
.area-2 {
grid-column: 1;
grid-row: 2;
border-bottom: $border-1;
padding: 1rem 0;
}
.area-3 {
grid-column: 1;
grid-row: 3;
border-bottom: $border-1;
padding: 1rem 0;
}
.area-4 {
grid-column: 1;
grid-row: 4;
border-bottom: $border-1;
padding: 1rem 0;
}
.area-5 {
grid-column: 1;
grid-row: 5;
padding: 1rem 0;
}
@media only screen and (min-width: 717px) {
.area-1 {
grid-column: 1 / span 2;
grid-row: 1;
border-bottom: none;
padding: 1rem 0;
}
.area-2 {
grid-column: 1;
grid-row: 2;
}
.area-3 {
grid-column: 1;
grid-row: 3;
}
.area-4 {
grid-column: 1;
grid-row: 4;
border-bottom: none;
}
.area-5 {
grid-column: 2;
grid-row: 2 / span 3;
border-left: $border-1;
padding-left: 1rem;
margin-left: 1rem;
}
}
@media only screen and (min-width: 870px) {
.area-1 {
grid-column: 1;
grid-row: 1 / span 3;
border-right: $border-1;
padding-right: 1rem;
margin-right: 1rem;
}
.area-2 {
grid-column: 2;
grid-row: 1;
border-bottom: none;
border-right: $border-1;
padding-right: 1rem;
margin-right: 1rem;
}
.area-3 {
grid-column: 3;
grid-row: 1;
border-bottom: none;
border-right: $border-1;
padding-right: 1rem;
margin-right: 1rem;
}
.area-4 {
grid-column: 4;
grid-row: 1;
}
.area-5 {
grid-column: 2 / span 3;
grid-row: 2;
border: none;
margin-left: 0;
padding-left: 0;
}
}
@media only screen and (min-width: 1188px) {
.area-1 {
grid-column: 1 / span 1;
grid-row: 1;
}
.area-2 {
grid-column: 2;
grid-row: 1;
}
.area-3 {
grid-column: 3;
grid-row: 1;
}
.area-4 {
grid-column: 4;
grid-row: 1;
border-right: $border-1;
padding-right: 1rem;
margin-right: 1rem;
}
.area-5 {
grid-column: 5 / span 1;
grid-row: 1;
max-width: 372px;
}
}
}

View File

@@ -39,17 +39,23 @@ export default class Gradebook extends React.Component {
}
setNewModalState = (userEntry, subsection) => {
let adjustedGradePossible = '';
let currentGradePossible = '';
if (subsection.attempted) {
adjustedGradePossible = ` / ${subsection.score_possible}`;
currentGradePossible = `/${subsection.score_possible}`;
}
this.setState({
modalModel: [{
username: userEntry.username,
currentGrade: `${subsection.score_earned}/${subsection.score_possible}`,
currentGrade: `${subsection.score_earned}${currentGradePossible}`,
adjustedGrade: (
<span>
<input
style={{ width: '25px' }}
type="text"
onChange={event => this.setState({ updateVal: event.target.value })}
/> / {subsection.score_possible}
/>{adjustedGradePossible}
</span>
),
assignmentName: `${subsection.subsection_name}`,
@@ -179,36 +185,51 @@ export default class Gradebook extends React.Component {
roundGrade = percent => parseFloat(percent.toFixed(DECIMAL_PRECISION));
formatter = {
percent: entries => entries.map((entry) => {
percent: (entries, areGradesFrozen) => entries.map((entry) => {
const results = { username: entry.username };
const assignments = entry.section_breakdown
.filter(section => section.is_graded)
.reduce((acc, subsection) => {
acc[subsection.label] = (
<button
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{this.roundGrade(subsection.percent * 100)}%
</button>);
if (areGradesFrozen) {
acc[subsection.label] = `${this.roundGrade(subsection.percent * 100)} %`;
} else {
acc[subsection.label] = (
<button
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{this.roundGrade(subsection.percent * 100)}%
</button>);
}
return acc;
}, {});
const totals = { total: `${this.roundGrade(entry.percent * 100)}%` };
return Object.assign(results, assignments, totals);
}),
absolute: entries => entries.map((entry) => {
absolute: (entries, areGradesFrozen) => entries.map((entry) => {
const results = { username: entry.username };
const assignments = entry.section_breakdown
.filter(section => section.is_graded)
.reduce((acc, subsection) => {
acc[subsection.label] = (
<button
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{this.roundGrade(subsection.score_earned)}/{this.roundGrade(subsection.score_possible)}
</button>);
const scoreEarned = this.roundGrade(subsection.score_earned);
const scorePossible = this.roundGrade(subsection.score_possible);
let label = `${scoreEarned}`;
if (subsection.attempted) {
label = `${scoreEarned}/${scorePossible}`;
}
if (areGradesFrozen) {
acc[subsection.label] = label;
} else {
acc[subsection.label] = (
<button
className="btn btn-header link-style"
onClick={() => this.setNewModalState(entry, subsection)}
>
{label}
</button>
);
}
return acc;
}, {});
@@ -233,6 +254,11 @@ export default class Gradebook extends React.Component {
</a>
<h1>Gradebook</h1>
<h3> {this.props.match.params.courseId}</h3>
{ this.props.areGradesFrozen &&
<div className="alert alert-warning" role="alert" >
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
}
<hr />
<div className="d-flex justify-content-between" >
<div>
@@ -300,7 +326,7 @@ export default class Gradebook extends React.Component {
</div>
<div>
<div style={{ marginLeft: '10px', marginBottom: '10px' }}>
<a href={`${this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}#view-data_download`}>Download Grade Report</a>
<a href={`${this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}#view-data_download`}>Generate Grade Report</a>
</div>
<SearchField
onSubmit={value => this.props.searchForUser(this.props.match.params.courseId, value, this.props.selectedCohort, this.props.selectedTrack)}
@@ -335,7 +361,7 @@ export default class Gradebook extends React.Component {
<div className="gbook">
<Table
columns={this.props.headings}
data={this.formatter[this.props.format](this.props.grades)}
data={this.formatter[this.props.format](this.props.grades, this.props.areGradesFrozen)}
tableSortable
defaultSortDirection="asc"
defaultSortedColumn="username"

View File

@@ -1,30 +1,94 @@
import React from 'react';
import { Hyperlink } from '@edx/paragon';
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>
</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;

View File

@@ -27,6 +27,7 @@ const mapStateToProps = state => (
prevPage: state.grades.prevPage,
nextPage: state.grades.nextPage,
assignmnetTypes: state.assignmentTypes.results,
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
showSpinner: state.grades.showSpinner,
}
);

View 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);

View File

@@ -2,12 +2,14 @@ import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';
import LmsApiService from '../services/LmsApiService';
const startedFetchingAssignmentTypes = () => ({ type: STARTED_FETCHING_ASSIGNMENT_TYPES });
const errorFetchingAssignmentTypes = () => ({ type: ERROR_FETCHING_ASSIGNMENT_TYPES });
const gotAssignmentTypes = assignmentTypes => ({ type: GOT_ASSIGNMENT_TYPES, assignmentTypes });
const gotGradesFrozen = areGradesFrozen => ({ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen });
const fetchAssignmentTypes = courseId => (
(dispatch) => {
@@ -16,6 +18,7 @@ const fetchAssignmentTypes = courseId => (
.then(response => response.data)
.then((data) => {
dispatch(gotAssignmentTypes(Object.keys(data.assignment_types)));
dispatch(gotGradesFrozen(data.grades_frozen));
})
.catch(() => {
dispatch(errorFetchingAssignmentTypes());

View File

@@ -9,6 +9,7 @@ import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';
const mockStore = configureMockStore([thunk]);
@@ -21,29 +22,30 @@ describe('actions', () => {
describe('fetchAssignmentTypes', () => {
const courseId = 'course-v1:edX+DemoX+Demo_Course';
it('dispatches success action after fetching fetchAssignmentTypes', () => {
const responseData = {
assignment_types: {
Exam: {
drop_count: 0,
min_count: 1,
short_label: 'Exam',
type: 'Exam',
weight: 0.25,
},
Homework: {
drop_count: 1,
min_count: 3,
short_label: 'Ex',
type: 'Homework',
weight: 0.75,
},
const responseData = {
assignment_types: {
Exam: {
drop_count: 0,
min_count: 1,
short_label: 'Exam',
type: 'Exam',
weight: 0.25,
},
};
Homework: {
drop_count: 1,
min_count: 3,
short_label: 'Ex',
type: 'Homework',
weight: 0.75,
},
},
grades_frozen: false,
};
it('dispatches success action after fetching fetchAssignmentTypes', () => {
const expectedActions = [
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: responseData.grades_frozen },
];
const store = mockStore();
@@ -69,5 +71,21 @@ describe('actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it('dispatches frozen grade action with True value after fetching', () => {
const expectedActions = [
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
{ type: GOT_ASSIGNMENT_TYPES, assignmentTypes: Object.keys(responseData.assignment_types) },
{ type: GOT_ARE_GRADES_FROZEN, areGradesFrozen: true },
];
const store = mockStore();
responseData.grades_frozen = true;
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
.replyOnce(200, JSON.stringify(responseData));
return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});

View File

@@ -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,

View File

@@ -1,10 +1,12 @@
const STARTED_FETCHING_ASSIGNMENT_TYPES = 'STARTED_FETCHING_ASSIGNMENT_TYPES';
const GOT_ASSIGNMENT_TYPES = 'GOT_ASSIGNMENT_TYPES';
const ERROR_FETCHING_ASSIGNMENT_TYPES = 'ERROR_FETCHING_ASSIGNMENT_TYPES';
const GOT_ARE_GRADES_FROZEN = 'GOT_ARE_GRADES_FROZEN';
export {
STARTED_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
};

View File

@@ -2,6 +2,7 @@ import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';
const initialState = {
@@ -31,6 +32,13 @@ const assignmentTypes = (state = initialState, action) => {
finishedFetching: true,
errorFetching: true,
};
case GOT_ARE_GRADES_FROZEN:
return {
...state,
areGradesFrozen: action.areGradesFrozen,
errorFetching: false,
finishedFetching: true,
};
default:
return state;
}

View File

@@ -3,6 +3,7 @@ import {
STARTED_FETCHING_ASSIGNMENT_TYPES,
ERROR_FETCHING_ASSIGNMENT_TYPES,
GOT_ASSIGNMENT_TYPES,
GOT_ARE_GRADES_FROZEN,
} from '../constants/actionTypes/assignmentTypes';
const initialState = {
@@ -51,4 +52,17 @@ describe('assignmentTypes reducer', () => {
type: ERROR_FETCHING_ASSIGNMENT_TYPES,
})).toEqual(expected);
});
it('updates areGradesFrozen success state', () => {
const expected = {
...initialState,
errorFetching: false,
finishedFetching: true,
areGradesFrozen: true,
};
expect(assignmentTypes(undefined, {
type: GOT_ARE_GRADES_FROZEN,
areGradesFrozen: true,
})).toEqual(expected);
});
});

View File

@@ -1,11 +1,21 @@
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';
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,

View File

@@ -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)),
);

View File

@@ -3,27 +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';
const App = () => (
<Provider store={store}>
<Router>
<div>
<Header />
<main>
<Switch>
<Route exact path="/:courseId" component={GradebookPage} />
</Switch>
</main>
</div>
</Router>
</Provider>
);
class App extends React.Component {
componentDidMount() {
const username = store.getState().authentication.username;
store.dispatch(fetchUserProfile(apiClient, username));
}
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'));

7
src/postcss.config.js Normal file
View File

@@ -0,0 +1,7 @@
/* I'm here to allow autoprefixing in webpack.prod.config.js */
module.exports = {
plugins: [
require('autoprefixer')({ grid: true, browsers: ['>1%'] }),
],
};