Compare commits

...

10 Commits

Author SHA1 Message Date
Simon Chen
0b9fa36fb7 Merge pull request #28 from edx/schen/styles
fix(Styles): Fix the gradebook styles to increase its width
2018-11-30 10:51:03 -05:00
Simon Chen
7bd0c49c14 fix(Styles): Fix the gradebook styles to increase its width 2018-11-30 10:30:59 -05:00
Alex Dusenbery
44f91bb453 Add documentation on platform configuration for local development. 2018-11-30 10:16:08 -05:00
Jansen Kantor
d8f229838f Merge pull request #25 from edx/pagination
fix(pagination) use paragon buttons, hide rather than disable
2018-11-29 16:22:38 -05:00
jkantor
7b5a095898 fix(pagination) use paragon buttons, hide rather than disable 2018-11-29 15:35:13 -05:00
Jansen Kantor
ff7937c2d7 Merge pull request #24 from edx/pagination
feat(pagination) added next and previous buttons to grades page
2018-11-29 14:34:56 -05:00
jkantor
d057497105 feat(pagination) added next and previous buttons to grades page 2018-11-29 13:11:11 -05:00
Richard I Reilly
e1402b0d4f Merge pull request #23 from edx/rir/update-auth
Update auth to the latest version of edx/frontend-auth
2018-11-28 15:41:24 -05:00
Rick Reilly
3ea337e3f8 Update auth to the latest version of edx/frontend-auth 2018-11-28 15:31:19 -05:00
Alex Dusenbery
9c2c16e378 Remove boilerplate README strings, add note about installing into LMS. 2018-11-28 10:30:12 -05:00
13 changed files with 132 additions and 85 deletions

View File

