Compare commits
1 Commits
v1.4.8
...
douglashal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d647f381 |
@@ -1,4 +1,3 @@
|
|||||||
coverage/*
|
coverage/*
|
||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
src/segment.js
|
|
||||||
|
|||||||
29
Dockerfile
Executable file
29
Dockerfile
Executable 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
|
||||||
32
Makefile
32
Makefile
@@ -1,9 +1,35 @@
|
|||||||
npm-install-%: ## install specified % npm package
|
shell: ## run a shell on the cookie-cutter container
|
||||||
npm install $* --save-dev
|
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
|
git add package.json
|
||||||
|
|
||||||
|
restart:
|
||||||
|
make down
|
||||||
|
make up
|
||||||
|
|
||||||
|
restart-detached:
|
||||||
|
make down
|
||||||
|
make up-detached
|
||||||
|
|
||||||
validate-no-uncommitted-package-lock-changes:
|
validate-no-uncommitted-package-lock-changes:
|
||||||
git diff --exit-code package-lock.json
|
git diff --exit-code package-lock.json
|
||||||
|
|
||||||
test:
|
test:
|
||||||
npm run test
|
docker exec -it edx.gradebook jest
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -21,20 +21,20 @@ npm i --save @edx/gradebook
|
|||||||
|
|
||||||
## Running the UI Standalone
|
## 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
|
## 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`:
|
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:
|
There are also several edx-platform waffle and feature flags you'll have to enable from the Django admin:
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -4,7 +4,6 @@ const path = require('path');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
segment: path.resolve(__dirname, '../src/segment.js'),
|
|
||||||
app: path.resolve(__dirname, '../src/index.jsx'),
|
app: path.resolve(__dirname, '../src/index.jsx'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ module.exports = Merge.smart(commonConfig, {
|
|||||||
entry: [
|
entry: [
|
||||||
// enable react's custom hot dev client so we get errors reported in the browser
|
// enable react's custom hot dev client so we get errors reported in the browser
|
||||||
require.resolve('react-dev-utils/webpackHotDevClient'),
|
require.resolve('react-dev-utils/webpackHotDevClient'),
|
||||||
path.resolve(__dirname, '../src/segment.js'),
|
|
||||||
path.resolve(__dirname, '../src/index.jsx'),
|
path.resolve(__dirname, '../src/index.jsx'),
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
@@ -98,7 +97,7 @@ module.exports = Merge.smart(commonConfig, {
|
|||||||
}),
|
}),
|
||||||
new webpack.EnvironmentPlugin({
|
new webpack.EnvironmentPlugin({
|
||||||
NODE_ENV: 'development',
|
NODE_ENV: 'development',
|
||||||
BASE_URL: 'localhost:1994',
|
BASE_URL: 'localhost:1991',
|
||||||
LMS_BASE_URL: 'http://localhost:18000',
|
LMS_BASE_URL: 'http://localhost:18000',
|
||||||
LOGIN_URL: 'http://localhost:18000/login',
|
LOGIN_URL: 'http://localhost:18000/login',
|
||||||
LOGOUT_URL: 'http://localhost:18000/login',
|
LOGOUT_URL: 'http://localhost:18000/login',
|
||||||
@@ -111,21 +110,6 @@ module.exports = Merge.smart(commonConfig, {
|
|||||||
FEATURE_FLAGS: {},
|
FEATURE_FLAGS: {},
|
||||||
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
|
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
|
||||||
CSRF_COOKIE_NAME: 'csrftoken',
|
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
|
// when the --hot option is not passed in as part of the command
|
||||||
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
|
// the HotModuleReplacementPlugin has to be specified in the Webpack configuration
|
||||||
@@ -136,7 +120,7 @@ module.exports = Merge.smart(commonConfig, {
|
|||||||
// reloading.
|
// reloading.
|
||||||
devServer: {
|
devServer: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 1994,
|
port: 1991,
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
hot: true,
|
hot: true,
|
||||||
inline: true,
|
inline: true,
|
||||||
|
|||||||
@@ -126,21 +126,6 @@ module.exports = Merge.smart(commonConfig, {
|
|||||||
CSRF_COOKIE_NAME: 'csrftoken',
|
CSRF_COOKIE_NAME: 'csrftoken',
|
||||||
NEW_RELIC_APP_ID: null,
|
NEW_RELIC_APP_ID: null,
|
||||||
NEW_RELIC_LICENSE_KEY: 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
20
docker-compose.yml
Executable 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:
|
||||||
@@ -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
|
|
||||||
11375
package-lock.json
generated
11375
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
83
package.json
83
package.json
@@ -26,68 +26,66 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/edx-bootstrap": "^0.4.3",
|
"@edx/edx-bootstrap": "^0.4.3",
|
||||||
"@edx/frontend-auth": "^1.3.0",
|
"@edx/frontend-auth": "^1.3.0",
|
||||||
"@edx/frontend-component-footer": "^1.0.0",
|
"@edx/paragon": "^3.7.2",
|
||||||
"@edx/paragon": "^3.8.3",
|
|
||||||
"@redux-beacon/segment": "^1.0.0",
|
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.5",
|
||||||
"email-prop-type": "^1.1.7",
|
"email-prop-type": "^1.1.5",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"history": "^4.7.2",
|
"history": "^4.7.2",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.5.10",
|
||||||
"query-string": "^5.1.1",
|
"query-string": "^5.1.1",
|
||||||
"react": "^16.7.0",
|
"react": "^16.2.0",
|
||||||
"react-dom": "^16.7.0",
|
"react-dom": "^16.2.0",
|
||||||
"react-redux": "^5.1.1",
|
"react-redux": "^5.0.7",
|
||||||
"react-router": "^4.3.1",
|
"react-router": "^4.2.0",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.2.2",
|
||||||
"react-router-redux": "^5.0.0-alpha.9",
|
"react-router-redux": "^5.0.0-alpha.9",
|
||||||
|
"reactstrap": "^6.5.0",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
"redux-devtools-extension": "^2.13.7",
|
"redux-devtools-extension": "^2.13.2",
|
||||||
"redux-beacon": "^2.0.3",
|
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.2.0",
|
||||||
"whatwg-fetch": "^2.0.4"
|
"whatwg-fetch": "^2.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^9.4.5",
|
"autoprefixer": "^9.4.2",
|
||||||
"axios-mock-adapter": "^1.16.0",
|
"axios-mock-adapter": "^1.15.0",
|
||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.26.0",
|
||||||
"babel-eslint": "^8.2.6",
|
"babel-eslint": "^8.2.2",
|
||||||
"babel-jest": "^22.4.4",
|
"babel-jest": "^22.4.0",
|
||||||
"babel-loader": "^7.1.5",
|
"babel-loader": "^7.1.2",
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
"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",
|
"babel-preset-react": "^6.24.1",
|
||||||
"codecov": "^3.1.0",
|
"codecov": "^3.0.0",
|
||||||
"css-loader": "^0.28.11",
|
"css-loader": "^0.28.9",
|
||||||
"enzyme": "^3.8.0",
|
"enzyme": "^3.3.0",
|
||||||
"enzyme-adapter-react-16": "^1.7.1",
|
"enzyme-adapter-react-16": "^1.1.1",
|
||||||
"es-check": "^2.3.0",
|
"es-check": "^2.0.2",
|
||||||
"eslint-config-edx": "^4.0.4",
|
"eslint-config-edx": "^4.0.3",
|
||||||
"fetch-mock": "^6.5.2",
|
"fetch-mock": "^6.3.0",
|
||||||
"file-loader": "^1.1.9",
|
"file-loader": "^1.1.9",
|
||||||
"html-webpack-harddisk-plugin": "^0.2.0",
|
"html-webpack-harddisk-plugin": "^0.2.0",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.0.3",
|
||||||
"husky": "^0.14.3",
|
"husky": "^0.14.3",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"image-webpack-loader": "^4.2.0",
|
"image-webpack-loader": "^4.2.0",
|
||||||
"jest": "^22.4.4",
|
"jest": "^22.4.0",
|
||||||
"mini-css-extract-plugin": "^0.4.0",
|
"mini-css-extract-plugin": "^0.4.0",
|
||||||
"node-sass": "^4.11.0",
|
"node-sass": "^4.7.2",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"react-dev-utils": "^5.0.3",
|
"react-dev-utils": "^5.0.0",
|
||||||
"react-test-renderer": "^16.7.0",
|
"react-test-renderer": "^16.2.0",
|
||||||
"redux-mock-store": "^1.5.3",
|
"redux-mock-store": "^1.5.1",
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
"semantic-release": "^15.13.3",
|
"semantic-release": "^15.10.7",
|
||||||
"style-loader": "^0.20.3",
|
"style-loader": "^0.20.2",
|
||||||
"travis-deploy-once": "^5.0.11",
|
"travis-deploy-once": "^5.0.9",
|
||||||
"webpack": "^4.28.4",
|
"webpack": "^4.25.1",
|
||||||
"webpack-cli": "^3.2.1",
|
"webpack-cli": "^3.1.2",
|
||||||
"webpack-dev-server": "^3.1.14",
|
"webpack-dev-server": "^3.1.0",
|
||||||
"webpack-merge": "^4.2.1"
|
"webpack-merge": "^4.1.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
@@ -108,7 +106,6 @@
|
|||||||
],
|
],
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!(@edx/paragon)/).*/"
|
"/node_modules/(?!(@edx/paragon)/).*/"
|
||||||
],
|
]
|
||||||
"testURL": "http://localhost"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-us">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Gradebook | edX</title>
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -6,6 +6,5 @@ $fa-font-path: "~font-awesome/fonts";
|
|||||||
|
|
||||||
@import "~@edx/paragon/src/SearchField/SearchField";
|
@import "~@edx/paragon/src/SearchField/SearchField";
|
||||||
|
|
||||||
@import "~@edx/frontend-component-footer/src/lib/scss/site-footer";
|
|
||||||
|
|
||||||
@import "./components/Gradebook/gradebook";
|
@import "./components/Gradebook/gradebook";
|
||||||
|
@import "./components/Gradebook/footer";
|
||||||
|
|||||||
122
src/components/Gradebook/footer.jsx
Normal file
122
src/components/Gradebook/footer.jsx
Normal 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 & 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
165
src/components/Gradebook/footer.scss
Normal file
165
src/components/Gradebook/footer.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,10 +68,6 @@
|
|||||||
padding-left: 170px;
|
padding-left: 170px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table tbody th {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-style {
|
.link-style {
|
||||||
color: #0075b4;
|
color: #0075b4;
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import { configuration } from '../../config';
|
import { configuration } from '../../config';
|
||||||
import PageButtons from '../PageButtons';
|
|
||||||
|
|
||||||
const DECIMAL_PRECISION = 2;
|
const DECIMAL_PRECISION = 2;
|
||||||
|
|
||||||
@@ -29,7 +28,14 @@ export default class Gradebook extends React.Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const urlQuery = queryString.parse(this.props.location.search);
|
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) => {
|
setNewModalState = (userEntry, subsection) => {
|
||||||
@@ -244,7 +250,7 @@ export default class Gradebook extends React.Component {
|
|||||||
href={this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}
|
href={this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}
|
||||||
className="mb-3"
|
className="mb-3"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">{'<< '}</span> {'Back to Dashboard'}
|
{'<< Back to Dashboard'}
|
||||||
</a>
|
</a>
|
||||||
<h1>Gradebook</h1>
|
<h1>Gradebook</h1>
|
||||||
<h3> {this.props.match.params.courseId}</h3>
|
<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.
|
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||||
</div>
|
</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 />
|
<hr />
|
||||||
<div className="d-flex justify-content-between" >
|
<div className="d-flex justify-content-between" >
|
||||||
<div>
|
<div>
|
||||||
<div role="radiogroup" aria-labelledby="score-view-group-label">
|
<div>
|
||||||
<span id="score-view-group-label">Score View:</span>
|
Score View:
|
||||||
<span>
|
<span>
|
||||||
<input
|
<input
|
||||||
id="score-view-percent"
|
id="score-view-percent"
|
||||||
@@ -270,7 +271,6 @@ export default class Gradebook extends React.Component {
|
|||||||
type="radio"
|
type="radio"
|
||||||
name="score-view"
|
name="score-view"
|
||||||
value="percent"
|
value="percent"
|
||||||
defaultChecked
|
|
||||||
onClick={() => this.props.toggleFormat('percent')}
|
onClick={() => this.props.toggleFormat('percent')}
|
||||||
/>
|
/>
|
||||||
<label className="mr-2" htmlFor="score-view-percent">Percent</label>
|
<label className="mr-2" htmlFor="score-view-percent">Percent</label>
|
||||||
@@ -294,34 +294,35 @@ export default class Gradebook extends React.Component {
|
|||||||
</span>
|
</span>
|
||||||
<InputSelect
|
<InputSelect
|
||||||
name="assignment-types"
|
name="assignment-types"
|
||||||
ariaLabel="Assignment Types"
|
|
||||||
value={this.mapSelectedTrackEntry(this.props.selectedAssignmentType)}
|
value={this.mapSelectedTrackEntry(this.props.selectedAssignmentType)}
|
||||||
options={this.mapAssignmentTypeEntries(this.props.assignmnetTypes)}
|
options={this.mapAssignmentTypeEntries(this.props.assignmnetTypes)}
|
||||||
onChange={this.updateAssignmentTypes}
|
onChange={this.updateAssignmentTypes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div className="student-filters">
|
{(this.props.tracks.length > 0 || this.props.cohorts.length > 0) &&
|
||||||
<span className="label">
|
<div className="student-filters">
|
||||||
Student Groups:
|
<span className="label">
|
||||||
</span>
|
Student Groups:
|
||||||
<InputSelect
|
</span>
|
||||||
name="Tracks"
|
{this.props.tracks.length > 0 &&
|
||||||
ariaLabel="Tracks"
|
<InputSelect
|
||||||
disabled={this.props.tracks.length === 0}
|
name="Tracks"
|
||||||
value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
|
value={this.mapSelectedTrackEntry(this.props.selectedTrack)}
|
||||||
options={this.mapTracksEntries(this.props.tracks)}
|
options={this.mapTracksEntries(this.props.tracks)}
|
||||||
onChange={this.updateTracks}
|
onChange={this.updateTracks}
|
||||||
/>
|
/>
|
||||||
<InputSelect
|
}
|
||||||
name="Cohorts"
|
{this.props.cohorts.length > 0 &&
|
||||||
ariaLabel="Cohorts"
|
<InputSelect
|
||||||
disabled={this.props.cohorts.length === 0}
|
name="Cohorts"
|
||||||
value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
|
value={this.mapSelectedCohortEntry(this.props.selectedCohort)}
|
||||||
options={this.mapCohortsEntries(this.props.cohorts)}
|
options={this.mapCohortsEntries(this.props.cohorts)}
|
||||||
onChange={this.updateCohorts}
|
onChange={this.updateCohorts}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginLeft: '10px', marginBottom: '10px' }}>
|
<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)}
|
onClear={() => this.props.getUserGrades(this.props.match.params.courseId, this.props.selectedCohort, this.props.selectedTrack)}
|
||||||
value={this.state.filterValue}
|
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>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -342,7 +358,6 @@ export default class Gradebook extends React.Component {
|
|||||||
onClose={() => this.props.updateBanner(false)}
|
onClose={() => this.props.updateBanner(false)}
|
||||||
open={this.props.showSuccess}
|
open={this.props.showSuccess}
|
||||||
/>
|
/>
|
||||||
{PageButtons(this.props)}
|
|
||||||
<div className="gbook">
|
<div className="gbook">
|
||||||
<Table
|
<Table
|
||||||
columns={this.props.headings}
|
columns={this.props.headings}
|
||||||
@@ -350,14 +365,11 @@ export default class Gradebook extends React.Component {
|
|||||||
tableSortable
|
tableSortable
|
||||||
defaultSortDirection="asc"
|
defaultSortDirection="asc"
|
||||||
defaultSortedColumn="username"
|
defaultSortedColumn="username"
|
||||||
rowHeaderColumnKey="username"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{PageButtons(this.props)}
|
|
||||||
<Modal
|
<Modal
|
||||||
open={this.state.modalOpen}
|
open={this.state.modalOpen}
|
||||||
title="Edit Grades"
|
title="Edit Grades"
|
||||||
closeText="Cancel"
|
|
||||||
body={(
|
body={(
|
||||||
<div>
|
<div>
|
||||||
<h3>{this.state.modalModel[0].assignmentName}</h3>
|
<h3>{this.state.modalModel[0].assignmentName}</h3>
|
||||||
@@ -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' }]}
|
columns={[{ label: 'Username', key: 'username' }, { label: 'Current grade', key: 'currentGrade' }, { label: 'Adjusted grade', key: 'adjustedGrade' }]}
|
||||||
data={this.state.modalModel}
|
data={this.state.modalModel}
|
||||||
/>
|
/>
|
||||||
<div>Note: Once you save, your changes will be visible to students.</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
buttons={[
|
buttons={[
|
||||||
<Button
|
<Button
|
||||||
label="Save Grade"
|
label="Edit Grade"
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
onClick={this.handleAdjustedGradeClick}
|
onClick={this.handleAdjustedGradeClick}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@@ -1,30 +1,94 @@
|
|||||||
import React from 'react';
|
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';
|
import EdxLogo from '../../../assets/edx-sm.png';
|
||||||
|
|
||||||
export default class Header extends React.Component {
|
class Header extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.toggle = this.toggle.bind(this);
|
||||||
this.state = {
|
this.state = {
|
||||||
mobileNavOpen: false,
|
mobileNavOpen: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLogo() {
|
toggle() {
|
||||||
return (
|
this.setState({
|
||||||
<img src={EdxLogo} alt="edX logo" height="30" width="60" />
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<Navbar light expand="md" className="border-bottom">
|
||||||
<header className="d-flex justify-content-center align-items-center p-3 border-bottom-blue">
|
<NavbarBrand href={configuration.LMS_BASE_URL}>
|
||||||
<Hyperlink content={this.renderLogo()} destination="https://www.edx.org" />
|
<img src={EdxLogo} alt="edX logo" height="30" width="60" />
|
||||||
<div />
|
</NavbarBrand>
|
||||||
</header>
|
<NavbarToggler onClick={this.toggle} />
|
||||||
</div>
|
<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;
|
||||||
|
|||||||
@@ -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'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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>
|
|
||||||
`;
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
import { fetchCohorts } from '../../data/actions/cohorts';
|
import { fetchCohorts } from '../../data/actions/cohorts';
|
||||||
import { fetchTracks } from '../../data/actions/tracks';
|
import { fetchTracks } from '../../data/actions/tracks';
|
||||||
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes';
|
||||||
import { getRoles } from '../../data/actions/roles';
|
|
||||||
|
|
||||||
const mapStateToProps = state => (
|
const mapStateToProps = state => (
|
||||||
{
|
{
|
||||||
@@ -29,21 +28,10 @@ const mapStateToProps = state => (
|
|||||||
nextPage: state.grades.nextPage,
|
nextPage: state.grades.nextPage,
|
||||||
assignmnetTypes: state.assignmentTypes.results,
|
assignmnetTypes: state.assignmentTypes.results,
|
||||||
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
|
areGradesFrozen: state.assignmentTypes.areGradesFrozen,
|
||||||
showSpinner: shouldShowSpinner(state),
|
showSpinner: state.grades.showSpinner,
|
||||||
canUserViewGradebook: state.roles.canUserViewGradebook
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
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 => (
|
const mapDispatchToProps = dispatch => (
|
||||||
{
|
{
|
||||||
getUserGrades: (courseId, cohort, track) => {
|
getUserGrades: (courseId, cohort, track) => {
|
||||||
@@ -76,9 +64,6 @@ const mapDispatchToProps = dispatch => (
|
|||||||
updateBanner: (showSuccess) => {
|
updateBanner: (showSuccess) => {
|
||||||
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,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,
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -4,6 +4,7 @@ import { configuration } from '../config';
|
|||||||
|
|
||||||
const apiClient = getAuthenticatedAPIClient({
|
const apiClient = getAuthenticatedAPIClient({
|
||||||
appBaseUrl: configuration.BASE_URL,
|
appBaseUrl: configuration.BASE_URL,
|
||||||
|
authBaseUrl: configuration.LMS_BASE_URL,
|
||||||
loginUrl: configuration.LOGIN_URL,
|
loginUrl: configuration.LOGIN_URL,
|
||||||
logoutUrl: configuration.LOGOUT_URL,
|
logoutUrl: configuration.LOGOUT_URL,
|
||||||
csrfTokenApiPath: process.env.CSRF_TOKEN_API_PATH,
|
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 { combineReducers } from 'redux';
|
||||||
|
import { userProfile } from '@edx/frontend-auth';
|
||||||
|
|
||||||
import cohorts from './cohorts';
|
import cohorts from './cohorts';
|
||||||
import grades from './grades';
|
import grades from './grades';
|
||||||
import tracks from './tracks';
|
import tracks from './tracks';
|
||||||
import assignmentTypes from './assignmentTypes';
|
import assignmentTypes from './assignmentTypes';
|
||||||
import roles from './roles';
|
|
||||||
|
const identityReducer = (state) => {
|
||||||
|
const newState = { ...state };
|
||||||
|
return newState;
|
||||||
|
};
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
|
// The authentication state is added as initialState when
|
||||||
|
// creating the store in data/store.js.
|
||||||
|
authentication: identityReducer,
|
||||||
|
userProfile,
|
||||||
grades,
|
grades,
|
||||||
cohorts,
|
cohorts,
|
||||||
tracks,
|
tracks,
|
||||||
assignmentTypes,
|
assignmentTypes,
|
||||||
roles,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
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`;
|
const assignmentTypesUrl = `${LmsApiService.baseUrl}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`;
|
||||||
return apiClient.get(assignmentTypesUrl);
|
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;
|
export default LmsApiService;
|
||||||
|
|||||||
@@ -2,48 +2,17 @@ import { applyMiddleware, createStore } from 'redux';
|
|||||||
import thunkMiddleware from 'redux-thunk';
|
import thunkMiddleware from 'redux-thunk';
|
||||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||||
import { createLogger } from 'redux-logger';
|
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';
|
import reducers from './reducers';
|
||||||
|
|
||||||
const loggerMiddleware = createLogger();
|
const loggerMiddleware = createLogger();
|
||||||
|
const initialState = apiClient.getAuthenticationState();
|
||||||
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 store = createStore(
|
const store = createStore(
|
||||||
reducers,
|
reducers,
|
||||||
composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware, segmentMiddleware)),
|
initialState,
|
||||||
|
composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware)),
|
||||||
);
|
);
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|||||||
@@ -3,49 +3,37 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import SiteFooter from '@edx/frontend-component-footer';
|
import { fetchUserProfile } from '@edx/frontend-auth';
|
||||||
|
|
||||||
import apiClient from './data/apiClient';
|
import apiClient from './data/apiClient';
|
||||||
|
import Footer from './components/Gradebook/footer';
|
||||||
import GradebookPage from './containers/GradebookPage';
|
import GradebookPage from './containers/GradebookPage';
|
||||||
import Header from './components/Header';
|
import Header from './containers/Header';
|
||||||
import store from './data/store';
|
import store from './data/store';
|
||||||
import FooterLogo from '../assets/edx-footer.png';
|
|
||||||
import './App.scss';
|
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 = () => (
|
render() {
|
||||||
<Provider store={store}>
|
return <Provider store={store}>
|
||||||
<Router>
|
<Router>
|
||||||
<div>
|
<div>
|
||||||
<Header />
|
<Header />
|
||||||
<main>
|
<main>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/:courseId" component={GradebookPage} />
|
<Route exact path="/:courseId" component={GradebookPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
<SiteFooter
|
<Footer />
|
||||||
siteName={process.env.SITE_NAME}
|
</div>
|
||||||
siteLogo={FooterLogo}
|
</Router>
|
||||||
marketingSiteBaseUrl={process.env.MARKETING_SITE_BASE_URL}
|
</Provider>;
|
||||||
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (apiClient.ensurePublicOrAuthencationAndCookies(window.location.pathname)) {
|
if (apiClient.ensurePublicOrAuthencationAndCookies(window.location.pathname)) {
|
||||||
ReactDOM.render(<App />, document.getElementById('root'));
|
ReactDOM.render(<App />, document.getElementById('root'));
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}());
|
|
||||||
Reference in New Issue
Block a user