Compare commits

..

1 Commits

Author SHA1 Message Date
Douglas Hall
66d647f381 feat(header): add new responsive header 2018-12-19 09:20:52 -05:00
36 changed files with 5468 additions and 7376 deletions

View File

@@ -1,4 +1,3 @@
coverage/*
dist/
node_modules/
src/segment.js

29
Dockerfile Executable file
View File

@@ -0,0 +1,29 @@
# Copied from https://github.com/BretFisher/node-docker-good-defaults/blob/master/Dockerfile
FROM node:8.9.3
# Create app directory
RUN mkdir -p /edx/app
ARG NODE_ENV=production
ENV NODE_ENV $NODE_ENV
ARG PORT=80
ENV PORT $PORT
EXPOSE $PORT 1991
WORKDIR /edx
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
# If you are building your code for production
# RUN npm install --only=production
RUN npm install
ENV PATH /edx/app/node_modules/.bin:$PATH
WORKDIR /edx/app
COPY . /edx/app
ENTRYPOINT npm install && npm run start

View File

@@ -1,9 +1,35 @@
npm-install-%: ## install specified % npm package
npm install $* --save-dev
shell: ## run a shell on the cookie-cutter container
docker exec -it edx.gradebook /bin/bash
build:
docker-compose build
up: ## bring up cookie-cutter container
docker-compose up
up-detached: ## bring up cookie-cutter container in detached mode
docker-compose up -d
logs: ## show logs for cookie-cutter container
docker-compose logs -f
down: ## stop and remove cookie-cutter container
docker-compose down
npm-install-%: ## install specified % npm package on the cookie-cutter container
docker exec npm install $* --save-dev
git add package.json
restart:
make down
make up
restart-detached:
make down
make up-detached
validate-no-uncommitted-package-lock-changes:
git diff --exit-code package-lock.json
test:
npm run test
docker exec -it edx.gradebook jest

View File

@@ -21,20 +21,20 @@ npm i --save @edx/gradebook
## Running the UI Standalone
To install the project please refer to the [`edX Developer Stack`](https://github.com/edx/devstack) instructions.
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 **1994**, so when you go to `http://localhost:1994/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.
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 gradebook-logs` in the `devstack` directory.
If you don't, you can see the log messages for the docker container by executing `make logs` in the `gradebook` directory.
Note that starting the container executes the `npm run start` script which will hot-reload JavaScript and Sass files changes, so you should (:crossed_fingers:) not need to do anything (other than wait) when making changes.
Note that `make up-detached` executes the `npm run start` script which will hot-reload JavaScript and Sass files changes, so you should (:crossed_fingers:) not need to do anything (other than wait) when making changes.
## Configuring for local use in edx-platform
Assuming you've got the UI running at `http://localhost:1994`, you can configure the LMS in edx-platform
Assuming you've got the UI running at `http://localhost:1991`, you can configure the LMS in edx-platform
to point to your local gradebook from the instructor dashboard by putting this settings in `lms/env/private.py`:
```
WRITABLE_GRADEBOOK_URL = 'http://localhost:1994'
WRITABLE_GRADEBOOK_URL = 'http://localhost:1991'
```
There are also several edx-platform waffle and feature flags you'll have to enable from the Django admin:

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -4,7 +4,6 @@ const path = require('path');
module.exports = {
entry: {
segment: path.resolve(__dirname, '../src/segment.js'),
app: path.resolve(__dirname, '../src/index.jsx'),
},
output: {

View File

@@ -12,7 +12,6 @@ module.exports = Merge.smart(commonConfig, {
entry: [
// enable react's custom hot dev client so we get errors reported in the browser
require.resolve('react-dev-utils/webpackHotDevClient'),
path.resolve(__dirname, '../src/segment.js'),
path.resolve(__dirname, '../src/index.jsx'),
],
module: {
@@ -98,7 +97,7 @@ module.exports = Merge.smart(commonConfig, {
}),
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
BASE_URL: 'localhost:1994',
BASE_URL: 'localhost:1991',
LMS_BASE_URL: 'http://localhost:18000',
LOGIN_URL: 'http://localhost:18000/login',
LOGOUT_URL: 'http://localhost:18000/login',
@@ -111,21 +110,6 @@ module.exports = Merge.smart(commonConfig, {
FEATURE_FLAGS: {},
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
CSRF_COOKIE_NAME: 'csrftoken',
SITE_NAME: 'edX',
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
SUPPORT_URL: 'http://localhost:18000/support',
CONTACT_URL: 'http://localhost:18000/contact',
OPEN_SOURCE_URL: 'http://localhost:18000/openedx',
TERMS_OF_SERVICE_URL: 'http://localhost:18000/terms-of-service',
PRIVACY_POLICY_URL: 'http://localhost:18000/privacy-policy',
FACEBOOK_URL: 'https://www.facebook.com',
TWITTER_URL: 'https://twitter.com',
YOU_TUBE_URL: 'https://www.youtube.com',
LINKED_IN_URL: 'https://www.linkedin.com',
GOOGLE_PLUS_URL: 'https://plus.google.com',
REDDIT_URL: 'https://www.reddit.com',
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
GOOGLE_PLAY_URL: 'https://play.google.com/store',
}),
// when the --hot option is not passed in as part of the command
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
@@ -136,7 +120,7 @@ module.exports = Merge.smart(commonConfig, {
// reloading.
devServer: {
host: '0.0.0.0',
port: 1994,
port: 1991,
historyApiFallback: true,
hot: true,
inline: true,

View File

@@ -126,21 +126,6 @@ module.exports = Merge.smart(commonConfig, {
CSRF_COOKIE_NAME: 'csrftoken',
NEW_RELIC_APP_ID: null,
NEW_RELIC_LICENSE_KEY: null,
SITE_NAME: null,
MARKETING_SITE_BASE_URL: null,
SUPPORT_URL: null,
CONTACT_URL: null,
OPEN_SOURCE_URL: null,
TERMS_OF_SERVICE_URL: null,
PRIVACY_POLICY_URL: null,
FACEBOOK_URL: null,
TWITTER_URL: null,
YOU_TUBE_URL: null,
LINKED_IN_URL: null,
GOOGLE_PLUS_URL: null,
REDDIT_URL: null,
APPLE_APP_STORE_URL: null,
GOOGLE_PLAY_URL: null,
}),
],
});

20
docker-compose.yml Executable file
View File

@@ -0,0 +1,20 @@
version: "2"
services:
web:
build:
context: .
dockerfile: Dockerfile
args:
- NODE_ENV=development
container_name: edx.gradebook
image: edxops/front-end-cookie-cutter:latest
volumes:
- .:/edx/app:delegated
- notused:/edx/app/node_modules
ports:
- "1991:1991"
environment:
- NODE_ENV=development
volumes:
notused:

View File

@@ -1,46 +0,0 @@
Usage of the bulk-update API
============================
Context
=======
The LMS Grades API exposes a set of Gradebook-related endpoints:
https://github.com/edx/edx-platform/blob/master/lms/djangoapps/grades/api/v1/gradebook_views.py
The ``bulk-update`` endpoint defined therein allows for the creation/modification of subsection
grades for multiple users and sections in a single request. This allows clients of the API to limit
the number of network requests made and to more easily manage client-side data. Moreover,
the course grade updates that occur during calls to this API are synchronous - the entire update operation
is completed before a response is given to the client.
For decisions made about the implementation of this API, see:
https://github.com/edx/edx-platform/blob/master/lms/djangoapps/grades/docs/decisions/0001-gradebook-api.rst
Decision
========
The Gradebook front-end will post data about a single subsection and user in a single request
to the ``bulk-update`` API. That is, we currently need only the "update" aspect of this
endpoint, and not the "bulk" aspect, for satisfying the requirements of the current UX.
Status
======
Accepted (circa December 2018)
Consequences
============
This is a scenario in which the implementation of the API is coupled to the
UX that depends on the API. Because the course grade update is synchronous, it means
the API response can contain the updated subsection and course grade data. Because
a response from the API contains this data, the UI can operate in a very familiar way:
- A user clicks a button to submit a request with grade update data to the update endpoint.
- On the server, the subsection and course grades are modified.
- In the meantime, the client-side user looks at a spinner.
- A response is returned with updated data and the spinner goes away.
- Updated data is displayed to the user, along with a message indicative of the update.
If the update becomes asynchronous, the user experience outlined above has to change.
Because a single call to this endpoint updates grades data for only a single user,
the endpoint does not necessarily have to utilize an asynchronous operation at this time.

View File

@@ -1,8 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
nick: grbk
oeps: {}
owner: edx/educator-neem
openedx-release: {ref: master}
track-pulls: true

11367
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,68 +26,66 @@
"dependencies": {
"@edx/edx-bootstrap": "^0.4.3",
"@edx/frontend-auth": "^1.3.0",
"@edx/frontend-component-footer": "^1.0.0",
"@edx/paragon": "^3.8.3",
"@redux-beacon/segment": "^1.0.0",
"@edx/paragon": "^3.7.2",
"babel-polyfill": "^6.26.0",
"classnames": "^2.2.6",
"email-prop-type": "^1.1.7",
"classnames": "^2.2.5",
"email-prop-type": "^1.1.5",
"font-awesome": "^4.7.0",
"history": "^4.7.2",
"prop-types": "^15.6.2",
"prop-types": "^15.5.10",
"query-string": "^5.1.1",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-redux": "^5.1.1",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.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.7",
"redux-beacon": "^2.0.3",
"redux-devtools-extension": "^2.13.2",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"whatwg-fetch": "^2.0.4"
"redux-thunk": "^2.2.0",
"whatwg-fetch": "^2.0.3"
},
"devDependencies": {
"autoprefixer": "^9.4.5",
"axios-mock-adapter": "^1.16.0",
"autoprefixer": "^9.4.2",
"axios-mock-adapter": "^1.15.0",
"babel-cli": "^6.26.0",
"babel-eslint": "^8.2.6",
"babel-jest": "^22.4.4",
"babel-loader": "^7.1.5",
"babel-eslint": "^8.2.2",
"babel-jest": "^22.4.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"codecov": "^3.1.0",
"css-loader": "^0.28.11",
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "^1.7.1",
"es-check": "^2.3.0",
"eslint-config-edx": "^4.0.4",
"fetch-mock": "^6.5.2",
"codecov": "^3.0.0",
"css-loader": "^0.28.9",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"es-check": "^2.0.2",
"eslint-config-edx": "^4.0.3",
"fetch-mock": "^6.3.0",
"file-loader": "^1.1.9",
"html-webpack-harddisk-plugin": "^0.2.0",
"html-webpack-plugin": "^3.2.0",
"html-webpack-plugin": "^3.0.3",
"husky": "^0.14.3",
"identity-obj-proxy": "^3.0.0",
"image-webpack-loader": "^4.2.0",
"jest": "^22.4.4",
"jest": "^22.4.0",
"mini-css-extract-plugin": "^0.4.0",
"node-sass": "^4.11.0",
"node-sass": "^4.7.2",
"postcss-loader": "^3.0.0",
"react-dev-utils": "^5.0.3",
"react-test-renderer": "^16.7.0",
"redux-mock-store": "^1.5.3",
"react-dev-utils": "^5.0.0",
"react-test-renderer": "^16.2.0",
"redux-mock-store": "^1.5.1",
"sass-loader": "^6.0.6",
"semantic-release": "^15.13.3",
"style-loader": "^0.20.3",
"travis-deploy-once": "^5.0.11",
"webpack": "^4.28.4",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14",
"webpack-merge": "^4.2.1"
"semantic-release": "^15.10.7",
"style-loader": "^0.20.2",
"travis-deploy-once": "^5.0.9",
"webpack": "^4.25.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.0",
"webpack-merge": "^4.1.1"
},
"jest": {
"setupFiles": [
@@ -108,7 +106,6 @@
],
"transformIgnorePatterns": [
"/node_modules/(?!(@edx/paragon)/).*/"
],
"testURL": "http://localhost"
]
}
}