@@ -29,6 +29,26 @@ If you don't, you can see the log messages for the docker container by executing
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: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:1991'
```
There are also several edx-platform waffle and feature flags you'll have to enable from the Django admin:
1. Grades > Persistent grades enabled flag. Add this flag if it doesn't exist,
check the ``enabled`` and ``enabled for all courses`` boxes.
2. Waffle > Switches. Add the ``grades.assume_zero_grade_if_absent`` switch and make it active.
3. Waffle_utils > Waffle flag course overrides. You want to activate this flag for any course
in which you'd like to enable the gradebook. Add a course override flag using a course id and the flag name
``grades.writable_gradebook``. Make sure to check the ``enabled`` box. Alternatively, you could add this as a
regular waffle flag to enable the gradebook for all courses.
## Directory Structure
* `config`
@@ -46,61 +66,6 @@ Note that `make up-detached` executes the `npm run start` script which will hot-
* `constants`
* `reducers`
* Directory for `Redux` reducers
* [`.babelrc`](#babelrc)
* [`.dockerignore`](#dockerignore)
* [`.eslintignore`](#eslintignore)
* [`.eslintrc.js`](#eslintrcjs)
* `.gitignore`
* [`npmignore`](#npmignore)
* [`.travis.yml`](#travisyml)
* `docker-compose.yml`
* `Dockerfile`
* `LICENSE`
* `Makefile`
* `package-lock.json`
* [`package.json`](#packagejson)
### `.babelrc`
We use [`Babel`](https://babeljs.io/) to transpile `ES2015+` JavaScript to `ES5` JavaScript. `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/).
The `.babelrc` file is used to specify a particular configuration - for example, we use the [`babel-preset-react`](https://babeljs.io/docs/plugins/preset-react/), which, among other things, allows `babel` to parse `JSX`.
### `.dockerignore`
The important thing to remember is to add the `node_modules` directory to `.dockerignore` - for more information [see the Docker documentation](https://docs.docker.com/engine/reference/builder/#dockerignore-file).
### `.eslintignore`
We use [`eslint`](https://eslint.org/) for our `JavaScript` linting needs. The `.eslintignore` file is used to [specify files or directories to, well, ignore](https://eslint.org/docs/user-guide/configuring#ignoring-files-and-directories).
While `eslint` automatically ignores `node_modules`, we like to add it to the `.eslintignore` just for the added explicitness. In addition, you probably want to add the directory for your compiled files (in our case, `./dist`) and your coverage directory (in our case, `./coverage`).
### `.eslintrc`
This is where the actual `eslint` configuration is specified. All `edX` JavaScript projects should extend either the [`eslint-config-edx`](https://github.com/edx/eslint-config-edx/blob/master/packages/eslint-config-edx/README.md) or [`eslint-config-edx-es5`](https://github.com/edx/eslint-config-edx/blob/master/packages/eslint-config-edx-es5/README.md) configurations (for `ES2015+` and `ES5` JavaScript, respectively). Both configurations can be found in [the `eslint-config-edx` repository](https://github.com/edx/eslint-config-edx).
### `.npmignore`
We are not currently publishing this package to [`npm`](https://www.npmjs.com/). If we did, we would want to exclude certain files from getting uploaded to `npm` (like our coverage files, for example). For more information, see [the `npm` documentation](https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package).
### `.travis.yml`
We use [`Travis CI`](https://travis-ci.org/) to build (and deploy) our application. The `.travis.yml` file specifies the configuration for `Travis` builds. For more information, see [the `Travis` documentation](https://docs.travis-ci.com/user/customizing-the-build/).
### `package.json`
Arguably, one of the **most important files in an `npm`-based application**, the `package.json` file specifies everything from the `name` of the application, were it to be published to `npm`, to it's `dependencies`.
For more information, see [the `npm` documentation](https://docs.npmjs.com/files/package.json).
## Helpful Applications
### [`Greenkeeper`](https://greenkeeper.io/)
[`Greenkeeper`](https://greenkeeper.io/) is basically a `GitHub` application that handles `npm` dependencies. It will automatically open PRs with `package.json` updates when new versions of your `npm` dependencies get published. There are ways to also automatically keep the `package-lock.json` in-line, in the same PR, using [`greenkeeper-lockfile`].
For more information, see [the `Greenkeeper` documentation](https://greenkeeper.io/docs.html#what-greenkeeper-does).
## Authentication with backend API services

View File

@@ -79,6 +79,7 @@ module.exports = Merge.smart(commonConfig, {
LMS_BASE_URL: 'http://localhost:18000',
LOGIN_URL: 'http://localhost:18000/login',
LOGOUT_URL: 'http://localhost:18000/login',
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login',
DATA_API_BASE_URL: 'http://localhost:8000',
// LMS_CLIENT_ID should match the lms DOT client application id your LMS container

View File

@@ -98,6 +98,7 @@ module.exports = Merge.smart(commonConfig, {
LMS_BASE_URL: null,
LOGIN_URL: null,
LOGOUT_URL: null,
CSRF_TOKEN_API_PATH: null,
REFRESH_ACCESS_TOKEN_ENDPOINT: null,
DATA_API_BASE_URL: null,
SEGMENT_KEY: null,

55
package-lock.json generated
View File

@@ -2965,18 +2965,20 @@
}
},
"@edx/frontend-auth": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-1.1.0.tgz",
"integrity": "sha512-Pl6CUgwRPHcKx3REX1CXJIGl/WqSCg20IPGNUbpsyQ4ZuIaOi/bxI6dYfGhmOWt8LPLeBiEmGerofEWLNydZ9w==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-1.2.1.tgz",
"integrity": "sha512-NEc+rAJq5HJ9UACPirezpwTp5yhX9G2AQDPfk1hW4ceEF7BFRMIfzb+nJqmTJkEX/37yRVYUfQFLW+z2j1ZRcw==",
"requires": {
"axios": "^0.18.0",
"jwt-decode": "^2.2.0",
"prop-types": "^15.5.10",
"pubsub-js": "^1.7.0",
"react": "^16.4.2",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"redux": "^4.0.0",
"universal-cookie": "^3.0.4"
"universal-cookie": "^3.0.4",
"url-parse": "^1.4.3"
},
"dependencies": {
"invariant": {
@@ -2987,6 +2989,11 @@
"loose-envify": "^1.0.0"
}
},
"querystringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.0.tgz",
"integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg=="
},
"react": {
"version": "16.6.3",
"resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz",
@@ -3055,13 +3062,13 @@
}
}
},
"scheduler": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz",
"integrity": "sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg==",
"url-parse": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.4.tgz",
"integrity": "sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
"querystringify": "^2.0.0",
"requires-port": "^1.0.0"
}
},
"warning": {
@@ -3533,9 +3540,9 @@
}
},
"@types/cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha512-64Uv+8bTRVZHlbB8eXQgMP9HguxPgnOOIYrQpwHWrtLDrtcG/lILKhUl7bV65NSOIJ9dXGYD7skQFXzhL8tk1A=="
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.2.tgz",
"integrity": "sha512-aHQA072E10/8iUQsPH7mQU/KUyQBZAGzTVRCUvnSz8mSvbrYsP4xEO2RSA0Pjltolzi0j8+8ixrm//Hr4umPzw=="
},
"@types/node": {
"version": "9.4.7",
@@ -3545,7 +3552,7 @@
},
"@types/object-assign": {
"version": "4.0.30",
"resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"resolved": "http://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
"integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI="
},
"@types/tapable": {
@@ -9162,9 +9169,9 @@
}
},
"follow-redirects": {
"version": "1.5.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.9.tgz",
"integrity": "sha512-Bh65EZI/RU8nx0wbYF9shkFZlqLP+6WT/5FnA3cE/djNSuKNHJEinGGZgu/cQEkeeb2GdFOgenAmn8qaqYke2w==",
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
},
@@ -19165,6 +19172,11 @@
"randombytes": "^2.0.1"
}
},
"pubsub-js": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.7.0.tgz",
"integrity": "sha512-Pb68P9qFZxnvDipHMuj9oT1FoIgBcXJ9C9eWdHCLZAnulaUoJ3+Y87RhGMYilWpun6DMWVmvK70T4RP4drZMSA=="
},
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
@@ -20102,8 +20114,7 @@
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"resolve": {
"version": "1.5.0",
@@ -22987,9 +22998,9 @@
}
},
"universal-cookie": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-3.0.4.tgz",
"integrity": "sha512-3rhx6RAIuRmCWJttnbgzMrp2TbHhUmgQ2GrpY/US03Siv5T28iXr2qYw1m3YqmluBxEyrvZaloVemkLSId+Oyg==",
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-3.0.6.tgz",
"integrity": "sha512-VxVnwj1bWVLuYKAbaeQ6PL4NlIEWB6r4PUjwKp76nFnrLyqQtnOKAHe9dOjESpcJ4gPoc/Zkxb/6ZK+FMuEioA==",
"requires": {
"@types/cookie": "^0.3.1",
"@types/object-assign": "^4.0.30",

View File

@@ -25,7 +25,7 @@
},
"dependencies": {
"@edx/edx-bootstrap": "^0.4.3",
"@edx/frontend-auth": "1.1.0",
"@edx/frontend-auth": "^1.2.1",
"@edx/paragon": "^3.7.2",
"babel-polyfill": "^6.26.0",
"classnames": "^2.2.5",

View File

@@ -1,3 +1,15 @@
.gradebook-container{
width: 500px;
@media only screen and (min-width: 640px) {
width: 630px;
}
@media only screen and (min-width: 992px) {
width: 900px;
}
@media only screen and (min-width: 1200px) {
width: 1024px;
}
}
.back-link{
float:right;
}
@@ -32,10 +44,6 @@
display: block;
background-color: #fff;
}
.table tr td:not(:first-child) {
//not real sylz. plz kill before prod
min-width: 250px;
}
.table tr td:nth-child(2) {
box-sizing: content-box;
padding-left: 170px;

View File

@@ -182,7 +182,7 @@ export default class Gradebook extends React.Component {
render() {
return (
<div className="d-flex justify-content-center">
<div className="card" style={{ width: '50rem' }}>
<div className="card gradebook-container">
<div className="card-body">
<a
href={this.lmsInstructorDashboardUrl(this.props.match.params.courseId)}
@@ -294,6 +294,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 />

View File

@@ -3,6 +3,7 @@ const configuration = {
LMS_BASE_URL: process.env.LMS_BASE_URL,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
SECURE_COOKIES: process.env.NODE_ENV !== 'development',

View File

@@ -4,6 +4,7 @@ import Gradebook from '../../components/Gradebook';
import {
fetchGrades,
fetchMatchingUserGrades,
fetchPrevNextGrades,
updateGrades,
toggleGradeFormat,
filterColumns,
@@ -22,6 +23,8 @@ const mapStateToProps = state => (
selectedCohort: state.grades.selectedCohort,
format: state.grades.gradeFormat,
showSuccess: state.grades.showSuccess,
prevPage: state.grades.prevPage,
nextPage: state.grades.nextPage,
}
);
@@ -33,6 +36,9 @@ const mapDispatchToProps = dispatch => (
searchForUser: (courseId, searchText, cohort, track) => {
dispatch(fetchMatchingUserGrades(courseId, searchText, cohort, track));
},
getPrevNextGrades : (endpoint, cohort, track) => {
dispatch(fetchPrevNextGrades(endpoint, cohort, track));
},
getCohorts: (courseId) => {
dispatch(fetchCohorts(courseId));
},

View File

@@ -14,6 +14,7 @@ import {
import LmsApiService from '../services/LmsApiService';
import store from '../store';
import { headingMapper, gradeSortMap, sortAlphaAsc } from './utils';
import apiClient from "../apiClient";
const sortGrades = (columnName, direction) => {
@@ -30,12 +31,14 @@ const sortGrades = (columnName, direction) => {
const startedFetchingGrades = () => ({ type: STARTED_FETCHING_GRADES });
const finishedFetchingGrades = () => ({ type: FINISHED_FETCHING_GRADES });
const errorFetchingGrades = () => ({ type: ERROR_FETCHING_GRADES });
const gotGrades = (grades, cohort, track, headings) => ({
const gotGrades = (grades, cohort, track, headings, prev, next) => ({
type: GOT_GRADES,
grades,
cohort,
track,
headings,
prev,
next,
});
const gradeUpdateRequest = () => ({ type: GRADE_UPDATE_REQUEST });
@@ -70,6 +73,8 @@ const fetchGrades = (courseId, cohort, track, showSuccess) => (
cohort,
track,
headingMapper.all(dispatch, data.results[0]),
data.previous,
data.next,
));
dispatch(finishedFetchingGrades());
dispatch(updateBanner(!!showSuccess));
@@ -91,6 +96,8 @@ const fetchMatchingUserGrades = (courseId, searchText, cohort, track) => (
cohort,
track,
headingMapper.all(dispatch, data.results[0]),
data.previous,
data.next,
));
dispatch(finishedFetchingGrades());
})
@@ -100,6 +107,29 @@ const fetchMatchingUserGrades = (courseId, searchText, cohort, track) => (
}
);
const fetchPrevNextGrades = (endpoint, cohort, track) => (
(dispatch) => {
dispatch(startedFetchingGrades());
return apiClient.get(endpoint)
.then(response => response.data)
.then((data) => {
dispatch(gotGrades(
data.results.sort(sortAlphaAsc),
cohort,
track,
headingMapper.all(dispatch, data.results[0]),
data.previous,
data.next,
));
dispatch(finishedFetchingGrades());
})
.catch(() => {
dispatch(errorFetchingGrades());
});
}
);
const updateGrades = (courseId, updateData) => (
(dispatch) => {
dispatch(gradeUpdateRequest());
@@ -122,6 +152,7 @@ export {
gotGrades,
fetchGrades,
fetchMatchingUserGrades,
fetchPrevNextGrades,
gradeUpdateRequest,
gradeUpdateSuccess,
gradeUpdateFailure,

View File

@@ -6,6 +6,7 @@ const apiClient = getAuthenticatedAPIClient({
appBaseUrl: configuration.BASE_URL,
loginUrl: configuration.LOGIN_URL,
logoutUrl: configuration.LOGOUT_URL,
csrfTokenApiPath: process.env.CSRF_TOKEN_API_PATH,
refreshAccessTokenEndpoint: configuration.REFRESH_ACCESS_TOKEN_ENDPOINT,
accessTokenCookieName: configuration.ACCESS_TOKEN_COOKIE_NAME,
csrfCookieName: configuration.CSRF_COOKIE_NAME,

View File

@@ -17,6 +17,8 @@ const initialState = {
errorFetching: false,
gradeFormat: 'percent',
showSuccess: false,
prevPage: null,
nextPage: null,
};
const grades = (state = initialState, action) => {
@@ -30,6 +32,8 @@ const grades = (state = initialState, action) => {
errorFetching: false,
selectedTrack: action.track,
selectedCohort: action.cohort,
prevPage: action.prev,
nextPage: action.next,
};
case STARTED_FETCHING_GRADES:
return {

View File

@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Provider } from 'react-redux';
import apiClient from './data/apiClient';
import GradebookPage from './containers/GradebookPage';
import store from './data/store';
import './App.scss';
@@ -20,4 +21,6 @@ const App = () => (
</Provider>
);
ReactDOM.render(<App />, document.getElementById('root'));
if (apiClient.ensurePublicOrAuthencationAndCookies(window.location.pathname)) {
ReactDOM.render(<App />, document.getElementById('root'));
}