Skip to content

Commit a75cc2b

Browse files
author
Ben Warzeski
authored
Fix assessment validation for criteria with no options but feedback enabled. (#158)
* chore: routing and modal hook tests * fix: assessments with criteria that have feedback but no options * fix: remove stale code * fix: Prompt relative asset urls pointing to LMS now * fix: lint
1 parent 6b084ca commit a75cc2b

File tree

11 files changed

+499
-154
lines changed

11 files changed

+499
-154
lines changed

src/components/ProgressBar/hooks.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { useParams } from 'react-router-dom';
22

3-
import { useIsEmbedded, useViewStep } from 'hooks/routing';
3+
import { useViewStep } from 'hooks/routing';
44
import { useGlobalState, useStepInfo } from 'hooks/app';
55
import { useOpenModal } from 'hooks/modal';
66
import { stepRoutes, stepStates, stepNames } from 'constants/index';
77

88
export const useProgressStepData = ({ step, canRevisit = false }) => {
99
const { xblockId, courseId } = useParams();
10-
const isEmbedded = useIsEmbedded();
1110
const viewStep = useViewStep();
1211
const {
1312
effectiveGrade,
@@ -17,9 +16,7 @@ export const useProgressStepData = ({ step, canRevisit = false }) => {
1716
const stepInfo = useStepInfo();
1817
const openModal = useOpenModal();
1918

20-
const href = `/${stepRoutes[step]}${
21-
isEmbedded ? '/embedded' : ''
22-
}/${courseId}/${xblockId}`;
19+
const href = `/${stepRoutes[step]}/${courseId}/${xblockId}`;
2320
const onClick = () => openModal({ view: step, title: step });
2421
const isActive = viewStep === stepNames.xblock
2522
? activeStepName === step

src/components/Prompt/index.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ const Prompt = ({ prompt, defaultOpen }) => {
1717
const activeStepName = useActiveStepName();
1818
const message = messages[viewStep] || messages[activeStepName];
1919
const title = message ? formatMessage(message) : '';
20+
const regex = /img src="\/asset-v1(.*)/g;
21+
const promptWithAssets = prompt.replaceAll(regex, `img src="${process.env.LMS_BASE_URL}/asset-v1$1`);
2022
return (
2123
<Collapsible title={(<h3 className="py-3">{title}</h3>)} open={open} onToggle={toggleOpen}>
22-
<div dangerouslySetInnerHTML={{ __html: prompt }} />
24+
<div dangerouslySetInnerHTML={{ __html: promptWithAssets }} />
2325
</Collapsible>
2426
);
2527
};

src/constants/eventTypes.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { StrictDict } from '@edx/react-unit-test-utils';
2+
3+
export default StrictDict({
4+
refresh: 'ora-refresh',
5+
modalClose: 'plugin.modal-close',
6+
modalOpen: 'plugin.modal',
7+
});

src/hooks/assessment.js

Lines changed: 162 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -7,152 +7,175 @@ import * as lmsSelectors from 'data/services/lms/hooks/selectors';
77
import * as lmsActions from 'data/services/lms/hooks/actions';
88
import * as routingHooks from './routing';
99

10-
const useIsCriterionFeedbackInvalid = () => {
11-
const viewStep = routingHooks.useViewStep();
12-
const criteriaConfig = lmsSelectors.useCriteriaConfig();
13-
return ({ value, criterionIndex }) => {
14-
const config = criteriaConfig[criterionIndex];
15-
return viewStep !== stepNames.studentTraining
16-
&& value === ''
17-
&& config.feedbackRequired;
18-
};
10+
export const hooks = {
11+
useIsCriterionFeedbackInvalid: () => {
12+
const viewStep = routingHooks.useViewStep();
13+
const criteriaConfig = lmsSelectors.useCriteriaConfig();
14+
return ({ value, criterionIndex }) => {
15+
const config = criteriaConfig[criterionIndex];
16+
return viewStep !== stepNames.studentTraining
17+
&& value === ''
18+
&& config.feedbackRequired;
19+
};
20+
},
21+
22+
useTrainingOptionValidity: (criterionIndex) => {
23+
const value = reduxHooks.useCriterionOption(criterionIndex);
24+
const expected = (lmsSelectors.useStepInfo().studentTraining || {}).expectedRubricSelections;
25+
if (!value || !expected || expected[criterionIndex] === null) {
26+
return null;
27+
}
28+
return `${expected[criterionIndex]}` === value ? 'valid' : 'invalid';
29+
},
30+
31+
useResetAssessment: () => {
32+
const reset = reduxHooks.useResetAssessment();
33+
const setFormFields = reduxHooks.useSetFormFields();
34+
const emptyRubric = lmsSelectors.useEmptyRubric();
35+
return () => {
36+
reset();
37+
setFormFields(emptyRubric);
38+
};
39+
},
40+
41+
useOverallFeedbackFormFields: () => {
42+
const value = reduxHooks.useOverallFeedbackValue();
43+
const setFeedback = reduxHooks.useSetOverallFeedback();
44+
return { value, onChange: (e) => setFeedback(e.target.value) };
45+
},
46+
47+
useCheckTrainingSelection: () => {
48+
const assessment = reduxHooks.useFormFields();
49+
const expected = (lmsSelectors.useStepInfo().studentTraining || {}).expectedRubricSelections;
50+
if (!expected || !assessment) {
51+
return true;
52+
}
53+
return assessment.criteria.every(
54+
(criterion, criterionIndex) => (
55+
!expected || `${expected[criterionIndex]}` === criterion.selectedOption
56+
),
57+
);
58+
},
59+
60+
useInitializeAssessment: () => {
61+
const emptyRubric = lmsSelectors.useEmptyRubric();
62+
const setFormFields = reduxHooks.useSetFormFields();
63+
const setResponse = reduxHooks.useSetResponse();
64+
const response = lmsSelectors.useResponseData();
65+
React.useEffect(() => {
66+
setResponse(response);
67+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
68+
69+
return React.useCallback(() => {
70+
setFormFields(emptyRubric);
71+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
72+
},
1973
};
20-
21-
const useTrainingOptionValidity = (criterionIndex) => {
22-
const value = reduxHooks.useCriterionOption(criterionIndex);
23-
const expected = (lmsSelectors.useStepInfo().studentTraining || {}).expectedRubricSelections;
24-
if (!value || !expected || expected[criterionIndex] === null) {
25-
return null;
26-
}
27-
return `${expected[criterionIndex]}` === value ? 'valid' : 'invalid';
28-
};
29-
30-
export const useCriterionOptionFormFields = (criterionIndex) => {
31-
const value = reduxHooks.useCriterionOption(criterionIndex);
32-
const setOption = reduxHooks.useSetCriterionOption(criterionIndex);
33-
const setShowTrainingError = reduxHooks.useSetShowTrainingError();
34-
const isInvalid = value === null;
35-
return {
36-
value,
37-
onChange: (e) => {
74+
Object.assign(hooks, {
75+
useCriterionOptionFormFields: (criterionIndex) => {
76+
const value = reduxHooks.useCriterionOption(criterionIndex);
77+
const setOption = reduxHooks.useSetCriterionOption(criterionIndex);
78+
const setShowTrainingError = reduxHooks.useSetShowTrainingError();
79+
const trainingOptionValidity = hooks.useTrainingOptionValidity(criterionIndex);
80+
81+
const onChange = React.useCallback((e) => {
3882
setShowTrainingError(false);
3983
setOption(e.target.value);
40-
},
41-
isInvalid,
42-
trainingOptionValidity: useTrainingOptionValidity(criterionIndex),
43-
};
44-
};
45-
46-
export const useCriterionFeedbackFormFields = (criterionIndex) => {
47-
const value = reduxHooks.useCriterionFeedback(criterionIndex);
48-
const setFeedback = reduxHooks.useSetCriterionFeedback(criterionIndex);
49-
const isInvalid = useIsCriterionFeedbackInvalid()({
50-
value, criterionIndex,
51-
});
52-
return { value, onChange: (e) => setFeedback(e.target.value), isInvalid };
53-
};
54-
export const useOverallFeedbackFormFields = () => {
55-
const value = reduxHooks.useOverallFeedbackValue();
56-
const setFeedback = reduxHooks.useSetOverallFeedback();
57-
return { value, onChange: (e) => setFeedback(e.target.value) };
58-
};
84+
}, [setOption, setShowTrainingError]);
5985

60-
export const useIsAssessmentInvalid = () => {
61-
const assessment = reduxHooks.useFormFields();
62-
const criteriaConfig = lmsSelectors.useCriteriaConfig();
63-
const isFeedbackInvalid = useIsCriterionFeedbackInvalid();
64-
if (!assessment.criteria.length) {
65-
return false;
66-
}
67-
return criteriaConfig.some(
68-
(c, criterionIndex) => {
69-
const { feedback, selectedOption } = assessment.criteria[criterionIndex];
70-
return (
71-
selectedOption === null
72-
|| isFeedbackInvalid({ value: feedback, criterionIndex })
73-
);
74-
},
75-
);
76-
};
77-
78-
export const useCheckTrainingSelection = () => {
79-
const assessment = reduxHooks.useFormFields();
80-
const expected = (lmsSelectors.useStepInfo().studentTraining || {}).expectedRubricSelections;
81-
if (!expected || !assessment) {
82-
return true;
83-
}
84-
return assessment.criteria.every(
85-
(criterion, criterionIndex) => (
86-
!expected || `${expected[criterionIndex]}` === criterion.selectedOption
87-
),
88-
);
89-
};
90-
export const useInitializeAssessment = () => {
91-
const emptyRubric = lmsSelectors.useEmptyRubric();
92-
const setFormFields = reduxHooks.useSetFormFields();
93-
const setResponse = reduxHooks.useSetResponse();
94-
const response = lmsSelectors.useResponseData();
95-
React.useEffect(() => {
96-
setResponse(response);
97-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
98-
99-
return React.useCallback(() => {
100-
setFormFields(emptyRubric);
101-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
102-
};
86+
const isInvalid = value === null;
10387

104-
export const useOnSubmit = () => {
105-
const setAssessment = reduxHooks.useLoadAssessment();
106-
const setShowValidation = reduxHooks.useSetShowValidation();
107-
const setShowTrainingError = reduxHooks.useSetShowTrainingError();
108-
const setHasSubmitted = reduxHooks.useSetHasSubmitted();
109-
110-
const isInvalid = useIsAssessmentInvalid();
111-
const checkTrainingSelection = useCheckTrainingSelection();
112-
113-
const viewStep = routingHooks.useViewStep();
114-
const formFields = reduxHooks.useFormFields();
115-
const submitAssessmentMutation = lmsActions.useSubmitAssessment({ onSuccess: setAssessment });
116-
117-
return {
118-
onSubmit: React.useCallback(() => {
119-
if (isInvalid) {
120-
return setShowValidation(true);
121-
}
122-
if (viewStep === stepNames.studentTraining && !checkTrainingSelection) {
123-
return setShowTrainingError(true);
124-
}
125-
return submitAssessmentMutation.mutateAsync({
126-
...formFields,
127-
step: viewStep,
128-
}).then((data) => {
129-
setAssessment(data);
130-
setHasSubmitted(true);
131-
});
132-
}, [
133-
viewStep,
134-
formFields,
88+
return {
89+
value,
90+
onChange,
13591
isInvalid,
136-
setShowValidation,
137-
checkTrainingSelection,
138-
submitAssessmentMutation,
139-
setAssessment,
140-
setShowTrainingError,
141-
setHasSubmitted,
142-
]),
143-
status: submitAssessmentMutation.status,
144-
};
145-
};
92+
trainingOptionValidity,
93+
};
94+
},
95+
96+
useCriterionFeedbackFormFields: (criterionIndex) => {
97+
const value = reduxHooks.useCriterionFeedback(criterionIndex);
98+
const setFeedback = reduxHooks.useSetCriterionFeedback(criterionIndex);
99+
const isInvalid = hooks.useIsCriterionFeedbackInvalid()({
100+
value, criterionIndex,
101+
});
102+
return { value, onChange: (e) => setFeedback(e.target.value), isInvalid };
103+
},
104+
105+
useIsAssessmentInvalid: () => {
106+
const assessment = reduxHooks.useFormFields();
107+
const criteriaConfig = lmsSelectors.useCriteriaConfig();
108+
const isFeedbackInvalid = hooks.useIsCriterionFeedbackInvalid();
109+
if (!assessment.criteria.length) {
110+
return false;
111+
}
112+
return criteriaConfig.some(
113+
(c, criterionIndex) => {
114+
const { feedback, selectedOption } = assessment.criteria[criterionIndex];
115+
return (
116+
(c.options.length && selectedOption === null)
117+
|| isFeedbackInvalid({ value: feedback, criterionIndex })
118+
);
119+
},
120+
);
121+
},
122+
});
123+
124+
Object.assign(hooks, {
125+
useOnSubmit: () => {
126+
const setAssessment = reduxHooks.useLoadAssessment();
127+
const setShowValidation = reduxHooks.useSetShowValidation();
128+
const setShowTrainingError = reduxHooks.useSetShowTrainingError();
129+
const setHasSubmitted = reduxHooks.useSetHasSubmitted();
130+
131+
const isInvalid = hooks.useIsAssessmentInvalid();
132+
const checkTrainingSelection = hooks.useCheckTrainingSelection();
133+
134+
const viewStep = routingHooks.useViewStep();
135+
const formFields = reduxHooks.useFormFields();
136+
const submitAssessmentMutation = lmsActions.useSubmitAssessment({ onSuccess: setAssessment });
137+
138+
return {
139+
onSubmit: React.useCallback(() => {
140+
if (isInvalid) {
141+
return setShowValidation(true);
142+
}
143+
if (viewStep === stepNames.studentTraining && !checkTrainingSelection) {
144+
return setShowTrainingError(true);
145+
}
146+
return submitAssessmentMutation.mutateAsync({
147+
...formFields,
148+
step: viewStep,
149+
}).then((data) => {
150+
setAssessment(data);
151+
setHasSubmitted(true);
152+
});
153+
}, [
154+
viewStep,
155+
formFields,
156+
isInvalid,
157+
setShowValidation,
158+
checkTrainingSelection,
159+
submitAssessmentMutation,
160+
setAssessment,
161+
setShowTrainingError,
162+
setHasSubmitted,
163+
]),
164+
status: submitAssessmentMutation.status,
165+
};
166+
},
167+
});
146168

147-
export const useResetAssessment = () => {
148-
const reset = reduxHooks.useResetAssessment();
149-
const setFormFields = reduxHooks.useSetFormFields();
150-
const emptyRubric = lmsSelectors.useEmptyRubric();
151-
return () => {
152-
reset();
153-
setFormFields(emptyRubric);
154-
};
155-
};
169+
export const {
170+
useResetAssessment,
171+
useOverallFeedbackFormFields,
172+
useCheckTrainingSelection,
173+
useInitializeAssessment,
174+
useCriterionOptionFormFields,
175+
useCriterionFeedbackFormFields,
176+
useIsAssessmentInvalid,
177+
useOnSubmit,
178+
} = hooks;
156179

157180
export const {
158181
useHasSubmitted,

0 commit comments

Comments
 (0)