-
-
-
+
`;
diff --git a/src/containers/ReviewModal/index.jsx b/src/containers/ReviewModal/index.jsx
index 192b162..079b555 100644
--- a/src/containers/ReviewModal/index.jsx
+++ b/src/containers/ReviewModal/index.jsx
@@ -9,7 +9,6 @@ import { RequestKeys } from 'data/constants/requests';
import LoadingMessage from 'components/LoadingMessage';
import ReviewActions from 'containers/ReviewActions';
-import ReviewError from './ReviewError';
import ReviewContent from './ReviewContent';
import messages from './messages';
@@ -29,11 +28,11 @@ export class ReviewModal extends React.Component {
}
get isLoading() {
- return !(this.props.hasError || this.props.isLoaded);
+ return !(this.props.errorStatus || this.props.isLoaded);
}
render() {
- const { isOpen, isLoaded, hasError } = this.props;
+ const { isOpen, isLoaded, errorStatus } = this.props;
return (
- {isOpen && (
- <>
- {isLoaded && }
- {hasError && }
- >
- )}
+ {isOpen && }
{/* even if the modal is closed, in case we want to add transitions later */}
- {!(isLoaded || hasError) && }
+ {!(isLoaded || errorStatus) && }
);
}
}
ReviewModal.defaultProps = {
+ errorStatus: null,
response: null,
};
ReviewModal.propTypes = {
@@ -66,7 +61,7 @@ ReviewModal.propTypes = {
}),
setShowReview: PropTypes.func.isRequired,
isLoaded: PropTypes.bool.isRequired,
- hasError: PropTypes.bool.isRequired,
+ errorStatus: PropTypes.number,
};
export const mapStateToProps = (state) => ({
@@ -74,7 +69,7 @@ export const mapStateToProps = (state) => ({
oraName: selectors.app.ora.name(state),
response: selectors.grading.selected.response(state),
isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.fetchSubmission }),
- hasError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchSubmission }),
+ errorStatus: selectors.requests.errorStatus(state, { requestKey: RequestKeys.fetchSubmission }),
});
export const mapDispatchToProps = {
diff --git a/src/containers/ReviewModal/index.test.jsx b/src/containers/ReviewModal/index.test.jsx
index 1c3f70f..38c8418 100644
--- a/src/containers/ReviewModal/index.test.jsx
+++ b/src/containers/ReviewModal/index.test.jsx
@@ -24,7 +24,7 @@ jest.mock('data/redux', () => ({
},
requests: {
isCompleted: (...args) => ({ isCompleted: args }),
- isFailed: (...args) => ({ isFailed: args }),
+ errorStatus: (...args) => ({ errorStatus: args }),
},
},
actions: {
@@ -35,7 +35,6 @@ jest.mock('data/redux', () => ({
}));
jest.mock('containers/ReviewActions', () => 'ReviewActions');
-jest.mock('./ReviewError', () => 'ReviewError');
jest.mock('./ReviewContent', () => 'ReviewContent');
jest.mock('components/LoadingMessage', () => 'LoadingMessage');
@@ -48,7 +47,6 @@ describe('ReviewModal component', () => {
response: { text: (some text
) },
showRubric: false,
isLoaded: false,
- hasError: false,
};
describe('component', () => {
beforeEach(() => {
@@ -69,7 +67,7 @@ describe('ReviewModal component', () => {
expect(render()).toMatchSnapshot();
});
test('error', () => {
- el.setProps({ isOpen: true, hasError: true });
+ el.setProps({ isOpen: true, errorStatus: 200 });
expect(render()).toMatchSnapshot();
});
test('success', () => {
@@ -96,8 +94,8 @@ describe('ReviewModal component', () => {
test('isLoaded loads from requests.isCompleted(fetchSubmission)', () => {
expect(mapped.isLoaded).toEqual(selectors.requests.isCompleted(testState, { requestKey }));
});
- test('hasError loads from requests.isFailed(fetchSubmission)', () => {
- expect(mapped.hasError).toEqual(selectors.requests.isFailed(testState, { requestKey }));
+ test('errorStatus loads from requests.errorStatus(fetchSubmission)', () => {
+ expect(mapped.errorStatus).toEqual(selectors.requests.errorStatus(testState, { requestKey }));
});
});
describe('mapDispatchToProps', () => {
diff --git a/src/containers/ReviewModal/messages.js b/src/containers/ReviewModal/messages.js
index ddc8a99..d342185 100644
--- a/src/containers/ReviewModal/messages.js
+++ b/src/containers/ReviewModal/messages.js
@@ -1,4 +1,5 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
+import { StrictDict } from 'utils';
const messages = defineMessages({
loadErrorHeading: {
@@ -21,6 +22,26 @@ const messages = defineMessages({
defaultMessage: 'Loading response',
description: 'loading text for submission response review screen',
},
+ gradeNotSubmittedHeading: {
+ id: 'ora-grading.ReviewModal.gradeNotSubmitted.heading',
+ defaultMessage: 'Grade not submitted',
+ description: 'Grade submission network error heading',
+ },
+ gradeNotSubmittedContent: {
+ id: 'ora-grading.ReviewModal.gradeNotSubmitted.heading',
+ defaultMessage: "We're sorry, something went wrong when we tried to submit this grade. Please try again.",
+ description: 'Grade submission network error message',
+ },
+ resubmitGrade: {
+ id: 'ora-grading.ReviewModal.resubmitGrade',
+ defaultMessage: 'Resubmit grate',
+ description: 'Resubmit grade button after network failure',
+ },
+ dismiss: {
+ id: 'ora-grading.ReviewModal.dismiss',
+ defaultMessage: 'Dismiss',
+ description: 'Dismiss error action button text',
+ },
});
-export default messages;
+export default StrictDict(messages);
diff --git a/src/containers/Rubric/Rubric.scss b/src/containers/Rubric/Rubric.scss
index fd30b07..63f53ee 100644
--- a/src/containers/Rubric/Rubric.scss
+++ b/src/containers/Rubric/Rubric.scss
@@ -69,4 +69,8 @@
justify-content: center;
padding: map-get($spacers, 3);
}
+
+ button.pgn__stateful-btn.pgn__stateful-btn-state-pending {
+ opacity: .4 !important;
+ }
}
diff --git a/src/containers/Rubric/__snapshots__/index.test.jsx.snap b/src/containers/Rubric/__snapshots__/index.test.jsx.snap
index db589fc..1fad1cb 100644
--- a/src/containers/Rubric/__snapshots__/index.test.jsx.snap
+++ b/src/containers/Rubric/__snapshots__/index.test.jsx.snap
@@ -19,27 +19,22 @@ exports[`Rubric Container snapshot is grading 1`] = `
/>
@@ -48,15 +43,109 @@ exports[`Rubric Container snapshot is grading 1`] = `
-
+
+`;
+
+exports[`Rubric Container snapshot is grading, submit pending 1`] = `
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ "default": ,
+ "pending": ,
+ }
+ }
+ onClick={[MockFunction this.submitGradeHandler]}
+ state="pending"
+ />
`;
@@ -80,27 +169,22 @@ exports[`Rubric Container snapshot is not grading 1`] = `
/>
@@ -108,3 +192,78 @@ exports[`Rubric Container snapshot is not grading 1`] = `
`;
+
+exports[`Rubric Container snapshot submit completed 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ "default": ,
+ "pending": ,
+ }
+ }
+ onClick={[MockFunction this.submitGradeHandler]}
+ state="complete"
+ />
+
+
+`;
diff --git a/src/containers/Rubric/index.jsx b/src/containers/Rubric/index.jsx
index 5f048ae..4c2a8ac 100644
--- a/src/containers/Rubric/index.jsx
+++ b/src/containers/Rubric/index.jsx
@@ -2,10 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { Card, Button } from '@edx/paragon';
+import { Card, StatefulButton } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import { StrictDict } from 'utils';
import { selectors, thunkActions } from 'data/redux';
+import { RequestKeys } from 'data/constants/requests';
import CriterionContainer from 'containers/CriterionContainer';
import RubricFeedback from './RubricFeedback';
@@ -13,6 +15,13 @@ import messages from './messages';
import './Rubric.scss';
+const ButtonStates = StrictDict({
+ default: 'default',
+ pending: 'pending',
+ complete: 'complete',
+ error: 'error',
+});
+
/**
*
*/
@@ -23,6 +32,16 @@ export class Rubric extends React.Component {
this.submitGradeHandler = this.submitGradeHandler.bind(this);
}
+ get submitButtonState() {
+ if (this.props.isPending) {
+ return ButtonStates.pending;
+ }
+ if (this.props.isCompleted) {
+ return ButtonStates.complete;
+ }
+ return ButtonStates.default;
+ }
+
submitGradeHandler() {
this.props.submitGrade();
}
@@ -44,11 +63,18 @@ export class Rubric extends React.Component {
- {isGrading && (
+ {(isGrading || this.props.isCompleted) && (
-
+ ,
+ [ButtonStates.pending]: ,
+ [ButtonStates.complete]: ,
+ }}
+ />
)}
@@ -59,13 +85,19 @@ Rubric.defaultProps = {
criteriaIndices: [],
};
Rubric.propTypes = {
+ isCompleted: PropTypes.bool.isRequired,
isGrading: PropTypes.bool.isRequired,
+ isPending: PropTypes.bool.isRequired,
criteriaIndices: PropTypes.arrayOf(PropTypes.number),
submitGrade: PropTypes.func.isRequired,
};
+const requestKey = RequestKeys.submitGrade;
+
export const mapStateToProps = (state) => ({
+ isCompleted: selectors.requests.isCompleted(state, { requestKey }),
isGrading: selectors.grading.selected.isGrading(state),
+ isPending: selectors.requests.isPending(state, { requestKey }),
criteriaIndices: selectors.app.rubric.criteriaIndices(state),
});
diff --git a/src/containers/Rubric/index.test.jsx b/src/containers/Rubric/index.test.jsx
index 6bdc617..da7c19d 100644
--- a/src/containers/Rubric/index.test.jsx
+++ b/src/containers/Rubric/index.test.jsx
@@ -18,9 +18,13 @@ jest.mock('data/redux', () => ({
},
grading: {
selected: {
- isGrading: jest.fn((...args) => ({ isGragrding: args })),
+ isGrading: jest.fn((...args) => ({ isGrading: args })),
},
},
+ requests: {
+ isCompleted: jest.fn((...args) => ({ isCompleted: args })),
+ isPending: jest.fn((...args) => ({ isPending: args })),
+ },
},
thunkActions: {
grading: {
@@ -31,6 +35,8 @@ jest.mock('data/redux', () => ({
describe('Rubric Container', () => {
const props = {
+ isCompleted: false,
+ isPending: false,
isGrading: true,
criteriaIndices: [1, 2, 3, 4, 5],
submitGrade: jest.fn().mockName('this.props.submitGrade'),
@@ -43,14 +49,23 @@ describe('Rubric Container', () => {
.mockName('this.submitGradeHandler');
});
describe('snapshot', () => {
+ beforeEach(() => {
+ el.instance().submitGradeHandler = jest.fn().mockName('this.submitGradeHandler');
+ });
test('is grading', () => {
- expect(el).toMatchSnapshot();
+ expect(el.instance().render()).toMatchSnapshot();
});
test('is not grading', () => {
- el.setProps({
- isGrading: false,
- });
- expect(el).toMatchSnapshot();
+ el.setProps({ isGrading: false });
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ test('is grading, submit pending', () => {
+ el.setProps({ isPending: true });
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ test('submit completed', () => {
+ el.setProps({ isCompleted: true, isGrading: false });
+ expect(el.instance().render()).toMatchSnapshot();
});
});
diff --git a/src/containers/Rubric/messages.js b/src/containers/Rubric/messages.js
index 7c11381..b9561d9 100644
--- a/src/containers/Rubric/messages.js
+++ b/src/containers/Rubric/messages.js
@@ -1,6 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
+ gradeSubmitted: {
+ id: 'ora-grading.Rubric.gradeSubmitted',
+ defaultMessage: 'Grade Submitted',
+ description: 'Submit Grade button text after successful submission',
+ },
rubric: {
id: 'ora-grading.Rubric.rubric',
defaultMessage: 'Rubric',
@@ -11,6 +16,11 @@ const messages = defineMessages({
defaultMessage: 'Submit grade',
description: 'Submit Grade button text',
},
+ submittingGrade: {
+ id: 'ora-grading.Rubric.submittingGrade',
+ defaultMessage: 'Submitting grade',
+ description: 'Submit Grade button text while submitting',
+ },
overallComments: {
id: 'ora-grading.Rubric.overallComments',
defaultMessage: 'Overall comments',
diff --git a/src/data/constants/requests.js b/src/data/constants/requests.js
index 932e212..5a1d2c6 100644
--- a/src/data/constants/requests.js
+++ b/src/data/constants/requests.js
@@ -16,3 +16,16 @@ export const RequestKeys = StrictDict({
prefetchPrev: 'prefetchPrev',
submitGrade: 'submitGrade',
});
+
+export const ErrorCodes = StrictDict({
+ missingParam: 'ERR_MISSING_PARAM',
+});
+
+export const ErrorStatuses = StrictDict({
+ badRequest: 400,
+ unauthorized: 401,
+ forbidden: 403,
+ notFound: 404,
+ conflict: 409,
+ serverError: 500,
+});
diff --git a/src/data/redux/grading/reducer.js b/src/data/redux/grading/reducer.js
index b6279f8..efd0b72 100644
--- a/src/data/redux/grading/reducer.js
+++ b/src/data/redux/grading/reducer.js
@@ -107,12 +107,11 @@ export const updateGradingData = (state, data) => {
*/
export const updateCriterion = (state, orderNum, data) => {
const entry = state.gradingData[state.current.submissionUUID];
+ const criteria = [...entry.criteria];
+ criteria[orderNum] = { ...entry.criteria[orderNum], ...data };
return updateGradingData(state, {
...entry,
- criteria: {
- ...entry.criteria,
- [orderNum]: { ...entry.criteria[orderNum], ...data },
- },
+ criteria,
});
};
@@ -196,15 +195,43 @@ const grading = createSlice({
},
};
},
- stopGrading: (state) => {
+ loadStatus: (state, { payload }) => {
+ const gradingData = { ...state.gradingData };
+ delete gradingData[state.current.submissionUUID];
+ return {
+ ...state,
+ gradeData: {
+ ...state.gradeData,
+ [state.current.submissionUUID]: { ...payload.gradeData },
+ },
+ gradingData,
+ current: {
+ ...state.current,
+ gradeStatus: payload.gradeStatus,
+ lockStatus: payload.lockStatus,
+ },
+ };
+ },
+ stopGrading: (state, { payload }) => {
+ const { submissionUUID } = state.current;
const localGradeData = { ...state.localGradeData };
- delete localGradeData[state.current.submissionUUID];
+ delete localGradeData[submissionUUID];
+ const gradeData = { ...state.gradeData };
+ let lockStatus = lockStatuses.unlocked;
+ let { gradeStatus } = state.current;
+ if (payload) {
+ const { submissionStatus } = payload;
+ gradeData[submissionUUID] = submissionStatus.gradeData;
+ lockStatus = submissionStatus.lockStatus;
+ gradeStatus = submissionStatus.gradeStatus;
+ }
return {
...state,
localGradeData,
current: {
...state.current,
- lockStatus: lockStatuses.unlocked,
+ lockStatus,
+ gradeStatus,
},
};
},
diff --git a/src/data/redux/requests/reducer.js b/src/data/redux/requests/reducer.js
index 709eccb..4e7b068 100644
--- a/src/data/redux/requests/reducer.js
+++ b/src/data/redux/requests/reducer.js
@@ -39,6 +39,10 @@ const requests = createSlice({
error: payload.error,
},
}),
+ clearRequest: (state, { payload }) => ({
+ ...state,
+ [payload.requestKey]: {},
+ }),
},
});
diff --git a/src/data/redux/requests/selectors.js b/src/data/redux/requests/selectors.js
index 15f8266..9fa746e 100644
--- a/src/data/redux/requests/selectors.js
+++ b/src/data/redux/requests/selectors.js
@@ -11,6 +11,9 @@ export const isPending = ({ status }) => status === RequestStates.pending;
export const isCompleted = ({ status }) => status === RequestStates.completed;
export const isFailed = ({ status }) => status === RequestStates.failed;
export const error = (request) => request.error;
+export const errorStatus = (request) => request.error?.response?.status;
+export const errorCode = (request) => request.error?.response?.data;
+
export const data = (request) => request.data;
export const allowNavigation = ({ requests }) => (
@@ -25,5 +28,7 @@ export default StrictDict({
isCompleted: statusSelector(isCompleted),
isFailed: statusSelector(isFailed),
error: statusSelector(error),
+ errorCode: statusSelector(errorCode),
+ errorStatus: statusSelector(errorStatus),
data: statusSelector(data),
});
diff --git a/src/data/redux/thunkActions/grading.js b/src/data/redux/thunkActions/grading.js
index 17b5937..585eda7 100644
--- a/src/data/redux/thunkActions/grading.js
+++ b/src/data/redux/thunkActions/grading.js
@@ -1,6 +1,7 @@
import { StrictDict } from 'utils';
import { actions, selectors } from 'data/redux';
+import { RequestKeys, ErrorStatuses } from 'data/constants/requests';
import * as module from './grading';
import requests from './requests';
@@ -39,10 +40,14 @@ export const loadSelectionForReview = (submissionUUIDs) => (dispatch) => {
export const loadSubmission = () => (dispatch, getState) => {
const submissionUUID = selectors.grading.selected.submissionUUID(getState());
+ dispatch(actions.requests.clearRequest({ requestKey: RequestKeys.submitGrade }));
dispatch(requests.fetchSubmission({
submissionUUID,
onSuccess: (response) => {
dispatch(actions.grading.loadSubmission({ ...response, submissionUUID }));
+ if (selectors.grading.selected.isGrading(getState())) {
+ dispatch(module.startGrading());
+ }
},
}));
};
@@ -55,12 +60,13 @@ export const loadSubmission = () => (dispatch, getState) => {
* based on the rubric config.
*/
export const startGrading = () => (dispatch, getState) => {
+ dispatch(actions.requests.clearRequest({ requestKey: RequestKeys.submitGrade }));
dispatch(requests.setLock({
value: true,
submissionUUID: selectors.grading.selected.submissionUUID(getState()),
onSuccess: (response) => {
dispatch(actions.app.setShowRubric(true));
- let { gradeData } = response;
+ let gradeData = selectors.grading.selected.gradeData(getState());
if (!gradeData) {
gradeData = selectors.app.emptyGrade(getState());
}
@@ -74,6 +80,7 @@ export const startGrading = () => (dispatch, getState) => {
* Releases the lock and dispatches stopGrading on success.
*/
export const cancelGrading = () => (dispatch, getState) => {
+ dispatch(actions.requests.clearRequest({ requestKey: RequestKeys.submitGrade }));
dispatch(requests.setLock({
value: false,
submissionUUID: selectors.grading.selected.submissionUUID(getState()),
@@ -103,8 +110,10 @@ export const submitGrade = () => (dispatch, getState) => {
onSuccess: (response) => {
dispatch(actions.grading.completeGrading(response));
},
- onFailure: () => {
- // on failure action
+ onFailure: (error) => {
+ if (error.response.status === ErrorStatuses.conflict) {
+ dispatch(actions.grading.stopGrading(error.response.data));
+ }
},
}));
} else {
diff --git a/src/data/redux/thunkActions/grading.test.js b/src/data/redux/thunkActions/grading.test.js
index a13199b..854398e 100644
--- a/src/data/redux/thunkActions/grading.test.js
+++ b/src/data/redux/thunkActions/grading.test.js
@@ -1,4 +1,5 @@
import { actions, selectors } from 'data/redux';
+import { RequestKeys } from 'data/constants/requests';
import * as thunkActions from './grading';
jest.mock('./requests', () => ({
@@ -21,8 +22,9 @@ jest.mock('data/redux/grading/selectors', () => ({
doesExist: jest.fn((state) => ({ nextDoesExist: state })),
},
selected: {
- submissionUUID: (state) => ({ selectedsubmissionUUID: state }),
gradeData: jest.fn((state) => ({ gradeData: state })),
+ isGrading: jest.fn((state) => ({ isGrading: state })),
+ submissionUUID: (state) => ({ selectedsubmissionUUID: state }),
},
}));
@@ -32,13 +34,11 @@ describe('grading thunkActions', () => {
const response = 'test-response';
const objResponse = { response };
let dispatch;
- let dispatched;
let actionArgs;
const getState = () => testState;
const getDispatched = (calledAction) => {
calledAction(dispatch, getState);
- [[dispatched]] = dispatch.mock.calls;
};
beforeEach(() => {
@@ -48,7 +48,11 @@ describe('grading thunkActions', () => {
describe('loadSubmission', () => {
beforeEach(() => {
getDispatched(thunkActions.loadSubmission());
- actionArgs = dispatched.fetchSubmission;
+ actionArgs = dispatch.mock.calls[1][0].fetchSubmission;
+ });
+ test('dispatches clearRequest for submitGrade', () => {
+ const requestKey = RequestKeys.submitGrade;
+ expect(dispatch.mock.calls[0]).toEqual([actions.requests.clearRequest({ requestKey })]);
});
test('dispatches fetchSubmission', () => {
expect(actionArgs).not.toEqual(undefined);
@@ -88,7 +92,7 @@ describe('grading thunkActions', () => {
});
});
describe('loadPrev', () => {
- test('dispatches actions.grading.loadPrev and then loadSubmission', () => {
+ test('clears submitGrade status and dispatches actions.grading.loadPrev and then loadSubmission', () => {
thunkActions.loadPrev()(dispatch, getState);
expect(dispatch.mock.calls).toEqual([
[actions.grading.loadPrev()],
@@ -117,13 +121,17 @@ describe('grading thunkActions', () => {
describe('startGrading', () => {
beforeEach(() => {
getDispatched(thunkActions.startGrading());
- actionArgs = dispatched.setLock;
+ actionArgs = dispatch.mock.calls[1][0].setLock;
});
test('dispatches setLock with selected submissionUUID and value: true', () => {
expect(actionArgs).not.toEqual(undefined);
expect(actionArgs.value).toEqual(true);
expect(actionArgs.submissionUUID).toEqual(selectors.grading.selected.submissionUUID(testState));
});
+ test('dispatches clearRequest for submitGrade', () => {
+ const requestKey = RequestKeys.submitGrade;
+ expect(dispatch.mock.calls[0]).toEqual([actions.requests.clearRequest({ requestKey })]);
+ });
describe('onSuccess', () => {
const gradeData = { some: 'test grade data' };
const startResponse = { other: 'fields', gradeData };
@@ -132,7 +140,12 @@ describe('grading thunkActions', () => {
});
test('dispatches startGrading with selected gradeData if truthy', () => {
actionArgs.onSuccess(startResponse);
- expect(dispatch.mock.calls).toContainEqual([actions.grading.startGrading(startResponse)]);
+ expect(dispatch.mock.calls).toContainEqual([
+ actions.grading.startGrading({
+ ...startResponse,
+ gradeData: selectors.grading.selected.gradeData(testState),
+ }),
+ ]);
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
});
test('dispatches startGrading with empty grade if selected gradeData is not truthy', () => {
@@ -140,6 +153,7 @@ describe('grading thunkActions', () => {
const expected = [
actions.grading.startGrading({ ...startResponse, gradeData: emptyGrade }),
];
+ selectors.grading.selected.gradeData.mockReturnValue(undefined);
actionArgs.onSuccess({ ...startResponse, gradeData: undefined });
expect(dispatch.mock.calls).toContainEqual(expected);
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
@@ -159,11 +173,15 @@ describe('grading thunkActions', () => {
});
beforeEach(() => {
getDispatched(thunkActions.cancelGrading());
- actionArgs = dispatched.setLock;
+ actionArgs = dispatch.mock.calls[1][0].setLock;
});
afterAll(() => {
thunkActions.stopGrading = stopGrading;
});
+ test('dispatches clearRequest for submitGrade', () => {
+ const requestKey = RequestKeys.submitGrade;
+ expect(dispatch.mock.calls[0]).toEqual([actions.requests.clearRequest({ requestKey })]);
+ });
test('dispatches setLock with selected submissionUUID and value: false', () => {
expect(actionArgs).not.toEqual(undefined);
expect(actionArgs.value).toEqual(false);
diff --git a/src/data/redux/thunkActions/requests.js b/src/data/redux/thunkActions/requests.js
index fdf16e2..b860b72 100644
--- a/src/data/redux/thunkActions/requests.js
+++ b/src/data/redux/thunkActions/requests.js
@@ -99,10 +99,10 @@ export const fetchSubmission = ({ submissionUUID, ...rest }) => (dispatch) => {
* @param {[func]} onSuccess - onSuccess method ((response) => { ... })
* @param {[func]} onFailure - onFailure method ((error) => { ... })
*/
-export const setLock = ({ submissionUUID, ...rest }) => (dispatch) => {
+export const setLock = ({ value, submissionUUID, ...rest }) => (dispatch) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.setLock,
- promise: api.lockSubmission(submissionUUID),
+ promise: value ? api.lockSubmission(submissionUUID) : api.unlockSubmission(submissionUUID),
...rest,
}));
};
diff --git a/src/data/redux/thunkActions/requests.test.js b/src/data/redux/thunkActions/requests.test.js
index 3ace9d0..2ba777b 100644
--- a/src/data/redux/thunkActions/requests.test.js
+++ b/src/data/redux/thunkActions/requests.test.js
@@ -8,7 +8,8 @@ jest.mock('data/services/lms/api', () => ({
fetchSubmissionResponse: (submissionUUID) => ({ fetchSubmissionResponse: submissionUUID }),
fetchSubmissionStatus: (submissionUUID) => ({ fetchSubmissionStatus: submissionUUID }),
fetchSubmission: (submissionUUID) => ({ fetchSubmission: submissionUUID }),
- lockSubmission: ({ submissionUUID, value }) => ({ lockSubmission: { submissionUUID, value } }),
+ lockSubmission: ({ submissionUUID }) => ({ lockSubmission: { submissionUUID } }),
+ unlockSubmission: ({ submissionUUID }) => ({ unlockSubmission: { submissionUUID } }),
updateGrade: (submissionUUID, gradeData) => ({ updateGrade: { submissionUUID, gradeData } }),
}));
@@ -153,10 +154,10 @@ describe('requests thunkActions module', () => {
},
});
});
- describe('setLock', () => {
+ describe('setLock: true', () => {
testNetworkRequestAction({
action: requests.setLock,
- args: { submissionUUID },
+ args: { submissionUUID, value: true },
expectedString: 'with setLock promise',
expectedData: {
requestKey: RequestKeys.setLock,
@@ -164,6 +165,17 @@ describe('requests thunkActions module', () => {
},
});
});
+ describe('setLock: false', () => {
+ testNetworkRequestAction({
+ action: requests.setLock,
+ args: { submissionUUID, value: false },
+ expectedString: 'with setLock promise',
+ expectedData: {
+ requestKey: RequestKeys.setLock,
+ promise: api.unlockSubmission(submissionUUID),
+ },
+ });
+ });
describe('submitGrade', () => {
const gradeData = 'test-grade-data';
testNetworkRequestAction({
diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js
index 748a88c..6d54606 100644
--- a/src/data/services/lms/api.js
+++ b/src/data/services/lms/api.js
@@ -2,7 +2,12 @@ import { StrictDict } from 'utils';
import { locationId } from 'data/constants/app';
import { paramKeys } from './constants';
import urls from './urls';
-import { get, post, stringifyUrl } from './utils';
+import {
+ client,
+ get,
+ post,
+ stringifyUrl,
+} from './utils';
/*********************************************************************************
* GET Actions
@@ -75,11 +80,8 @@ export const fetchSubmissionResponse = (submissionUUID) => get(
}),
).then(response => response.data);
-/* I assume this is the "Start Grading" call, even for if a
- * submission is already graded and we are attempting re-lock.
- * Assuming the check for if allowed would happen locally first.
+/**
* post('api/lock', { ora_location, submissionUUID });
- * @param {bool} value - new lock value
* @param {string} submissionUUID
*/
const lockSubmission = (submissionUUID) => post(
@@ -88,12 +90,23 @@ const lockSubmission = (submissionUUID) => post(
[paramKeys.submissionUUID]: submissionUUID,
}),
).then(response => response.data);
+/**
+ * unlockSubmission(submissionUUID)
+ * @param {string} submissionUUID
+ */
+const unlockSubmission = (submissionUUID) => client().delete(
+ stringifyUrl(urls.fetchSubmissionLockUrl, {
+ [paramKeys.oraLocation]: locationId,
+ [paramKeys.submissionUUID]: submissionUUID,
+ }),
+).then(response => response.data);
+
/*
* post('api/updateGrade', { submissionUUID, gradeData })
* @param {object} gradeData - full grading submission data
*/
const updateGrade = (submissionUUID, gradeData) => post(
- stringifyUrl(urls.updateSubmissioonGradeUrl, {
+ stringifyUrl(urls.updateSubmissionGradeUrl, {
[paramKeys.oraLocation]: locationId,
[paramKeys.submissionUUID]: submissionUUID,
}),
@@ -107,4 +120,5 @@ export default StrictDict({
fetchSubmissionStatus,
lockSubmission,
updateGrade,
+ unlockSubmission,
});
diff --git a/src/data/services/lms/fakeData/testUtils.js b/src/data/services/lms/fakeData/testUtils.js
new file mode 100644
index 0000000..55529ec
--- /dev/null
+++ b/src/data/services/lms/fakeData/testUtils.js
@@ -0,0 +1,73 @@
+import { StrictDict } from 'utils';
+import { ErrorStatuses, RequestKeys } from 'data/constants/requests';
+import { gradeStatuses, lockStatuses } from 'data/services/lms/constants';
+import { actions } from 'data/redux';
+
+export const errorData = (status, data = '') => ({
+ response: {
+ status,
+ data,
+ },
+});
+
+export const networkErrorData = errorData(ErrorStatuses.badRequest);
+
+const gradeData = {
+ overallFeedback: 'was okay',
+ criteria: [{ feedback: 'did alright', name: 'firstCriterion', selectedOption: 'good' }],
+};
+
+export const genTestUtils = ({ dispatch }) => {
+ const mockStart = (requestKey) => () => {
+ dispatch(actions.requests.startRequest(requestKey));
+ };
+
+ const mockError = (requestKey, status, data) => () => {
+ dispatch(actions.requests.failRequest({
+ requestKey,
+ error: errorData(status, data),
+ }));
+ };
+
+ const mockNetworkError = (requestKey) => (
+ mockError(requestKey, ErrorStatuses.badRequest)
+ );
+
+ return {
+ init: StrictDict({
+ start: mockStart(RequestKeys.initialize),
+ networkError: mockNetworkError(RequestKeys.initialize),
+ }),
+ fetch: StrictDict({
+ start: mockStart(RequestKeys.fetchSubmission),
+ mockError: mockError(RequestKeys.fetchSubmission, ErrorStatuses.badRequest),
+ }),
+ submitGrade: StrictDict({
+ start: mockStart(RequestKeys.submitGrade),
+ success: () => {
+ dispatch(actions.requests.completeRequest({
+ requestKey: RequestKeys.submitGrade,
+ response: {
+ gradeStatus: gradeStatuses.graded,
+ lockStatus: lockStatuses.unlocked,
+ gradeData,
+ },
+ }));
+ },
+ networkError: mockError(RequestKeys.submitGrade, ErrorStatuses.badRequest),
+ rejectedError: mockError(
+ RequestKeys.submitGrade,
+ ErrorStatuses.conflict,
+ {
+ submissionStatus: {
+ gradeStatus: gradeStatuses.ungraded,
+ lockStatus: lockStatuses.locked,
+ gradeData,
+ },
+ },
+ ),
+ }),
+ };
+};
+
+export default genTestUtils;
diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js
index 9980487..51ab06d 100644
--- a/src/data/services/lms/urls.js
+++ b/src/data/services/lms/urls.js
@@ -10,7 +10,7 @@ const oraInitializeUrl = `${baseEsgUrl}initialize`;
const fetchSubmissionUrl = `${baseEsgUrl}submission`;
const fetchSubmissionStatusUrl = `${baseEsgUrl}submission/status`;
const fetchSubmissionLockUrl = `${baseEsgUrl}submission/lock`;
-const updateSubmissioonGradeUrl = `${baseEsgUrl}submission/grade`;
+const updateSubmissionGradeUrl = `${baseEsgUrl}submission/grade`;
const course = (courseId) => `${baseUrl}/courses/${courseId}`;
@@ -25,7 +25,7 @@ export default StrictDict({
fetchSubmissionUrl,
fetchSubmissionStatusUrl,
fetchSubmissionLockUrl,
- updateSubmissioonGradeUrl,
+ updateSubmissionGradeUrl,
baseUrl,
course,
openResponse,
diff --git a/src/data/services/lms/utils.js b/src/data/services/lms/utils.js
index 9e01ee4..4b0dff5 100644
--- a/src/data/services/lms/utils.js
+++ b/src/data/services/lms/utils.js
@@ -15,6 +15,8 @@ export const get = (...args) => getAuthenticatedHttpClient().get(...args);
*/
export const post = (...args) => getAuthenticatedHttpClient().post(...args);
+export const client = getAuthenticatedHttpClient;
+
/**
* stringifyUrl(url, query)
* simple wrapper around queryString.stringifyUrl that sets skip behavior
diff --git a/src/data/store.js b/src/data/store.js
index 4e73ca3..2098947 100755
--- a/src/data/store.js
+++ b/src/data/store.js
@@ -3,6 +3,8 @@ import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import { createLogger } from 'redux-logger';
+import apiTestUtils from 'data/services/lms/fakeData/testUtils';
+
import reducer, { actions, selectors } from './redux';
export const createStore = () => {
@@ -22,6 +24,7 @@ export const createStore = () => {
window.store = store;
window.actions = actions;
window.selectors = selectors;
+ window.apiTestUtils = apiTestUtils(store);
}
return store;
diff --git a/src/setupTest.js b/src/setupTest.js
index 426a51e..41daf94 100755
--- a/src/setupTest.js
+++ b/src/setupTest.js
@@ -71,6 +71,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Content: 'Popover.Content',
},
Row: 'Row',
+ StatefulButton: 'StatefulButton',
TextFilter: 'TextFilter',
}));