Compare commits
6 Commits
bw/compone
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
395a6d0631 | ||
|
|
85709d9c71 | ||
|
|
dc36d138c1 | ||
|
|
ff4d0c75dd | ||
|
|
c4846f9ebd | ||
|
|
bccd87fd49 |
2
.env
2
.env
@@ -30,3 +30,5 @@ ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -37,3 +37,5 @@ ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
60
package-lock.json
generated
60
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "^11.1.1",
|
||||
"@edx/frontend-component-header": "^3.1.1",
|
||||
"@edx/frontend-platform": "2.3.0",
|
||||
"@edx/frontend-platform": "2.5.0",
|
||||
"@edx/paragon": "19.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
@@ -31,6 +31,7 @@
|
||||
"query-string": "6.13.0",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^2.9.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "5.2.0",
|
||||
@@ -3986,9 +3987,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.3.0.tgz",
|
||||
"integrity": "sha512-vZAw3eKJgUvD3wu8QOlCbNvuhe9YOGhdVuiTiFGMJKsYagJNMuQZxTJ2DwPCr7/gprJ65mboisJ3BF5IoFzVJA==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.5.0.tgz",
|
||||
"integrity": "sha512-Ws40TMkxrF9Fz71K8bqp+qui7kXYOBvl8+PYLa1K0lmzwD70FFU73mQBTvgTJKKWcw8VsjK9oJCxmjGvz6Qe1Q==",
|
||||
"dependencies": {
|
||||
"@cospired/i18n-iso-languages": "2.2.0",
|
||||
"@formatjs/intl-pluralrules": "^4.3.3",
|
||||
@@ -4014,7 +4015,7 @@
|
||||
"transifex-utils.js": "i18n/scripts/transifex-utils.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/paragon": ">= 10.0.0 < 20.0.0",
|
||||
"@edx/paragon": ">= 10.0.0 < 21.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
@@ -28541,6 +28542,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-helmet": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
|
||||
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
|
||||
"dependencies": {
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.1.1",
|
||||
"react-side-effect": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-intl": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz",
|
||||
@@ -28803,6 +28818,14 @@
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-side-effect": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
|
||||
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",
|
||||
@@ -37680,9 +37703,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-platform": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.3.0.tgz",
|
||||
"integrity": "sha512-vZAw3eKJgUvD3wu8QOlCbNvuhe9YOGhdVuiTiFGMJKsYagJNMuQZxTJ2DwPCr7/gprJ65mboisJ3BF5IoFzVJA==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-2.5.0.tgz",
|
||||
"integrity": "sha512-Ws40TMkxrF9Fz71K8bqp+qui7kXYOBvl8+PYLa1K0lmzwD70FFU73mQBTvgTJKKWcw8VsjK9oJCxmjGvz6Qe1Q==",
|
||||
"requires": {
|
||||
"@cospired/i18n-iso-languages": "2.2.0",
|
||||
"@formatjs/intl-pluralrules": "^4.3.3",
|
||||
@@ -56733,6 +56756,17 @@
|
||||
"use-sidecar": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"react-helmet": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
|
||||
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
|
||||
"requires": {
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-fast-compare": "^3.1.1",
|
||||
"react-side-effect": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"react-intl": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.9.0.tgz",
|
||||
@@ -56936,6 +56970,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-side-effect": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
|
||||
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-style-singleton": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -33,7 +33,7 @@
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "^11.1.1",
|
||||
"@edx/frontend-component-header": "^3.1.1",
|
||||
"@edx/frontend-platform": "2.3.0",
|
||||
"@edx/frontend-platform": "2.5.0",
|
||||
"@edx/paragon": "19.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
@@ -52,6 +52,7 @@
|
||||
"query-string": "6.13.0",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^2.9.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "5.2.0",
|
||||
|
||||
@@ -10,9 +10,11 @@ import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
import './App.scss';
|
||||
import Head from './head/Head';
|
||||
|
||||
const App = () => (
|
||||
<AppProvider store={store}>
|
||||
<Head />
|
||||
<Router>
|
||||
<div>
|
||||
<Header />
|
||||
|
||||
@@ -12,6 +12,7 @@ import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
|
||||
import App from './App';
|
||||
import Head from './head/Head';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
BrowserRouter: () => 'BrowserRouter',
|
||||
@@ -41,7 +42,7 @@ describe('App router component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.childAt(0);
|
||||
router = el.childAt(1);
|
||||
});
|
||||
describe('AppProvider', () => {
|
||||
test('AppProvider is the parent component, passed the redux store props', () => {
|
||||
@@ -49,8 +50,13 @@ describe('App router component', () => {
|
||||
expect(el.props().store).toEqual(store);
|
||||
});
|
||||
});
|
||||
describe('Router', () => {
|
||||
describe('Head', () => {
|
||||
test('first child of AppProvider', () => {
|
||||
expect(el.childAt(0).type()).toBe(Head);
|
||||
});
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('second child of AppProvider', () => {
|
||||
expect(router.type()).toBe(Router);
|
||||
});
|
||||
test('Header is above/outside-of the routing', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`App router component snapshot 1`] = `
|
||||
<AppProvider
|
||||
store="testStore"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Header />
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { configuration } from 'config';
|
||||
import { views } from 'data/constants/app';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
@@ -25,7 +25,7 @@ export class GradebookHeader extends React.Component {
|
||||
}
|
||||
|
||||
lmsInstructorDashboardUrl = courseId => (
|
||||
`${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`
|
||||
`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
|
||||
);
|
||||
|
||||
handleToggleViewClick() {
|
||||
|
||||
@@ -21,15 +21,28 @@ export class AdjustedGradeInput extends React.Component {
|
||||
}
|
||||
|
||||
onChange = ({ target }) => {
|
||||
this.props.setModalState({ adjustedGradeValue: target.value });
|
||||
let adjustedGradeValue;
|
||||
switch (true) {
|
||||
case target.value < 0:
|
||||
adjustedGradeValue = 0;
|
||||
break;
|
||||
case this.props.possibleGrade && target.value > this.props.possibleGrade:
|
||||
adjustedGradeValue = this.props.possibleGrade;
|
||||
break;
|
||||
default:
|
||||
adjustedGradeValue = target.value;
|
||||
}
|
||||
this.props.setModalState({ adjustedGradeValue });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
<Form.Control
|
||||
type="text"
|
||||
type="number"
|
||||
name="adjustedGradeValue"
|
||||
min="0"
|
||||
max={this.props.possibleGrade ? this.props.possibleGrade : ''}
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
|
||||
@@ -54,9 +54,34 @@ describe('AdjustedGradeInput', () => {
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('onChange', () => {
|
||||
it('calls props.setModalState event target value', () => {
|
||||
it('calls props.setModalState event target value with correct value', () => {
|
||||
const value = 3;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: value,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls props.setModalState event target value with a value more then the possibleGrade value', () => {
|
||||
const value = 42;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: props.possibleGrade,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls props.setModalState event target value with less then 0', () => {
|
||||
const value = -5;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls props.setModalState event target value without possibleGrade value', () => {
|
||||
const value = 100;
|
||||
const newEl = shallow(<AdjustedGradeInput {...props} possibleGrade={null} />);
|
||||
newEl.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: value,
|
||||
});
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
exports[`AdjustedGradeInput Component snapshots displays input control and "out of possible grade" label 1`] = `
|
||||
<span>
|
||||
<Control
|
||||
max={5}
|
||||
min="0"
|
||||
name="adjustedGradeValue"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
type="text"
|
||||
type="number"
|
||||
value={1}
|
||||
/>
|
||||
/ 5
|
||||
|
||||
@@ -59,6 +59,6 @@ exports[`OverrideTable Component snapshots basic snapshot shows a row for each e
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
itemCount={3}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -26,6 +26,16 @@ export const OverrideTable = ({
|
||||
if (hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tableData = [
|
||||
...gradeOverrides,
|
||||
{
|
||||
adjustedGrade: <AdjustedGradeInput />,
|
||||
date: todaysDate,
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={[
|
||||
@@ -37,15 +47,8 @@ export const OverrideTable = ({
|
||||
accessor: columns.adjustedGrade,
|
||||
},
|
||||
]}
|
||||
data={[
|
||||
...gradeOverrides,
|
||||
{
|
||||
adjustedGrade: <AdjustedGradeInput />,
|
||||
date: todaysDate,
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
]}
|
||||
itemCount={gradeOverrides.length}
|
||||
data={tableData}
|
||||
itemCount={tableData.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
const configuration = {
|
||||
BASE_URL: process.env.BASE_URL,
|
||||
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',
|
||||
SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
export { configuration, features };
|
||||
@@ -91,17 +91,10 @@ export const formattedGradeLimits = (state) => {
|
||||
const { assignmentGradeMax, assignmentGradeMin } = app.assignmentGradeLimits(state);
|
||||
const { courseGradeMax, courseGradeMin } = app.courseGradeLimits(state);
|
||||
const hasAssignment = filters.selectedAssignmentId(state) !== undefined;
|
||||
if (!hasAssignment) {
|
||||
return {
|
||||
assignmentGradeMax: null,
|
||||
assignmentGradeMin: null,
|
||||
courseGradeMax: null,
|
||||
courseGradeMin: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
assignmentGradeMax: assignmentGradeMax === maxGrade ? null : assignmentGradeMax,
|
||||
assignmentGradeMin: assignmentGradeMin === minGrade ? null : assignmentGradeMin,
|
||||
assignmentGradeMax: (assignmentGradeMax === maxGrade || !hasAssignment) ? null : assignmentGradeMax,
|
||||
assignmentGradeMin: (assignmentGradeMin === minGrade || !hasAssignment) ? null : assignmentGradeMin,
|
||||
courseGradeMax: courseGradeMax === maxGrade ? null : courseGradeMax,
|
||||
courseGradeMin: courseGradeMin === minGrade ? null : courseGradeMin,
|
||||
};
|
||||
|
||||
@@ -260,15 +260,15 @@ describe('root selectors', () => {
|
||||
};
|
||||
const grade1 = '42';
|
||||
const grade2 = '3.14';
|
||||
it('returns an object of nulls if assignment is not set', () => {
|
||||
it('returns an object of nullable assignmentGrades if assignment is not set', () => {
|
||||
mockId(undefined);
|
||||
mockAssgn(grade1, grade2);
|
||||
mockCourse(grade1, grade2);
|
||||
expect(selector(testState)).toEqual({
|
||||
assignmentGradeMax: null,
|
||||
assignmentGradeMin: null,
|
||||
courseGradeMax: null,
|
||||
courseGradeMin: null,
|
||||
courseGradeMax: '42',
|
||||
courseGradeMin: '3.14',
|
||||
});
|
||||
});
|
||||
it('returns null for each extreme iff they are equal their default', () => {
|
||||
|
||||
@@ -14,10 +14,10 @@ const { get, post, stringifyUrl } = utils;
|
||||
/*********************************************************************************
|
||||
* GET Actions
|
||||
*********************************************************************************/
|
||||
const assignmentTypes = () => get(urls.assignmentTypes);
|
||||
const cohorts = () => get(urls.cohorts);
|
||||
const roles = () => get(urls.roles);
|
||||
const tracks = () => get(urls.tracks);
|
||||
const assignmentTypes = () => get(urls.getAssignmentTypesUrl());
|
||||
const cohorts = () => get(urls.getCohortsUrl());
|
||||
const roles = () => get(urls.getRolesUrl());
|
||||
const tracks = () => get(urls.getTracksUrl());
|
||||
|
||||
/**
|
||||
* fetch.gradebookData(searchText, cohort, track, options)
|
||||
@@ -45,7 +45,7 @@ const gradebookData = (searchText, cohort, track, options = {}) => {
|
||||
[paramKeys.assignmentGradeMax]: options.assignmentGradeMax,
|
||||
[paramKeys.assignmentGradeMin]: options.assignmentGradeMin,
|
||||
};
|
||||
return get(stringifyUrl(urls.gradebook, queryParams));
|
||||
return get(stringifyUrl(urls.getGradebookUrl(), queryParams));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ const gradebookData = (searchText, cohort, track, options = {}) => {
|
||||
* fetches bulk operation history and raises an error if the operation fails
|
||||
* @return {Promise} - get response
|
||||
*/
|
||||
const gradeBulkOperationHistory = () => get(urls.bulkHistory)
|
||||
const gradeBulkOperationHistory = () => get(urls.getBulkHistoryUrl())
|
||||
.then(response => response.data)
|
||||
.catch(() => Promise.reject(Error(messages.errors.unhandledResponse)));
|
||||
|
||||
@@ -87,7 +87,7 @@ const gradeOverrideHistory = (subsectionId, userId) => (
|
||||
* }
|
||||
* @return {Promise} - post response
|
||||
*/
|
||||
const updateGradebookData = (updateData) => post(urls.bulkUpdate, updateData);
|
||||
const updateGradebookData = (updateData) => post(urls.getBulkUpdateUrl(), updateData);
|
||||
|
||||
/**
|
||||
* uploadGradeCsv(formData)
|
||||
|
||||
@@ -35,28 +35,28 @@ describe('lms service api', () => {
|
||||
describe('fetch.assignmentTypes', () => {
|
||||
testSimpleFetch(
|
||||
api.fetch.assignmentTypes,
|
||||
urls.assignmentTypes,
|
||||
urls.getAssignmentTypesUrl(),
|
||||
'fetches from urls.assignmentTypes',
|
||||
);
|
||||
});
|
||||
describe('fetch.cohorts', () => {
|
||||
testSimpleFetch(
|
||||
api.fetch.cohorts,
|
||||
urls.cohorts,
|
||||
urls.getCohortsUrl(),
|
||||
'fetches from urls.cohorts',
|
||||
);
|
||||
});
|
||||
describe('fetch.roles', () => {
|
||||
testSimpleFetch(
|
||||
api.fetch.roles,
|
||||
urls.roles,
|
||||
urls.getRolesUrl(),
|
||||
'fetches from urls.roles',
|
||||
);
|
||||
});
|
||||
describe('fetch.tracks', () => {
|
||||
testSimpleFetch(
|
||||
api.fetch.tracks,
|
||||
urls.tracks,
|
||||
urls.getTracksUrl(),
|
||||
'fetches from urls.tracks',
|
||||
);
|
||||
});
|
||||
@@ -98,7 +98,7 @@ describe('lms service api', () => {
|
||||
});
|
||||
test('loads only passed values if options is empty', () => (
|
||||
api.fetch.gradebookData(searchText, cohort, track).then(({ data }) => {
|
||||
expect(data).toEqual(utils.stringifyUrl(urls.gradebook, {
|
||||
expect(data).toEqual(utils.stringifyUrl(urls.getGradebookUrl(), {
|
||||
[paramKeys.pageSize]: pageSize,
|
||||
[paramKeys.userContains]: searchText,
|
||||
[paramKeys.cohortId]: cohort,
|
||||
@@ -114,7 +114,7 @@ describe('lms service api', () => {
|
||||
));
|
||||
test('loads ["all"] for excludedCorseRoles if not includeCourseRoles', () => (
|
||||
api.fetch.gradebookData(searchText, cohort, track, options).then(({ data }) => {
|
||||
expect(data).toEqual(utils.stringifyUrl(urls.gradebook, {
|
||||
expect(data).toEqual(utils.stringifyUrl(urls.getGradebookUrl(), {
|
||||
[paramKeys.pageSize]: pageSize,
|
||||
[paramKeys.userContains]: searchText,
|
||||
[paramKeys.cohortId]: cohort,
|
||||
@@ -130,7 +130,7 @@ describe('lms service api', () => {
|
||||
));
|
||||
test('loads null for excludedCorseRoles if includeCourseRoles', () => (
|
||||
api.fetch.gradebookData(searchText, cohort, track, options).then(({ data }) => {
|
||||
expect(data).toEqual(utils.stringifyUrl(urls.gradebook, {
|
||||
expect(data).toEqual(utils.stringifyUrl(urls.getGradebookUrl(), {
|
||||
[paramKeys.pageSize]: pageSize,
|
||||
[paramKeys.userContains]: searchText,
|
||||
[paramKeys.cohortId]: cohort,
|
||||
@@ -153,7 +153,7 @@ describe('lms service api', () => {
|
||||
});
|
||||
it('fetches from urls.bulkHistory and returns the data', () => (
|
||||
api.fetch.gradeBulkOperationHistory().then(url => {
|
||||
expect(url).toEqual(urls.bulkHistory);
|
||||
expect(url).toEqual(urls.getBulkHistoryUrl());
|
||||
})
|
||||
));
|
||||
});
|
||||
@@ -195,7 +195,7 @@ describe('lms service api', () => {
|
||||
});
|
||||
test('posts to urls.bulkUpdate with passed data', () => (
|
||||
api.updateGradebookData(updateData).then(({ data }) => {
|
||||
expect(data).toEqual({ url: urls.bulkUpdate, data: updateData });
|
||||
expect(data).toEqual({ url: urls.getBulkUpdateUrl(), data: updateData });
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
@@ -1,59 +1,54 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { StrictDict } from 'utils';
|
||||
import { configuration } from 'config';
|
||||
import { historyRecordLimit } from './constants';
|
||||
import { filterQuery, stringifyUrl } from './utils';
|
||||
|
||||
const baseUrl = `${configuration.LMS_BASE_URL}`;
|
||||
|
||||
const courseId = window.location.pathname.split('/').filter(Boolean).pop() || '';
|
||||
|
||||
const api = `${baseUrl}/api/`;
|
||||
const bulkGrades = `${api}bulk_grades/course/${courseId}/`;
|
||||
const enrollment = `${api}enrollment/v1/`;
|
||||
const grades = `${api}grades/v1/`;
|
||||
const gradebook = `${grades}gradebook/${courseId}/`;
|
||||
const bulkUpdate = `${gradebook}bulk-update`;
|
||||
const intervention = `${bulkGrades}intervention/`;
|
||||
|
||||
const cohorts = `${baseUrl}/courses/${courseId}/cohorts/`;
|
||||
const tracks = `${enrollment}course/${courseId}?include_expired=1`;
|
||||
const bulkHistory = `${bulkGrades}history/`;
|
||||
|
||||
const assignmentTypes = stringifyUrl(`${gradebook}grading-info`, { graded_only: true });
|
||||
const roles = stringifyUrl(`${enrollment}roles/`, { courseId });
|
||||
|
||||
export const getUrlPrefix = () => `${getConfig().LMS_BASE_URL}/api/`;
|
||||
export const getBulkGradesUrl = () => `${getUrlPrefix()}bulk_grades/course/${courseId}/`;
|
||||
export const getEnrollmentUrl = () => `${getUrlPrefix()}enrollment/v1/`;
|
||||
export const getGradesUrl = () => `${getUrlPrefix()}grades/v1/`;
|
||||
export const getGradebookUrl = () => `${getGradesUrl()}gradebook/${courseId}/`;
|
||||
export const getBulkUpdateUrl = () => `${getGradebookUrl()}bulk-update`;
|
||||
export const getInterventionUrl = () => `${getBulkGradesUrl()}intervention/`;
|
||||
export const getCohortsUrl = () => `${getUrlPrefix()}courses/${courseId}/cohorts/`;
|
||||
export const getTracksUrl = () => `${getEnrollmentUrl()}course/${courseId}?include_expired=1`;
|
||||
export const getBulkHistoryUrl = () => `${getBulkUpdateUrl()}history/`;
|
||||
export const getAssignmentTypesUrl = () => stringifyUrl(`${getGradebookUrl()}grading-info`, { graded_only: true });
|
||||
export const getRolesUrl = () => stringifyUrl(`${getEnrollmentUrl()}roles/`, { courseId });
|
||||
/**
|
||||
* bulkGradesUrlByCourseAndRow(courseId, rowId)
|
||||
* returns the bulkGrades url with the given rowId.
|
||||
* @param {string} rowId - row/error identifier
|
||||
* @return {string} - bulk grades fetch url
|
||||
*/
|
||||
export const bulkGradesUrlByRow = (rowId) => stringifyUrl(bulkGrades, { error_id: rowId });
|
||||
export const bulkGradesUrlByRow = (rowId) => stringifyUrl(getBulkGradesUrl(), { error_id: rowId });
|
||||
|
||||
export const gradeCsvUrl = (options = {}) => stringifyUrl(bulkGrades, filterQuery(options));
|
||||
export const gradeCsvUrl = (options = {}) => stringifyUrl(getBulkGradesUrl(), filterQuery(options));
|
||||
|
||||
export const interventionExportCsvUrl = (options = {}) => (
|
||||
stringifyUrl(intervention, filterQuery(options))
|
||||
stringifyUrl(getInterventionUrl(), filterQuery(options))
|
||||
);
|
||||
|
||||
export const sectionOverrideHistoryUrl = (subsectionId, userId) => stringifyUrl(
|
||||
`${grades}subsection/${subsectionId}/`,
|
||||
`${getGradesUrl()}subsection/${subsectionId}/`,
|
||||
{ user_id: userId, history_record_limit: historyRecordLimit },
|
||||
);
|
||||
|
||||
export default StrictDict({
|
||||
assignmentTypes,
|
||||
bulkGrades,
|
||||
bulkHistory,
|
||||
bulkUpdate,
|
||||
cohorts,
|
||||
enrollment,
|
||||
grades,
|
||||
gradebook,
|
||||
intervention,
|
||||
roles,
|
||||
tracks,
|
||||
|
||||
getUrlPrefix,
|
||||
getBulkGradesUrl,
|
||||
getEnrollmentUrl,
|
||||
getGradesUrl,
|
||||
getGradebookUrl,
|
||||
getBulkUpdateUrl,
|
||||
getInterventionUrl,
|
||||
getCohortsUrl,
|
||||
getTracksUrl,
|
||||
getBulkHistoryUrl,
|
||||
getAssignmentTypesUrl,
|
||||
getRolesUrl,
|
||||
bulkGradesUrlByRow,
|
||||
gradeCsvUrl,
|
||||
interventionExportCsvUrl,
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('lms api url methods', () => {
|
||||
it('returns bulkGrades url with error_id', () => {
|
||||
const id = 'heyo';
|
||||
expect(bulkGradesUrlByRow(id)).toEqual(
|
||||
utils.stringifyUrl(urls.bulkGrades, { error_id: id }),
|
||||
utils.stringifyUrl(urls.getBulkGradesUrl(), { error_id: id }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -25,12 +25,12 @@ describe('lms api url methods', () => {
|
||||
it('returns bulkGrades with filterQuery-loaded options as query', () => {
|
||||
const options = { some: 'fun', query: 'options' };
|
||||
expect(gradeCsvUrl(options)).toEqual(
|
||||
utils.stringifyUrl(urls.bulkGrades, utils.filterQuery(options)),
|
||||
utils.stringifyUrl(urls.getBulkGradesUrl(), utils.filterQuery(options)),
|
||||
);
|
||||
});
|
||||
it('defaults options to empty object', () => {
|
||||
expect(gradeCsvUrl()).toEqual(
|
||||
utils.stringifyUrl(urls.bulkGrades, utils.filterQuery({})),
|
||||
utils.stringifyUrl(urls.getBulkGradesUrl(), utils.filterQuery({})),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -38,12 +38,12 @@ describe('lms api url methods', () => {
|
||||
it('returns intervention url with filterQuery-loaded options as query', () => {
|
||||
const options = { some: 'fun', query: 'options' };
|
||||
expect(interventionExportCsvUrl(options)).toEqual(
|
||||
utils.stringifyUrl(urls.intervention, utils.filterQuery(options)),
|
||||
utils.stringifyUrl(urls.getInterventionUrl(), utils.filterQuery(options)),
|
||||
);
|
||||
});
|
||||
it('defaults options to empty object', () => {
|
||||
expect(interventionExportCsvUrl()).toEqual(
|
||||
utils.stringifyUrl(urls.intervention, utils.filterQuery({})),
|
||||
utils.stringifyUrl(urls.getInterventionUrl(), utils.filterQuery({})),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -53,7 +53,7 @@ describe('lms api url methods', () => {
|
||||
const userId = 'Tom';
|
||||
expect(sectionOverrideHistoryUrl(subsectionId, userId)).toEqual(
|
||||
utils.stringifyUrl(
|
||||
`${urls.grades}subsection/${subsectionId}/`,
|
||||
`${urls.getGradesUrl()}subsection/${subsectionId}/`,
|
||||
{ user_id: userId, history_record_limit: historyRecordLimit },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,19 +4,19 @@ import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProductio
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { createMiddleware } from 'redux-beacon';
|
||||
import Segment from '@redux-beacon/segment';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import actions from './actions';
|
||||
import selectors from './selectors';
|
||||
import reducers from './reducers';
|
||||
import eventsMap from './services/segment/mapping';
|
||||
import { configuration } from '../config';
|
||||
|
||||
export const createStore = () => {
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
const middleware = [thunkMiddleware, loggerMiddleware];
|
||||
// Conditionally add the segmentMiddleware only if the SEGMENT_KEY environment variable exists.
|
||||
if (configuration.SEGMENT_KEY) {
|
||||
if (getConfig().SEGMENT_KEY) {
|
||||
middleware.push(createMiddleware(eventsMap, Segment()));
|
||||
}
|
||||
const store = redux.createStore(
|
||||
|
||||
@@ -4,12 +4,12 @@ import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProductio
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { createMiddleware } from 'redux-beacon';
|
||||
import Segment from '@redux-beacon/segment';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import actions from './actions';
|
||||
import selectors from './selectors';
|
||||
import reducers from './reducers';
|
||||
import eventsMap from './services/segment/mapping';
|
||||
import { configuration } from '../config';
|
||||
|
||||
import exportedStore, { createStore } from './store';
|
||||
|
||||
@@ -22,10 +22,10 @@ jest.mock('redux-logger', () => ({
|
||||
createLogger: () => 'logger',
|
||||
}));
|
||||
jest.mock('redux-thunk', () => 'thunkMiddleware');
|
||||
jest.mock('../config', () => ({
|
||||
configuration: {
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({
|
||||
SEGMENT_KEY: 'a-fake-segment-key',
|
||||
},
|
||||
})),
|
||||
}));
|
||||
jest.mock('redux-beacon', () => ({
|
||||
createMiddleware: jest.fn((map, model) => ({ map, model })),
|
||||
@@ -60,9 +60,9 @@ describe('store aggregator module', () => {
|
||||
});
|
||||
});
|
||||
describe('if no SEGMENT_KEY', () => {
|
||||
const key = configuration.SEGMENT_KEY;
|
||||
const key = getConfig().SEGMENT_KEY;
|
||||
beforeEach(() => {
|
||||
configuration.SEGMENT_KEY = false;
|
||||
getConfig.mockImplementation(() => ({ SEGMENT_KEY: false }));
|
||||
});
|
||||
it('exports thunk and logger middleware, composed and applied with dev tools', () => {
|
||||
expect(createStore().middleware).toEqual(
|
||||
@@ -70,7 +70,7 @@ describe('store aggregator module', () => {
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
configuration.SEGMENT_KEY = key;
|
||||
getConfig.mockImplementation(() => ({ SEGMENT_KEY: key }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
21
src/head/Head.jsx
Normal file
21
src/head/Head.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const Head = ({ intl }) => (
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages['gradebook.page.title'], { siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
);
|
||||
|
||||
Head.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Head);
|
||||
17
src/head/Head.test.jsx
Normal file
17
src/head/Head.test.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { mount } from 'enzyme';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import Head from './Head';
|
||||
|
||||
describe('Head', () => {
|
||||
const props = {};
|
||||
it('should match render title tag and favicon with the site configuration values', () => {
|
||||
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
|
||||
const helmet = Helmet.peek();
|
||||
expect(helmet.title).toEqual(`Gradebook | ${getConfig().SITE_NAME}`);
|
||||
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
|
||||
expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL);
|
||||
});
|
||||
});
|
||||
11
src/head/messages.js
Normal file
11
src/head/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'gradebook.page.title': {
|
||||
id: 'gradebook.page.title',
|
||||
defaultMessage: 'Gradebook | {siteName}',
|
||||
description: 'Title tag',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -7,6 +7,7 @@ import ReactDOM from 'react-dom';
|
||||
import {
|
||||
APP_READY,
|
||||
initialize,
|
||||
mergeConfig,
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
@@ -20,6 +21,22 @@ subscribe(APP_READY, () => {
|
||||
});
|
||||
|
||||
initialize({
|
||||
handlers: {
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
BASE_URL: process.env.BASE_URL,
|
||||
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',
|
||||
SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
});
|
||||
},
|
||||
},
|
||||
messages: [
|
||||
appMessages,
|
||||
headerMessages,
|
||||
|
||||
@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
|
||||
import {
|
||||
APP_READY,
|
||||
initialize,
|
||||
mergeConfig,
|
||||
subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
@@ -19,6 +20,7 @@ jest.mock('react-dom', () => ({
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
APP_READY: 'app-is-ready-key',
|
||||
initialize: jest.fn(),
|
||||
mergeConfig: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-header', () => ({
|
||||
@@ -46,10 +48,23 @@ describe('app registry', () => {
|
||||
ReactDOM.render(<App />, document.getElementById('root')),
|
||||
);
|
||||
});
|
||||
test('initialize is called with footerMessages and requireAuthenticatedUser', () => {
|
||||
test('initialize is called with requireAuthenticatedUser, messages, and a config handler', () => {
|
||||
expect(initialize).toHaveBeenCalledWith({
|
||||
messages: [appMessages, headerMessages, footerMessages],
|
||||
requireAuthenticatedUser: true,
|
||||
handlers: {
|
||||
config: expect.any(Function),
|
||||
},
|
||||
});
|
||||
});
|
||||
test('initialize config loads LMS_BASE_URL from env', () => {
|
||||
const oldEnv = process.env;
|
||||
const initializeArg = initialize.mock.calls[0][0];
|
||||
process.env = { ...oldEnv, LMS_BASE_URL: 'http://example.com/fake' };
|
||||
initializeArg.handlers.config();
|
||||
expect(mergeConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ LMS_BASE_URL: 'http://example.com/fake' }),
|
||||
);
|
||||
process.env = oldEnv;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// The code in this file is from Segment's website:
|
||||
// https://segment.com/docs/sources/website/analytics.js/quickstart/
|
||||
import { configuration } from './config';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
(function () {
|
||||
// Create a queue, but don't obliterate an existing one!
|
||||
@@ -81,5 +81,5 @@ import { configuration } from './config';
|
||||
|
||||
// Load Analytics.js with your key, which will automatically
|
||||
// load the tools you've enabled for your account. Boosh!
|
||||
analytics.load(configuration.SEGMENT_KEY);
|
||||
analytics.load(getConfig().SEGMENT_KEY);
|
||||
}());
|
||||
|
||||
@@ -8,6 +8,8 @@ Enzyme.configure({ adapter: new Adapter() });
|
||||
// These configuration values are usually set in webpack's EnvironmentPlugin however
|
||||
// Jest does not use webpack so we need to set these so for testing
|
||||
process.env.LMS_BASE_URL = 'http://localhost:18000';
|
||||
process.env.SITE_NAME = 'localhost';
|
||||
process.env.FAVICON_URL = 'http://localhost:18000/favicon.ico';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
|
||||
|
||||
Reference in New Issue
Block a user