Skip to content

Commit 91dd109

Browse files
leangseu-edxBen Warzeski
and
Ben Warzeski
authored
fix: cannot select criterion (openedx#100)
* fix: cannot select criterion * fix: refactor fill grade data * fix: update tests Co-authored-by: Ben Warzeski <[email protected]>
1 parent b2098be commit 91dd109

File tree

4 files changed

+119
-100
lines changed

4 files changed

+119
-100
lines changed

src/data/redux/app/selectors.js

+31-29
Original file line numberDiff line numberDiff line change
@@ -124,44 +124,46 @@ rubric.criteriaIndices = createSelector(
124124
* Returns true iff the passed feedback value is required or optional
125125
* @return {bool} - should include feedback?
126126
*/
127-
const shouldIncludeFeedback = (feedback) => ([
127+
export const shouldIncludeFeedback = (feedback) => [
128128
feedbackRequirement.required,
129129
feedbackRequirement.optional,
130-
]).includes(feedback);
130+
].includes(feedback);
131131

132132
/**
133-
* Returns an empty grade data object based on the rubric config loaded in the app model.
134-
* @return {obj} - empty grade data object
133+
* take current grade and fill the empty fill with default value
134+
* @param {obj} gradeData
135+
* @returns
135136
*/
136-
export const emptyGrade = createSelector(
137-
[module.rubric.hasConfig, module.rubric.criteria, module.rubric.feedbackConfig],
138-
(hasConfig, criteria, feedbackConfig) => {
139-
if (!hasConfig) {
140-
return null;
141-
}
142-
const gradeData = {};
143-
if (shouldIncludeFeedback(feedbackConfig)) {
144-
gradeData.overallFeedback = '';
145-
}
146-
gradeData.criteria = criteria.map(criterion => {
147-
const entry = {
148-
orderNum: criterion.orderNum,
149-
name: criterion.name,
150-
selectedOption: '',
151-
};
152-
if (shouldIncludeFeedback(criterion.feedback)) {
153-
entry.feedback = '';
154-
}
155-
return entry;
156-
});
157-
return gradeData;
158-
},
159-
);
137+
export const fillGradeData = (state, data) => {
138+
const hasConfig = module.rubric.hasConfig(state);
139+
if (!hasConfig || Array.isArray(data?.criteria)) {
140+
return data;
141+
}
142+
143+
const feedbackConfig = module.rubric.feedbackConfig(state);
144+
const criteria = module.rubric.criteria(state);
145+
146+
const overallFeedback = (
147+
module.shouldIncludeFeedback(feedbackConfig) && { overallFeedback: '' }
148+
);
149+
const criteriaFeedback = (feedback) => (
150+
module.shouldIncludeFeedback(feedback) && { feedback: '' }
151+
);
152+
153+
const gradeData = { ...overallFeedback };
154+
gradeData.criteria = criteria.map(({ feedback, name, orderNum }) => ({
155+
...criteriaFeedback(feedback),
156+
name,
157+
orderNum,
158+
selectedOption: '',
159+
}));
160+
return gradeData;
161+
};
160162

161163
export default StrictDict({
162164
...simpleSelectors,
163165
courseId,
164166
ora,
165167
rubric: StrictDict(rubric),
166-
emptyGrade,
168+
fillGradeData,
167169
});

src/data/redux/app/selectors.test.js

+79-46
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { feedbackRequirement } from 'data/services/lms/constants';
22

3+
import { keyStore } from '../../../utils';
34
// import * in order to mock in-file references
45
import * as selectors from './selectors';
56

@@ -44,6 +45,8 @@ const testState = {
4445
},
4546
};
4647

48+
const selectorKeys = keyStore(selectors);
49+
4750
describe('app selectors unit tests', () => {
4851
const { appSelector, simpleSelectors, rubric } = selectors;
4952
describe('appSelector', () => {
@@ -180,56 +183,86 @@ describe('app selectors unit tests', () => {
180183
});
181184
});
182185
});
183-
describe('emptyGrade selector', () => {
184-
const { rubricConfig } = testState.app.oraMetadata;
185-
let preSelectors;
186-
let cb;
186+
describe('shouldIncludeFeedback', () => {
187+
it('returns true iff the passed feedback is optional or required', () => {
188+
expect(selectors.shouldIncludeFeedback(feedbackRequirement.optional)).toEqual(true);
189+
expect(selectors.shouldIncludeFeedback(feedbackRequirement.required)).toEqual(true);
190+
expect(selectors.shouldIncludeFeedback(feedbackRequirement.disabled)).toEqual(false);
191+
expect(selectors.shouldIncludeFeedback('aribitrary')).toEqual(false);
192+
});
193+
});
194+
describe('fillGradeData selector', () => {
195+
const cb = selectors.fillGradeData;
196+
const spies = {};
197+
let oldRubric;
198+
const criteria = [
199+
{ name: 'criteria1', orderNum: 0, feedback: true },
200+
{ name: 'criteria2', orderNum: 1, feedback: false },
201+
{ name: 'criteria3', orderNum: 2, feedback: true },
202+
];
203+
204+
const data = { arbitrary: 'data', criteria };
205+
beforeAll(() => {
206+
oldRubric = { ...rubric };
207+
});
187208
beforeEach(() => {
188-
({ preSelectors, cb } = selectors.emptyGrade);
189-
});
190-
it('is a memoized selector based on rubric.[hasConfig, criteria, feedbackConfig]', () => {
191-
expect(preSelectors).toEqual([
192-
rubric.hasConfig,
193-
rubric.criteria,
194-
rubric.feedbackConfig,
195-
]);
196-
});
197-
describe('If the config is not loaded (hasConfig = undefined)', () => {
198-
it('returns null', () => {
199-
expect(cb(false, {}, '')).toEqual(null);
209+
rubric.hasConfig = jest.fn(() => true);
210+
rubric.feedbackConfig = jest.fn(() => true);
211+
rubric.criteria = jest.fn(() => criteria);
212+
spies.shouldIncludeFeedback = jest.spyOn(
213+
selectors,
214+
selectorKeys.shouldIncludeFeedback,
215+
).mockImplementation(val => val);
216+
});
217+
afterEach(() => {
218+
spies[selectorKeys.shouldIncludeFeedback].mockRestore();
219+
});
220+
afterAll(() => {
221+
selectors.rubric = { ...oldRubric };
222+
});
223+
224+
describe('if rubric config is not loaded', () => {
225+
it('returns passed gradeData', () => {
226+
rubric.hasConfig.mockReturnValueOnce(false);
227+
expect(cb(testState, data)).toEqual(data);
200228
});
201229
});
202-
describe('The generated object', () => {
203-
it('loads an overallFeedback field iff feedbackConfig is optional or required', () => {
204-
let gradeData = cb(true, rubricConfig.criteria, feedbackRequirement.optional);
205-
expect(gradeData.overallFeedback).toEqual('');
206-
gradeData = cb(true, rubricConfig.criteria, feedbackRequirement.required);
207-
expect(gradeData.overallFeedback).toEqual('');
208-
gradeData = cb(true, rubricConfig.criteria, feedbackRequirement.disabled);
209-
expect(gradeData.overallFeedback).toEqual(undefined);
230+
231+
describe('if rubric config is loaded', () => {
232+
describe('gradeData is passed, contains criteria', () => {
233+
it('returns the passed gradeData', () => {
234+
expect(cb(testState, data)).toEqual(data);
235+
});
210236
});
211-
it('loads criteria with feedback field based on requirement config', () => {
212-
const gradeData = cb(true, rubricConfig.criteria, rubricConfig.feedback);
213-
const { criteria } = rubricConfig;
214-
expect(gradeData.criteria).toEqual([
215-
{
216-
orderNum: criteria[0].orderNum,
217-
name: criteria[0].name,
218-
selectedOption: '',
219-
feedback: '',
220-
},
221-
{
222-
orderNum: criteria[1].orderNum,
223-
name: criteria[1].name,
224-
selectedOption: '',
225-
},
226-
{
227-
orderNum: criteria[2].orderNum,
228-
name: criteria[2].name,
229-
selectedOption: '',
230-
feedback: '',
231-
},
232-
]);
237+
describe('gradeData is not passed', () => {
238+
it('adds overall feedback iff is configured for inclusion', () => {
239+
expect(cb(testState, null).overallFeedback).toEqual('');
240+
rubric.feedbackConfig.mockReturnValueOnce(false);
241+
expect(cb(testState, null).overallFeedback).toEqual(undefined);
242+
});
243+
describe('criteria', () => {
244+
it('displays name, orderNum, and feedback per config and empty selection', () => {
245+
expect(cb(testState, null).criteria).toEqual([
246+
{
247+
name: criteria[0].name,
248+
orderNum: criteria[0].orderNum,
249+
feedback: '',
250+
selectedOption: '',
251+
},
252+
{
253+
name: criteria[1].name,
254+
orderNum: criteria[1].orderNum,
255+
selectedOption: '',
256+
},
257+
{
258+
name: criteria[2].name,
259+
orderNum: criteria[2].orderNum,
260+
feedback: '',
261+
selectedOption: '',
262+
},
263+
]);
264+
});
265+
});
233266
});
234267
});
235268
});

src/data/redux/thunkActions/grading.js

+2-8
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,8 @@ export const loadSubmission = () => (dispatch, getState) => {
4949
dispatch(actions.grading.loadSubmission({ ...response, submissionUUID }));
5050
if (selectors.grading.selected.isGrading(getState())) {
5151
dispatch(actions.app.setShowRubric(true));
52-
// safety constraints
5352
let { gradeData } = response;
54-
if (gradeData === null || gradeData === undefined || Object.keys(gradeData).length) {
55-
gradeData = selectors.app.emptyGrade(getState());
56-
}
53+
gradeData = selectors.app.fillGradeData(getState(), gradeData);
5754
const lockStatus = selectors.grading.selected.lockStatus(getState());
5855
dispatch(actions.grading.startGrading({ lockStatus, gradeData }));
5956
}
@@ -76,10 +73,7 @@ export const startGrading = () => (dispatch, getState) => {
7673
onSuccess: (response) => {
7774
dispatch(actions.app.setShowRubric(true));
7875
let gradeData = selectors.grading.selected.gradeData(getState());
79-
// safety constraints
80-
if (gradeData === null || gradeData === undefined || Object.keys(gradeData).length) {
81-
gradeData = selectors.app.emptyGrade(getState());
82-
}
76+
gradeData = selectors.app.fillGradeData(getState(), gradeData);
8377
dispatch(actions.grading.startGrading({ ...response, gradeData }));
8478
},
8579
onFailure: (error) => {

src/data/redux/thunkActions/grading.test.js

+7-17
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jest.mock('./requests', () => ({
1010
}));
1111

1212
jest.mock('data/redux/app/selectors', () => ({
13-
emptyGrade: (state) => ({ emptyGrade: state }),
13+
fillGradeData: (state, data) => ({ fillGradeData: state, data }),
1414
}));
1515

1616
jest.mock('data/redux/grading/selectors', () => ({
@@ -140,32 +140,22 @@ describe('grading thunkActions', () => {
140140
beforeEach(() => {
141141
dispatch.mockClear();
142142
});
143-
test('dispatches startGrading with selected gradeData if truthy', () => {
143+
const fillString = 'selectors.app.fillGradeData based on selected gradeData';
144+
test(`dispatches startGrading w/ ${fillString}`, () => {
144145
actionArgs.onSuccess(startResponse);
145146
expect(dispatch.mock.calls).toContainEqual([
146147
actions.app.setShowRubric(true),
147148
], [
148149
actions.grading.startGrading({
149150
...startResponse,
150-
gradeData: selectors.grading.selected.gradeData(testState),
151+
gradeData: selectors.app.fillGradeData(
152+
testState,
153+
selectors.grading.selected.gradeData(testState),
154+
),
151155
}),
152156
]);
153157
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
154158
});
155-
test('dispatches startGrading with empty grade if selected gradeData is null', () => {
156-
const emptyGrade = selectors.app.emptyGrade(testState);
157-
const expected = [
158-
actions.grading.startGrading({ ...startResponse, gradeData: emptyGrade }),
159-
];
160-
selectors.grading.selected.gradeData.mockReturnValue(null);
161-
actionArgs.onSuccess({ ...startResponse, gradeData: null });
162-
expect(dispatch.mock.calls).toContainEqual(expected);
163-
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
164-
dispatch.mockClear();
165-
actionArgs.onSuccess({ ...startResponse, gradeData: null });
166-
expect(dispatch.mock.calls).toContainEqual(expected);
167-
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
168-
});
169159
});
170160
});
171161

0 commit comments

Comments
 (0)