View File

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

View File

@@ -6,6 +6,5 @@ $fa-font-path: "~font-awesome/fonts";
@import "~@edx/paragon/src/SearchField/SearchField";
@import "~@edx/frontend-component-footer/src/lib/scss/site-footer";
@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

@@ -68,10 +68,6 @@
padding-left: 170px;
}
.table tbody th {
font-weight: normal;
}
.link-style {
color: #0075b4;
&:hover, &:focus {

View File

@@ -10,7 +10,6 @@ import {
} from '@edx/paragon';
import queryString from 'query-string';
import { configuration } from '../../config';
import PageButtons from '../PageButtons';
const DECIMAL_PRECISION = 2;
@@ -29,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) => {
@@ -176,7 +182,7 @@ export default class Gradebook extends React.Component {
return 'Tracks';
};
roundGrade = percent => parseFloat((percent || 0).toFixed(DECIMAL_PRECISION));
roundGrade = percent => parseFloat(percent.toFixed(DECIMAL_PRECISION));
formatter = {
percent: (entries, areGradesFrozen) => entries.map((entry) => {
@@ -244,7 +250,7 @@ export default class Gradebook extends React.Component {
href={this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}
className="mb-3"
>
<span aria-hidden="true">{'<< '}</span> {'Back to Dashboard'}
{'<< Back to Dashboard'}
</a>
<h1>Gradebook</h1>
<h3> {this.props.match.params.courseId}</h3>
@@ -253,16 +259,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 === false) &&
<div className="alert alert-warning" role="alert" >
You are not authorized to view the gradebook for this course.
</div>
}
<hr />
<div className="d-flex justify-content-between" >
<div>
<div role="radiogroup" aria-labelledby="score-view-group-label">
<span id="score-view-group-label">Score View:</span>
<div>
Score View:
<span>
<input
id="score-view-percent"
@@ -270,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>
@@ -294,34 +294,35 @@ export default class Gradebook extends React.Component {
</span>
<InputSelect
name="assignment-types"
ariaLabel="Assignment Types"
value={this.mapSelectedTrackEntry(this.props.selectedAssignmentType)}
options={this.mapAssignmentTypeEntries(this.props.assignmnetTypes)}
onChange={this.updateAssignmentTypes}
/>
</div>
}
<div className="student-filters">
<span className="label">
Student Groups:
</span>
<InputSelect
name="Tracks"
ariaLabel="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"
ariaLabel="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' }}>
@@ -333,6 +334,21 @@ export default class Gradebook extends React.Component {
onClear={() => this.props.getUserGrades(this.props.match.params.courseId, this.props.selectedCohort, this.props.selectedTrack)}
value={this.state.filterValue}
/>
<div className="d-flex justify-content-end" style={{ marginTop: '20px' }}>
<Button
label="Previous"
buttonType="primary"
style={{ visibility: (!this.props.prevPage ? 'hidden' : 'visible') }}
onClick={() => this.props.getPrevNextGrades(this.props.prevPage, this.props.selectedCohort, this.props.selectedTrack)}
/>
<div style={{ width: '10px' }} />
<Button
label="Next"
buttonType="primary"
style={{ visibility: (!this.props.nextPage ? 'hidden' : 'visible') }}
onClick={() => this.props.getPrevNextGrades(this.props.nextPage, this.props.selectedCohort, this.props.selectedTrack)}
/>
</div>
</div>
</div>
<br />
@@ -342,7 +358,6 @@ export default class Gradebook extends React.Component {
onClose={() => this.props.updateBanner(false)}
open={this.props.showSuccess}
/>
{PageButtons(this.props)}
<div className="gbook">
<Table
columns={this.props.headings}
@@ -350,14 +365,11 @@ export default class Gradebook extends React.Component {
tableSortable
defaultSortDirection="asc"
defaultSortedColumn="username"
rowHeaderColumnKey="username"
/>
</div>
{PageButtons(this.props)}
<Modal
open={this.state.modalOpen}
title="Edit Grades"
closeText="Cancel"
body={(
<div>
<h3>{this.state.modalModel[0].assignmentName}</h3>
@@ -365,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}
/>,

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

@@ -1,36 +0,0 @@
import renderer from 'react-test-renderer';
import PageButtons from '.';
const createInput = function createInput(prevPage, nextPage) {
return {
prevPage,
nextPage,
selectedTrack: 't',
selectedCohort: 'c',
getPrevNextGrades() {},
};
};
describe('PageButtons', () => {
const assertPageButtonsSnapshot = function assertPageButtonsSnapshot(input) {
const pb = renderer.create(PageButtons(input));
const tree = pb.toJSON();
expect(tree).toMatchSnapshot();
};
it('prev null, next null', () => {
assertPageButtonsSnapshot(createInput(null, null));
});
it('prev null, next not null', () => {
assertPageButtonsSnapshot(createInput(null, 'np'));
});
it('prev not null, next null', () => {
assertPageButtonsSnapshot(createInput('pp', null));
});
it('prev not null, next not null', () => {
assertPageButtonsSnapshot(createInput('pp', 'np'));
});
});

View File

@@ -1,169 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PageButtons prev not null, next not null 1`] = `
<div
className="d-flex justify-content-center"
style={
Object {
"paddingBottom": "20px",
}
}
>
<button
className="btn btn-primary"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"margin": "20px",
}
}
type="button"
>
Previous Page
</button>
<button
className="btn btn-primary"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"margin": "20px",
}
}
type="button"
>
Next Page
</button>
</div>
`;
exports[`PageButtons prev not null, next null 1`] = `
<div
className="d-flex justify-content-center"
style={
Object {
"paddingBottom": "20px",
}
}
>
<button
className="btn btn-primary"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"margin": "20px",
}
}
type="button"
>
Previous Page
</button>
<button
className="btn btn-primary"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"margin": "20px",
}
}
type="button"
>
Next Page
</button>
</div>
`;
exports[`PageButtons prev null, next not null 1`] = `
<div
className="d-flex justify-content-center"
style={
Object {
"paddingBottom": "20px",
}
}
>
<button
className="btn btn-primary"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"margin": "20px",
}
}
type="button"
>
Previous Page
</button>
<button
className="btn btn-primary"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"margin": "20px",
}
}
type="button"
>
Next Page
</button>
</div>
`;
exports[`PageButtons prev null, next null 1`] = `
<div
className="d-flex justify-content-center"
style={
Object {
"paddingBottom": "20px",
}
}
>
<button
className="btn btn-primary"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"margin": "20px",
}
}
type="button"
>
Previous Page
</button>
<button
className="btn btn-primary"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
style={
Object {
"margin": "20px",
}
}
type="button"
>
Next Page
</button>
</div>
`;

View File

@@ -1,30 +0,0 @@
import React from 'react';
import {
Button,
} from '@edx/paragon';
export default function PageButtons({prevPage, nextPage, selectedTrack, selectedCohort, getPrevNextGrades}) {
return (
<div
className="d-flex justify-content-center"
style={{ paddingBottom: '20px' }}
>
<Button
label="Previous Page"
style={{ margin: '20px' }}
buttonType="primary"
disabled={!prevPage}
onClick={() => getPrevNextGrades(prevPage, selectedCohort, selectedTrack)}
/>
<Button
label="Next Page"
style={{ margin: '20px' }}
buttonType="primary"
disabled={!nextPage}
onClick={() => getPrevNextGrades(nextPage, selectedCohort, selectedTrack)}
/>
</div>
);
}

View File

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

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

@@ -1,42 +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, courseId) => ({
type: GOT_ROLES,
canUserViewGradebook,
courseId,
});
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, courseId));
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

@@ -1,146 +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,
is_staff: isGlobalStaff,
};
}
function makeRoleObj(courseId, role) {
return {
course_id: courseId,
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, courseId: course1Id },
{ 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, courseId: course1Id },
{ 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, courseId: course1Id,
},
];
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, courseId: course1Id },
];
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, courseId: course1Id },
{ 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);
});
});
});
});

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,7 +0,0 @@
const GOT_ROLES = 'GOT_ROLES';
const ERROR_FETCHING_ROLES = 'ERROR_FETCHING_ROLES'
export {
GOT_ROLES,
ERROR_FETCHING_ROLES,
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,48 +2,17 @@ import { applyMiddleware, createStore } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import { createLogger } from 'redux-logger';
import { createMiddleware } from 'redux-beacon';
import Segment, { trackEvent, trackPageView } from '@redux-beacon/segment';
import { GOT_ROLES } from './constants/actionTypes/roles';
import { GOT_GRADES, GRADE_UPDATE_SUCCESS, GRADE_UPDATE_FAILURE } from './constants/actionTypes/grades';
import apiClient from './apiClient';
import reducers from './reducers';
const loggerMiddleware = createLogger();
const eventsMap = {
[GOT_ROLES]: trackPageView(action => ({
page: action.courseId,
})),
[GOT_GRADES]: trackEvent(action => ({
name: 'Grades displayed or paginated',
properties: {
track: action.track,
cohort: action.cohort,
prev: action.prev,
next: action.next,
},
})),
[GRADE_UPDATE_SUCCESS]: trackEvent(action => ({
name: 'Grades Updated',
properties: {
updatedGrades: action.payload.responseData,
},
})),
[GRADE_UPDATE_FAILURE]: trackEvent(action => ({
name: 'Grades Fail to Update',
properties: {
error: action.payload.error,
},
})),
};
const segmentMiddleware = createMiddleware(eventsMap, Segment());
const initialState = apiClient.getAuthenticationState();
const store = createStore(
reducers,
composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware, segmentMiddleware)),
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware)),
);
export default store;

View File

@@ -3,49 +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 SiteFooter from '@edx/frontend-component-footer';
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 FooterLogo from '../assets/edx-footer.png';
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>
<SiteFooter
siteName={process.env.SITE_NAME}
siteLogo={FooterLogo}
marketingSiteBaseUrl={process.env.MARKETING_SITE_BASE_URL}
supportUrl={process.env.SUPPORT_URL}
contactUrl={process.env.CONTACT_URL}
openSourceUrl={process.env.OPEN_SOURCE_URL}
termsOfServiceUrl={process.env.TERMS_OF_SERVICE_URL}
privacyPolicyUrl={process.env.PRIVACY_POLICY_URL}
facebookUrl={process.env.FACEBOOK_URL}
twitterUrl={process.env.TWITTER_URL}
youTubeUrl={process.env.YOU_TUBE_URL}
linkedInUrl={process.env.LINKED_IN_URL}
googlePlusUrl={process.env.GOOGLE_PLUS_URL}
redditUrl={process.env.REDDIT_URL}
appleAppStoreUrl={process.env.APPLE_APP_STORE_URL}
googlePlayUrl={process.env.GOOGLE_PLAY_URL}
/>
</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'));

View File

@@ -1,85 +0,0 @@
// The code in this file is from Segment's website:
// https://segment.com/docs/sources/website/analytics.js/quickstart/
import { configuration } from './config';
(function () {
// Create a queue, but don't obliterate an existing one!
const analytics = window.analytics = window.analytics || [];
// If the real analytics.js is already on the page return.
if (analytics.initialize) return;
// If the snippet was invoked already show an error.
if (analytics.invoked) {
if (window.console && console.error) {
console.error('Segment snippet included twice.');
}
return;
}
// Invoked flag, to make sure the snippet
// is never invoked twice.
analytics.invoked = true;
// A list of the methods in Analytics.js to stub.
analytics.methods = [
'trackSubmit',
'trackClick',
'trackLink',
'trackForm',
'pageview',
'identify',
'reset',
'group',
'track',
'ready',
'alias',
'debug',
'page',
'once',
'off',
'on',
];
// Define a factory to create stubs. These are placeholders
// for methods in Analytics.js so that you never have to wait
// for it to load to actually record data. The `method` is
// stored as the first argument, so we can replay the data.
analytics.factory = function (method) {
return function () {
const args = Array.prototype.slice.call(arguments);
args.unshift(method);
analytics.push(args);
return analytics;
};
};
// For each of our methods, generate a queueing stub.
for (let i = 0; i < analytics.methods.length; i++) {
const key = analytics.methods[i];
analytics[key] = analytics.factory(key);
}
// Define a method to load Analytics.js from our CDN,
// and that will be sure to only ever load it once.
analytics.load = function (key, options) {
// Create an async script element based on your key.
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = `https://cdn.segment.com/analytics.js/v1/${
key}/analytics.min.js`;
// Insert our script next to the first script element.
const first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(script, first);
analytics._loadOptions = options;
};
// Add a version to keep track of what's in the wild.
analytics.SNIPPET_VERSION = '4.1.0';
// Load Analytics.js with your key, which will automatically
// load the tools you've enabled for your account. Boosh!
analytics.load(configuration.SEGMENT_KEY);
}());