feat: Group, General Feedback Settings, Randomization
This Ticket adds three new settings widgets to the Problem Editor: Randomization: This is a setting for advanced problems only which deals with python scripts. General Feedback: This is feedback which is only applied to certain problem types for mass-adding feedback to incorrect problems. Group Feedback: For certain problems, the user can provide specific feedback for a combination of specific answers.
This commit is contained in:
@@ -7,19 +7,24 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page 1`] = `
|
||||
<div
|
||||
className="mb-3 settingsCardTopdiv"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
<TypeCard
|
||||
problemType="stringresponse"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<ScoringCard />
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<HintsCard />
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<GroupFeedback />
|
||||
</div>
|
||||
<div>
|
||||
<Advanced
|
||||
@@ -51,27 +56,27 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page 1`] = `
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<ShowAnswerCard />
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<ResetCard />
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<TimerCard />
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<MatlabCard />
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
<SwitchToAdvancedEditorCard
|
||||
problemType="stringresponse"
|
||||
/>
|
||||
</div>
|
||||
@@ -87,19 +92,24 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page advanced
|
||||
<div
|
||||
className="mb-3 settingsCardTopdiv"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
<TypeCard
|
||||
problemType="stringresponse"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<ScoringCard />
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<HintsCard />
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<GroupFeedback />
|
||||
</div>
|
||||
<div>
|
||||
<Advanced
|
||||
@@ -131,27 +141,27 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page advanced
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<ShowAnswerCard />
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<ResetCard />
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<TimerCard />
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<MatlabCard />
|
||||
</div>
|
||||
<div
|
||||
className="my-3"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
<SwitchToAdvancedEditorCard
|
||||
problemType="stringresponse"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -13,17 +13,23 @@ import ResetCard from './settingsComponents/ResetCard';
|
||||
import MatlabCard from './settingsComponents/MatlabCard';
|
||||
import TimerCard from './settingsComponents/TimerCard';
|
||||
import TypeCard from './settingsComponents/TypeCard';
|
||||
import GeneralFeedbackCard from './settingsComponents/GeneralFeedback/index';
|
||||
import GroupFeedbackCard from './settingsComponents/GroupFeedback/index';
|
||||
import SwitchToAdvancedEditorCard from './settingsComponents/SwitchToAdvancedEditorCard';
|
||||
import messages from './messages';
|
||||
import { showAdvancedSettingsCards } from './hooks';
|
||||
|
||||
import './index.scss';
|
||||
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
|
||||
import Randomization from './settingsComponents/Randomization';
|
||||
|
||||
// This widget should be connected, grab all settings from store, update them as needed.
|
||||
export const SettingsWidget = ({
|
||||
problemType,
|
||||
// redux
|
||||
answers,
|
||||
generalFeedback,
|
||||
groupFeedbackList,
|
||||
blockTitle,
|
||||
correctAnswerCount,
|
||||
settings,
|
||||
@@ -33,6 +39,30 @@ export const SettingsWidget = ({
|
||||
updateAnswer,
|
||||
}) => {
|
||||
const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards();
|
||||
|
||||
const feedbackCard = () => {
|
||||
if (problemType === ProblemTypeKeys.ADVANCED) {
|
||||
return (<></>);
|
||||
}
|
||||
if ([ProblemTypeKeys.MULTISELECT, ProblemTypeKeys.TEXTINPUT, ProblemTypeKeys.NUMERIC].includes(problemType)) {
|
||||
return (
|
||||
<div className="mt-3"><GroupFeedbackCard
|
||||
groupFeedbacks={groupFeedbackList}
|
||||
updateSettings={updateField}
|
||||
answers={answers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mt-3"><GeneralFeedbackCard
|
||||
generalFeedback={generalFeedback}
|
||||
updateSettings={updateField}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settingsWidget ml-4">
|
||||
<div className="mb-3 settingsCardTopdiv">
|
||||
@@ -52,7 +82,7 @@ export const SettingsWidget = ({
|
||||
<div className="mt-3">
|
||||
<HintsCard hints={settings.hints} updateSettings={updateSettings} />
|
||||
</div>
|
||||
|
||||
{feedbackCard()}
|
||||
<div>
|
||||
<Collapsible.Advanced open={!isAdvancedCardsVisible}>
|
||||
<Collapsible.Body className="collapsible-body small">
|
||||
@@ -80,6 +110,13 @@ export const SettingsWidget = ({
|
||||
<div className="my-3">
|
||||
<ResetCard showResetButton={settings.showResetButton} updateSettings={updateSettings} />
|
||||
</div>
|
||||
{
|
||||
problemType === ProblemTypeKeys.ADVANCED && (
|
||||
<div className="my-3">
|
||||
<Randomization randomization={settings.randomization} updateSettings={updateSettings} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="my-3">
|
||||
<TimerCard timeBetween={settings.timeBetween} updateSettings={updateSettings} />
|
||||
</div>
|
||||
@@ -103,6 +140,16 @@ SettingsWidget.propTypes = {
|
||||
title: PropTypes.string,
|
||||
unselectedFeedback: PropTypes.string,
|
||||
})).isRequired,
|
||||
generalFeedback: PropTypes.string.isRequired,
|
||||
groupFeedbackList: PropTypes.arrayOf(
|
||||
PropTypes.shape(
|
||||
{
|
||||
id: PropTypes.number,
|
||||
feedback: PropTypes.string,
|
||||
answers: PropTypes.arrayOf(PropTypes.string),
|
||||
},
|
||||
),
|
||||
).isRequired,
|
||||
blockTitle: PropTypes.string.isRequired,
|
||||
correctAnswerCount: PropTypes.number.isRequired,
|
||||
problemType: PropTypes.string.isRequired,
|
||||
@@ -115,6 +162,8 @@ SettingsWidget.propTypes = {
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
generalFeedback: selectors.problem.generalFeedback(state),
|
||||
groupFeedbackList: selectors.problem.groupFeedbackList(state),
|
||||
settings: selectors.problem.settings(state),
|
||||
answers: selectors.problem.answers(state),
|
||||
blockTitle: selectors.app.blockTitle(state),
|
||||
|
||||
@@ -9,6 +9,18 @@ jest.mock('./hooks', () => ({
|
||||
showAdvancedSettingsCards: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./settingsComponents/GeneralFeedback', () => 'GeneralFeedback');
|
||||
jest.mock('./settingsComponents/GroupFeedback', () => 'GroupFeedback');
|
||||
jest.mock('./settingsComponents/Randomization', () => 'Randomization');
|
||||
jest.mock('./settingsComponents/HintsCard', () => 'HintsCard');
|
||||
jest.mock('./settingsComponents/MatlabCard', () => 'MatlabCard');
|
||||
jest.mock('./settingsComponents/ResetCard', () => 'ResetCard');
|
||||
jest.mock('./settingsComponents/ScoringCard', () => 'ScoringCard');
|
||||
jest.mock('./settingsComponents/ShowAnswerCard', () => 'ShowAnswerCard');
|
||||
jest.mock('./settingsComponents/SwitchToAdvancedEditorCard', () => 'SwitchToAdvancedEditorCard');
|
||||
jest.mock('./settingsComponents/TimerCard', () => 'TimerCard');
|
||||
jest.mock('./settingsComponents/TypeCard', () => 'TypeCard');
|
||||
|
||||
describe('SettingsWidget', () => {
|
||||
const props = {
|
||||
problemType: ProblemTypeKeys.TEXTINPUT,
|
||||
|
||||
@@ -205,5 +205,4 @@ export const messages = {
|
||||
description: 'Solution Explanation text',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RandomizationCard snapshot snapshot: renders general feedback setting card 1`] = `
|
||||
<SettingsOption
|
||||
className=""
|
||||
extraSections={Array []}
|
||||
none={false}
|
||||
summary={
|
||||
Object {
|
||||
"defaultMessage": "sUmmary",
|
||||
}
|
||||
}
|
||||
title="General Feedback"
|
||||
>
|
||||
<div
|
||||
className="halfSpacedMessage"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Enter the feedback to appear when a student submits a wrong answer. This will be overridden if you add answer-specific feedback."
|
||||
description="description for general feedback input, clariying useage"
|
||||
id="authoring.problemeditor.settings.generalFeedbackInputDescription"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
floatingLabel="Enter General Feedback"
|
||||
onChange={[MockFunction randomizationCardHooks.handleChange]}
|
||||
value="sOmE_vAlUE"
|
||||
/>
|
||||
</Form.Group>
|
||||
</SettingsOption>
|
||||
`;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import _ from 'lodash-es';
|
||||
import messages from './messages';
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = {
|
||||
summary: (val) => useState(val),
|
||||
};
|
||||
|
||||
export const generalFeedbackHooks = (generalFeedback, updateSettings) => {
|
||||
const [summary, setSummary] = module.state.summary({
|
||||
message: messages.noGeneralFeedbackSummary, values: {}, intl: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (_.isEmpty(generalFeedback)) {
|
||||
setSummary({ message: messages.noGeneralFeedbackSummary, values: {}, intl: true });
|
||||
} else {
|
||||
setSummary({
|
||||
message: generalFeedback,
|
||||
values: {},
|
||||
intl: false,
|
||||
});
|
||||
}
|
||||
}, [generalFeedback]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
updateSettings({ generalFeedback: event.target.value });
|
||||
};
|
||||
|
||||
return {
|
||||
summary,
|
||||
handleChange,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useEffect } from 'react';
|
||||
import { MockUseState } from '../../../../../../../../testUtils';
|
||||
import messages from './messages';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('react', () => {
|
||||
const updateState = jest.fn();
|
||||
return {
|
||||
updateState,
|
||||
useEffect: jest.fn(),
|
||||
useState: jest.fn(val => ([{ state: val }, (newVal) => updateState({ val, newVal })])),
|
||||
};
|
||||
});
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
describe('Problem settings hooks', () => {
|
||||
let output;
|
||||
let updateSettings;
|
||||
let generalFeedback;
|
||||
beforeEach(() => {
|
||||
updateSettings = jest.fn();
|
||||
generalFeedback = 'sOmE_vAlUe';
|
||||
state.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
state.restore();
|
||||
useEffect.mockClear();
|
||||
});
|
||||
describe('Show advanced settings', () => {
|
||||
beforeEach(() => {
|
||||
output = hooks.generalFeedbackHooks(generalFeedback, updateSettings);
|
||||
});
|
||||
test('test default state is false', () => {
|
||||
expect(output.summary.message).toEqual(messages.noGeneralFeedbackSummary);
|
||||
});
|
||||
test('test showAdvancedCards sets state to true', () => {
|
||||
const mockEvent = { target: { value: 'sOmE_otheR_ValUe' } };
|
||||
output.handleChange(mockEvent);
|
||||
expect(updateSettings).toHaveBeenCalledWith({ generalFeedback: mockEvent.target.value });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import SettingsOption from '../../SettingsOption';
|
||||
import messages from './messages';
|
||||
import { generalFeedbackHooks } from './hooks';
|
||||
|
||||
export const GeneralFeedbackCard = ({
|
||||
generalFeedback,
|
||||
updateSettings,
|
||||
// inject
|
||||
intl,
|
||||
}) => {
|
||||
const { summary, handleChange } = generalFeedbackHooks(generalFeedback, updateSettings);
|
||||
return (
|
||||
<SettingsOption
|
||||
title={intl.formatMessage(messages.generalFeebackSettingTitle)}
|
||||
summary={summary.intl ? intl.formatMessage(summary.message) : summary.message}
|
||||
none={!generalFeedback}
|
||||
>
|
||||
<div className="halfSpacedMessage">
|
||||
<span>
|
||||
<FormattedMessage {...messages.generalFeedbackDescription} />
|
||||
</span>
|
||||
</div>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
value={generalFeedback}
|
||||
onChange={handleChange}
|
||||
floatingLabel={intl.formatMessage(messages.generalFeedbackInputLabel)}
|
||||
/>
|
||||
</Form.Group>
|
||||
</SettingsOption>
|
||||
);
|
||||
};
|
||||
|
||||
GeneralFeedbackCard.propTypes = {
|
||||
generalFeedback: PropTypes.string.isRequired,
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GeneralFeedbackCard);
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { formatMessage } from '../../../../../../../../testUtils';
|
||||
import { GeneralFeedbackCard } from './index';
|
||||
import { generalFeedbackHooks } from './hooks';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
generalFeedbackHooks: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('RandomizationCard', () => {
|
||||
const props = {
|
||||
generalFeedback: 'sOmE_vAlUE',
|
||||
updateSettings: jest.fn().mockName('args.updateSettings'),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
|
||||
const randomizationCardHooksProps = {
|
||||
summary: { message: { defaultMessage: 'sUmmary' } },
|
||||
handleChange: jest.fn().mockName('randomizationCardHooks.handleChange'),
|
||||
};
|
||||
|
||||
generalFeedbackHooks.mockReturnValue(randomizationCardHooksProps);
|
||||
|
||||
describe('behavior', () => {
|
||||
it(' calls generalFeedbackHooks with props when initialized', () => {
|
||||
shallow(<GeneralFeedbackCard {...props} />);
|
||||
expect(generalFeedbackHooks).toHaveBeenCalledWith(
|
||||
props.generalFeedback, props.updateSettings,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('snapshot: renders general feedback setting card', () => {
|
||||
expect(shallow(<GeneralFeedbackCard {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
export const messages = {
|
||||
generalFeebackSettingTitle: {
|
||||
id: 'authoring.problemeditor.settings.generalFeebackSettingTitle',
|
||||
defaultMessage: 'General Feedback',
|
||||
description: 'label for general feedback setting',
|
||||
},
|
||||
generalFeedbackInputLabel: {
|
||||
id: 'authoring.problemeditor.settings.generalFeedbackInputLabel',
|
||||
defaultMessage: 'Enter General Feedback',
|
||||
description: 'label for general feedback input describing rules',
|
||||
},
|
||||
generalFeedbackDescription: {
|
||||
id: 'authoring.problemeditor.settings.generalFeedbackInputDescription',
|
||||
defaultMessage: 'Enter the feedback to appear when a student submits a wrong answer. This will be overridden if you add answer-specific feedback.',
|
||||
description: 'description for general feedback input, clariying useage',
|
||||
},
|
||||
noGeneralFeedbackSummary: {
|
||||
id: 'authoring.problemeditor.settings.generalFeedback.noFeedbackSummary',
|
||||
defaultMessage: 'None',
|
||||
description: 'message which informs use there is no general feedback set.',
|
||||
},
|
||||
};
|
||||
export default messages;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Form, Icon, IconButton, Row,
|
||||
} from '@edx/paragon';
|
||||
import { DeleteOutline } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import messages from '../../messages';
|
||||
|
||||
export const GroupFeedbackRow = ({
|
||||
value,
|
||||
handleAnswersSelectedChange,
|
||||
handleFeedbackChange,
|
||||
handleDelete,
|
||||
answers,
|
||||
// injected
|
||||
intl,
|
||||
}) => (
|
||||
|
||||
<div className="mb-4">
|
||||
<ActionRow className="mb-2">
|
||||
<Form.Control
|
||||
value={value.feedback}
|
||||
onChange={handleFeedbackChange}
|
||||
/>
|
||||
<div className="d-flex flex-row flex-nowrap">
|
||||
<IconButton
|
||||
src={DeleteOutline}
|
||||
iconAs={Icon}
|
||||
alt={intl.formatMessage(messages.settingsDeleteIconAltText)}
|
||||
onClick={handleDelete}
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</ActionRow>
|
||||
<Form.CheckboxSet
|
||||
onChange={handleAnswersSelectedChange}
|
||||
value={value.answers}
|
||||
>
|
||||
<Row className="mx-0">
|
||||
{answers.map((letter) => (
|
||||
<Form.Checkbox
|
||||
className="mr-4 mt-1"
|
||||
value={letter.id}
|
||||
checked={value.answers.indexOf(letter.id)}
|
||||
isValid={value.answers.indexOf(letter.id)}
|
||||
>{letter.id}
|
||||
</Form.Checkbox>
|
||||
))}
|
||||
</Row>
|
||||
</Form.CheckboxSet>
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
GroupFeedbackRow.propTypes = {
|
||||
answers: PropTypes.arrayOf(PropTypes.shape({
|
||||
correct: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
selectedFeedback: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
unselectedFeedback: PropTypes.string,
|
||||
})).isRequired,
|
||||
handleAnswersSelectedChange: PropTypes.func.isRequired,
|
||||
handleFeedbackChange: PropTypes.func.isRequired,
|
||||
handleDelete: PropTypes.func.isRequired,
|
||||
value: PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
answers: PropTypes.arrayOf(PropTypes.string),
|
||||
feedback: PropTypes.string,
|
||||
}).isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GroupFeedbackRow);
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { formatMessage } from '../../../../../../../../testUtils';
|
||||
import { GroupFeedbackRow } from './GroupFeedbackRow';
|
||||
|
||||
describe('GroupFeedbackRow', () => {
|
||||
const props = {
|
||||
value: { answers: ['A', 'C'], feedback: 'sOmE FeEDBACK' },
|
||||
answers: ['A', 'B', 'C', 'D'],
|
||||
handleAnswersSelectedChange: jest.fn().mockName('handleAnswersSelectedChange'),
|
||||
handleFeedbackChange: jest.fn().mockName('handleFeedbackChange'),
|
||||
handleEmptyFeedback: jest.fn().mockName('handleEmptyFeedback'),
|
||||
handleDelete: jest.fn().mockName('handleDelete'),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('snapshot: renders hints row', () => {
|
||||
expect(shallow(<GroupFeedbackRow {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GroupFeedbackRow snapshot snapshot: renders hints row 1`] = `
|
||||
<div
|
||||
className="mb-4"
|
||||
>
|
||||
<ActionRow
|
||||
className="mb-2"
|
||||
>
|
||||
<Form.Control
|
||||
onChange={[MockFunction handleFeedbackChange]}
|
||||
value="sOmE FeEDBACK"
|
||||
/>
|
||||
<div
|
||||
className="d-flex flex-row flex-nowrap"
|
||||
>
|
||||
<IconButton
|
||||
alt="Delete answer"
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction handleDelete]}
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</ActionRow>
|
||||
<Component
|
||||
onChange={[MockFunction handleAnswersSelectedChange]}
|
||||
value={
|
||||
Array [
|
||||
"A",
|
||||
"C",
|
||||
]
|
||||
}
|
||||
>
|
||||
<Row
|
||||
className="mx-0"
|
||||
>
|
||||
<Form.Checkbox
|
||||
checked={-1}
|
||||
className="mr-4 mt-1"
|
||||
isValid={-1}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={-1}
|
||||
className="mr-4 mt-1"
|
||||
isValid={-1}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={-1}
|
||||
className="mr-4 mt-1"
|
||||
isValid={-1}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={-1}
|
||||
className="mr-4 mt-1"
|
||||
isValid={-1}
|
||||
/>
|
||||
</Row>
|
||||
</Component>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,168 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HintsCard snapshot snapshot: renders groupFeedbacks setting card multiple groupFeedbacks 1`] = `
|
||||
<SettingsOption
|
||||
className=""
|
||||
extraSections={Array []}
|
||||
none={false}
|
||||
summary=""
|
||||
title="Group Feedback"
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Group feedback will appear when a student selects a specific set of answers."
|
||||
description="label for group feedback input"
|
||||
id="authoring.problemeditor.settings.GroupFeedbackInputLabel"
|
||||
/>
|
||||
</div>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
answers={
|
||||
Array [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
]
|
||||
}
|
||||
id={1}
|
||||
key="1"
|
||||
props="propsValue"
|
||||
value={
|
||||
Object {
|
||||
"answers": Array [
|
||||
"A",
|
||||
"C",
|
||||
],
|
||||
"feedback": "sOmE FeEDBACK",
|
||||
"id": 1,
|
||||
"value": "groupFeedback1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
answers={
|
||||
Array [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
]
|
||||
}
|
||||
id={2}
|
||||
key="2"
|
||||
props="propsValue"
|
||||
value={
|
||||
Object {
|
||||
"answers": Array [
|
||||
"A",
|
||||
],
|
||||
"feedback": "sOmE FeEDBACK oTher FeEdback",
|
||||
"id": 2,
|
||||
"value": "",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
className="m-0 p-0 font-weight-bold"
|
||||
onClick={[MockFunction groupFeedbacksCardHooks.handleAdd]}
|
||||
size="sm"
|
||||
text={null}
|
||||
variant="add"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add group feedback"
|
||||
description="addGroupFeedbackButtonText"
|
||||
id="authoring.problemeditor.settings.addGroupFeedbackButtonText"
|
||||
/>
|
||||
</Button>
|
||||
</SettingsOption>
|
||||
`;
|
||||
|
||||
exports[`HintsCard snapshot snapshot: renders groupFeedbacks setting card no groupFeedbacks 1`] = `
|
||||
<SettingsOption
|
||||
className=""
|
||||
extraSections={Array []}
|
||||
none={true}
|
||||
summary="None"
|
||||
title="Group Feedback"
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Group feedback will appear when a student selects a specific set of answers."
|
||||
description="label for group feedback input"
|
||||
id="authoring.problemeditor.settings.GroupFeedbackInputLabel"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="m-0 p-0 font-weight-bold"
|
||||
onClick={[MockFunction groupFeedbacksCardHooks.handleAdd]}
|
||||
size="sm"
|
||||
text={null}
|
||||
variant="add"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add group feedback"
|
||||
description="addGroupFeedbackButtonText"
|
||||
id="authoring.problemeditor.settings.addGroupFeedbackButtonText"
|
||||
/>
|
||||
</Button>
|
||||
</SettingsOption>
|
||||
`;
|
||||
|
||||
exports[`HintsCard snapshot snapshot: renders groupFeedbacks setting card one groupFeedback 1`] = `
|
||||
<SettingsOption
|
||||
className=""
|
||||
extraSections={Array []}
|
||||
none={false}
|
||||
summary="groupFeedback1"
|
||||
title="Group Feedback"
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Group feedback will appear when a student selects a specific set of answers."
|
||||
description="label for group feedback input"
|
||||
id="authoring.problemeditor.settings.GroupFeedbackInputLabel"
|
||||
/>
|
||||
</div>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
answers={
|
||||
Array [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
]
|
||||
}
|
||||
id={1}
|
||||
key="1"
|
||||
props="propsValue"
|
||||
value={
|
||||
Object {
|
||||
"answers": Array [
|
||||
"A",
|
||||
"C",
|
||||
],
|
||||
"feedback": "sOmE FeEDBACK",
|
||||
"id": 1,
|
||||
"value": "groupFeedback1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
className="m-0 p-0 font-weight-bold"
|
||||
onClick={[MockFunction groupFeedbacksCardHooks.handleAdd]}
|
||||
size="sm"
|
||||
text={null}
|
||||
variant="add"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add group feedback"
|
||||
description="addGroupFeedbackButtonText"
|
||||
id="authoring.problemeditor.settings.addGroupFeedbackButtonText"
|
||||
/>
|
||||
</Button>
|
||||
</SettingsOption>
|
||||
`;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import _ from 'lodash-es';
|
||||
import messages from './messages';
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = {
|
||||
summary: (val) => useState(val),
|
||||
};
|
||||
|
||||
export const groupFeedbackCardHooks = (groupFeedbacks, updateSettings, answerslist) => {
|
||||
const [summary, setSummary] = module.state.summary({ message: messages.noGroupFeedbackSummary, values: {} });
|
||||
|
||||
useEffect(() => {
|
||||
if (groupFeedbacks.length === 0) {
|
||||
setSummary({ message: messages.noGroupFeedbackSummary, values: {} });
|
||||
} else {
|
||||
const feedbacksInList = groupFeedbacks.map(({ answers, feedback }) => {
|
||||
const answerIDs = answerslist.map((a) => a.id);
|
||||
const answersString = answers.filter((value) => answerIDs.includes(value));
|
||||
return `${answersString} ${feedback}\n`;
|
||||
});
|
||||
setSummary({
|
||||
message: messages.groupFeedbackSummary,
|
||||
values: { groupFeedback: feedbacksInList },
|
||||
});
|
||||
}
|
||||
}, [groupFeedbacks, answerslist]);
|
||||
|
||||
const handleAdd = () => {
|
||||
let newId = 0;
|
||||
if (!_.isEmpty(groupFeedbacks)) {
|
||||
newId = Math.max(...groupFeedbacks.map(feedback => feedback.id)) + 1;
|
||||
}
|
||||
const groupFeedback = { id: newId, answers: [], feedback: '' };
|
||||
const modifiedGroupFeedbacks = [...groupFeedbacks, groupFeedback];
|
||||
updateSettings({ groupFeedbackList: modifiedGroupFeedbacks });
|
||||
};
|
||||
|
||||
return {
|
||||
summary,
|
||||
handleAdd,
|
||||
};
|
||||
};
|
||||
|
||||
export const groupFeedbackRowHooks = ({ id, groupFeedbacks, updateSettings }) => {
|
||||
// Hooks for the answers associated with a groupfeedback
|
||||
const addSelectedAnswer = ({ value }) => {
|
||||
const oldGroupFeedback = groupFeedbacks.find(x => x.id === id);
|
||||
const newAnswers = [...oldGroupFeedback.answers, value];
|
||||
const newFeedback = { ...oldGroupFeedback, answers: newAnswers };
|
||||
const remainingFeedbacks = groupFeedbacks.filter((item) => (item.id !== id));
|
||||
const updatedFeedbackList = [newFeedback, ...remainingFeedbacks].sort((a, b) => a.id - b.id);
|
||||
|
||||
updateSettings({ groupFeedbackList: updatedFeedbackList });
|
||||
};
|
||||
const removedSelectedAnswer = ({ value }) => {
|
||||
const oldGroupFeedback = groupFeedbacks.find(x => x.id === id);
|
||||
const newAnswers = oldGroupFeedback.answers.filter(item => item !== value);
|
||||
const newFeedback = { ...oldGroupFeedback, answers: newAnswers };
|
||||
const remainingFeedbacks = groupFeedbacks.filter((item) => (item.id !== id));
|
||||
const updatedFeedbackList = [newFeedback, ...remainingFeedbacks].sort((a, b) => a.id - b.id);
|
||||
|
||||
updateSettings({ groupFeedbackList: updatedFeedbackList });
|
||||
};
|
||||
const handleAnswersSelectedChange = (event) => {
|
||||
const { checked, value } = event.target;
|
||||
if (checked) {
|
||||
addSelectedAnswer({ value });
|
||||
} else {
|
||||
removedSelectedAnswer({ value });
|
||||
}
|
||||
};
|
||||
|
||||
// Delete Button
|
||||
const handleDelete = () => {
|
||||
const modifiedGroupFeedbacks = groupFeedbacks.filter((item) => (item.id !== id));
|
||||
updateSettings({ groupFeedbackList: modifiedGroupFeedbacks });
|
||||
};
|
||||
|
||||
// Hooks for the feedback associated with a groupfeedback
|
||||
const handleFeedbackChange = (event) => {
|
||||
const { value } = event.target;
|
||||
const modifiedGroupFeedback = groupFeedbacks.map(groupFeedback => {
|
||||
if (groupFeedback.id === id) {
|
||||
return { ...groupFeedback, feedback: value };
|
||||
}
|
||||
return groupFeedback;
|
||||
});
|
||||
updateSettings({ groupFeedbackList: modifiedGroupFeedback });
|
||||
};
|
||||
|
||||
return {
|
||||
handleAnswersSelectedChange, handleFeedbackChange, handleDelete,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useEffect } from 'react';
|
||||
import { MockUseState } from '../../../../../../../../testUtils';
|
||||
import messages from './messages';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('react', () => {
|
||||
const updateState = jest.fn();
|
||||
return {
|
||||
updateState,
|
||||
useEffect: jest.fn(),
|
||||
useState: jest.fn(val => ([{ state: val }, (newVal) => updateState({ val, newVal })])),
|
||||
};
|
||||
});
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
describe('groupFeedbackCardHooks', () => {
|
||||
let output;
|
||||
let updateSettings;
|
||||
let groupFeedbacks;
|
||||
beforeEach(() => {
|
||||
updateSettings = jest.fn();
|
||||
groupFeedbacks = [];
|
||||
state.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
state.restore();
|
||||
useEffect.mockClear();
|
||||
});
|
||||
describe('Show advanced settings', () => {
|
||||
beforeEach(() => {
|
||||
output = hooks.groupFeedbackCardHooks(groupFeedbacks, updateSettings);
|
||||
});
|
||||
test('test default state is false', () => {
|
||||
expect(output.summary.message).toEqual(messages.noGroupFeedbackSummary);
|
||||
});
|
||||
test('test Event adds a new feedback ', () => {
|
||||
output.handleAdd();
|
||||
expect(updateSettings).toHaveBeenCalledWith({ groupFeedbackList: [{ id: 0, answers: [], feedback: '' }] });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupFeedbackRowHooks', () => {
|
||||
const mockId = 'iD';
|
||||
const mockAnswer = 'moCkAnsweR';
|
||||
const mockFeedback = 'mOckFEEdback';
|
||||
let groupFeedbacks;
|
||||
let output;
|
||||
let updateSettings;
|
||||
|
||||
beforeEach(() => {
|
||||
updateSettings = jest.fn();
|
||||
groupFeedbacks = [{ id: mockId, answers: [mockAnswer], feedback: mockFeedback }];
|
||||
state.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
state.restore();
|
||||
useEffect.mockClear();
|
||||
});
|
||||
describe('Show advanced settings', () => {
|
||||
beforeEach(() => {
|
||||
output = hooks.groupFeedbackRowHooks({ id: mockId, groupFeedbacks, updateSettings });
|
||||
});
|
||||
test('test associate an answer with the feedback object', () => {
|
||||
const mockNewAnswer = 'nEw VAluE';
|
||||
output.handleAnswersSelectedChange({ target: { checked: true, value: mockNewAnswer } });
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
{ groupFeedbackList: [{ id: mockId, answers: [mockAnswer, mockNewAnswer], feedback: mockFeedback }] },
|
||||
);
|
||||
});
|
||||
test('test unassociate an answer with the feedback object', () => {
|
||||
output.handleAnswersSelectedChange({ target: { checked: false, value: mockAnswer } });
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
{ groupFeedbackList: [{ id: mockId, answers: [], feedback: mockFeedback }] },
|
||||
);
|
||||
});
|
||||
test('test update feedback text with a groupfeedback', () => {
|
||||
const mockNewFeedback = 'nEw fEedBack';
|
||||
output.handleFeedbackChange({ target: { checked: false, value: mockNewFeedback } });
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
{ groupFeedbackList: [{ id: mockId, answers: [mockAnswer], feedback: mockNewFeedback }] },
|
||||
);
|
||||
});
|
||||
test('Delete a Row from the list of feedbacks', () => {
|
||||
output.handleDelete();
|
||||
expect(updateSettings).toHaveBeenCalledWith(
|
||||
{ groupFeedbackList: [] },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import SettingsOption from '../../SettingsOption';
|
||||
import messages from './messages';
|
||||
import { groupFeedbackCardHooks, groupFeedbackRowHooks } from './hooks';
|
||||
import GroupFeedbackRow from './GroupFeedbackRow';
|
||||
import Button from '../../../../../../../sharedComponents/Button';
|
||||
|
||||
export const GroupFeedbackCard = ({
|
||||
groupFeedbacks,
|
||||
updateSettings,
|
||||
answers,
|
||||
// inject
|
||||
intl,
|
||||
}) => {
|
||||
const { summary, handleAdd } = groupFeedbackCardHooks(groupFeedbacks, updateSettings, answers);
|
||||
return (
|
||||
<SettingsOption
|
||||
title={intl.formatMessage(messages.groupFeedbackSettingTitle)}
|
||||
summary={intl.formatMessage(summary.message, { ...summary.values })}
|
||||
none={!groupFeedbacks.length}
|
||||
>
|
||||
<div className="py-3">
|
||||
<FormattedMessage {...messages.groupFeedbackInputLabel} />
|
||||
</div>
|
||||
{groupFeedbacks.map((groupFeedback) => (
|
||||
<GroupFeedbackRow
|
||||
key={groupFeedback.id}
|
||||
id={groupFeedback.id}
|
||||
value={groupFeedback}
|
||||
answers={answers}
|
||||
{...groupFeedbackRowHooks({ id: groupFeedback.id, groupFeedbacks, updateSettings })}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
className="m-0 p-0 font-weight-bold"
|
||||
variant="add"
|
||||
onClick={handleAdd}
|
||||
size="sm"
|
||||
>
|
||||
<FormattedMessage {...messages.addGroupFeedbackButtonText} />
|
||||
</Button>
|
||||
</SettingsOption>
|
||||
);
|
||||
};
|
||||
|
||||
GroupFeedbackCard.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
groupFeedbacks: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
feedback: PropTypes.string.isRequired,
|
||||
answers: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
})).isRequired,
|
||||
answers: PropTypes.arrayOf(PropTypes.shape({
|
||||
correct: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
selectedFeedback: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
unselectedFeedback: PropTypes.string,
|
||||
})).isRequired,
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GroupFeedbackCard);
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { formatMessage } from '../../../../../../../../testUtils';
|
||||
import { GroupFeedbackCard } from './index';
|
||||
import { groupFeedbackRowHooks, groupFeedbackCardHooks } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
groupFeedbackCardHooks: jest.fn(),
|
||||
groupFeedbackRowHooks: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('HintsCard', () => {
|
||||
const answers = ['A', 'B', 'C'];
|
||||
const groupFeedback1 = {
|
||||
id: 1, value: 'groupFeedback1', answers: ['A', 'C'], feedback: 'sOmE FeEDBACK',
|
||||
};
|
||||
const groupFeedback2 = {
|
||||
id: 2, value: '', answers: ['A'], feedback: 'sOmE FeEDBACK oTher FeEdback',
|
||||
};
|
||||
const groupFeedbacks0 = [];
|
||||
const groupFeedbacks1 = [groupFeedback1];
|
||||
const groupFeedbacks2 = [groupFeedback1, groupFeedback2];
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
groupFeedbacks: groupFeedbacks0,
|
||||
updateSettings: jest.fn().mockName('args.updateSettings'),
|
||||
answers,
|
||||
};
|
||||
|
||||
const groupFeedbacksRowHooksProps = { props: 'propsValue' };
|
||||
groupFeedbackRowHooks.mockReturnValue(groupFeedbacksRowHooksProps);
|
||||
|
||||
describe('behavior', () => {
|
||||
it(' calls groupFeedbacksCardHooks when initialized', () => {
|
||||
const groupFeedbacksCardHooksProps = {
|
||||
summary: { message: messages.noGroupFeedbackSummary },
|
||||
handleAdd: jest.fn().mockName('groupFeedbacksCardHooks.handleAdd'),
|
||||
};
|
||||
|
||||
groupFeedbackCardHooks.mockReturnValue(groupFeedbacksCardHooksProps);
|
||||
shallow(<GroupFeedbackCard {...props} />);
|
||||
expect(groupFeedbackCardHooks).toHaveBeenCalledWith(groupFeedbacks0, props.updateSettings, answers);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('snapshot: renders groupFeedbacks setting card no groupFeedbacks', () => {
|
||||
const groupFeedbacksCardHooksProps = {
|
||||
summary: { message: messages.noGroupFeedbackSummary, values: {} },
|
||||
handleAdd: jest.fn().mockName('groupFeedbacksCardHooks.handleAdd'),
|
||||
};
|
||||
|
||||
groupFeedbackCardHooks.mockReturnValue(groupFeedbacksCardHooksProps);
|
||||
expect(shallow(<GroupFeedbackCard {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: renders groupFeedbacks setting card one groupFeedback', () => {
|
||||
const groupFeedbacksCardHooksProps = {
|
||||
summary: {
|
||||
message: messages.groupFeedbackSummary,
|
||||
values: { groupFeedback: groupFeedback1.value, count: 1 },
|
||||
},
|
||||
handleAdd: jest.fn().mockName('groupFeedbacksCardHooks.handleAdd'),
|
||||
};
|
||||
|
||||
groupFeedbackCardHooks.mockReturnValue(groupFeedbacksCardHooksProps);
|
||||
expect(shallow(<GroupFeedbackCard {...props} groupFeedbacks={groupFeedbacks1} />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: renders groupFeedbacks setting card multiple groupFeedbacks', () => {
|
||||
const groupFeedbacksCardHooksProps = {
|
||||
summary: {
|
||||
message: messages.groupFeedbackSummary,
|
||||
values: { groupFeedback: groupFeedback2.value, count: 2 },
|
||||
},
|
||||
handleAdd: jest.fn().mockName('groupFeedbacksCardHooks.handleAdd'),
|
||||
};
|
||||
|
||||
groupFeedbackCardHooks.mockReturnValue(groupFeedbacksCardHooksProps);
|
||||
expect(shallow(<GroupFeedbackCard {...props} groupFeedbacks={groupFeedbacks2} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
export const messages = {
|
||||
noGroupFeedbackSummary: {
|
||||
id: 'authoring.problemeditor.settings.GroupFeedbackSummary.message',
|
||||
defaultMessage: 'None',
|
||||
description: 'message to confirm that a user wants to use the advanced editor',
|
||||
},
|
||||
groupFeedbackSummary: {
|
||||
id: 'authoring.problemeditor.settings.GroupFeedbackSummary.message',
|
||||
defaultMessage: '{groupFeedback}',
|
||||
description: 'summary of current feedbacks provided for multiple problems',
|
||||
},
|
||||
addGroupFeedbackButtonText: {
|
||||
id: 'authoring.problemeditor.settings.addGroupFeedbackButtonText',
|
||||
defaultMessage: 'Add group feedback',
|
||||
description: 'addGroupFeedbackButtonText',
|
||||
},
|
||||
groupFeedbackInputLabel: {
|
||||
id: 'authoring.problemeditor.settings.GroupFeedbackInputLabel',
|
||||
defaultMessage: 'Group feedback will appear when a student selects a specific set of answers.',
|
||||
description: 'label for group feedback input',
|
||||
},
|
||||
groupFeedbackSettingTitle: {
|
||||
id: 'authoring.problemeditor.settings.GroupFeedbackSettingTitle',
|
||||
defaultMessage: 'Group Feedback',
|
||||
description: 'label for group feedback setting',
|
||||
},
|
||||
};
|
||||
export default messages;
|
||||
@@ -0,0 +1,55 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RandomizationCard snapshot snapshot: renders reset true setting card 1`] = `
|
||||
<SettingsOption
|
||||
className=""
|
||||
extraSections={Array []}
|
||||
none={false}
|
||||
summary="sUmmary"
|
||||
title="Randomization"
|
||||
>
|
||||
<div
|
||||
className="halfSpacedMessage"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Defines when to randomize the variables specified in the associated Python script. For problems that do not randomize values, specify \\"Never\\"."
|
||||
description="Description of Possibilities for value in Randomization widget"
|
||||
id="authoring.problemeditor.settings.randomization.SettingText"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={[MockFunction randomizationCardHooks.handleChange]}
|
||||
value="sOmE_vAlUE"
|
||||
>
|
||||
<option
|
||||
key="always"
|
||||
value="always"
|
||||
>
|
||||
Always
|
||||
</option>
|
||||
<option
|
||||
key="never"
|
||||
value="never"
|
||||
>
|
||||
Never
|
||||
</option>
|
||||
<option
|
||||
key="on_reset"
|
||||
value="on_reset"
|
||||
>
|
||||
On Reset
|
||||
</option>
|
||||
<option
|
||||
key="per_student"
|
||||
value="per_student"
|
||||
>
|
||||
Per Student
|
||||
</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</SettingsOption>
|
||||
`;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import messages from './messages';
|
||||
import { RandomizationTypes } from '../../../../../../../data/constants/problem';
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = {
|
||||
summary: (val) => useState(val),
|
||||
};
|
||||
|
||||
export const useRandomizationSettingStatus = ({ randomization, updateSettings }) => {
|
||||
const [summary, setSummary] = module.state.summary({ message: messages.noRandomizationSummary, values: {} });
|
||||
useEffect(() => {
|
||||
setSummary({
|
||||
message: randomization ? RandomizationTypes[randomization] : messages.noRandomizationSummary,
|
||||
});
|
||||
}, [randomization]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
updateSettings({ randomization: event.target.value });
|
||||
};
|
||||
return { summary, handleChange };
|
||||
};
|
||||
|
||||
export default useRandomizationSettingStatus;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useEffect } from 'react';
|
||||
import { MockUseState } from '../../../../../../../../testUtils';
|
||||
import messages from './messages';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('react', () => {
|
||||
const updateState = jest.fn();
|
||||
return {
|
||||
updateState,
|
||||
useEffect: jest.fn(),
|
||||
useState: jest.fn(val => ([{ state: val }, (newVal) => updateState({ val, newVal })])),
|
||||
};
|
||||
});
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
describe('Problem settings hooks', () => {
|
||||
let output;
|
||||
let updateSettings;
|
||||
let randomization;
|
||||
beforeEach(() => {
|
||||
updateSettings = jest.fn();
|
||||
randomization = 'sOmE_vAlUe';
|
||||
state.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
state.restore();
|
||||
useEffect.mockClear();
|
||||
});
|
||||
describe('Show advanced settings', () => {
|
||||
beforeEach(() => {
|
||||
output = hooks.useRandomizationSettingStatus({ randomization, updateSettings });
|
||||
});
|
||||
test('test default state is false', () => {
|
||||
expect(output.summary).toEqual({ message: messages.noRandomizationSummary, values: {} });
|
||||
});
|
||||
test('test showAdvancedCards sets state to true', () => {
|
||||
const mockEvent = { target: { value: 'sOmE_otheR_ValUe' } };
|
||||
output.handleChange(mockEvent);
|
||||
expect(updateSettings).toHaveBeenCalledWith({ randomization: mockEvent.target.value });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import SettingsOption from '../../SettingsOption';
|
||||
import messages from './messages';
|
||||
import { useRandomizationSettingStatus } from './hooks';
|
||||
import { RandomizationTypesKeys, RandomizationTypes } from '../../../../../../../data/constants/problem';
|
||||
|
||||
export const RandomizationCard = ({
|
||||
randomization,
|
||||
updateSettings,
|
||||
// inject
|
||||
intl,
|
||||
}) => {
|
||||
const { summary, handleChange } = useRandomizationSettingStatus({ randomization, updateSettings });
|
||||
return (
|
||||
<SettingsOption
|
||||
title={intl.formatMessage(messages.randomizationSettingTitle)}
|
||||
summary={intl.formatMessage(summary.message)}
|
||||
none={!randomization}
|
||||
>
|
||||
<div className="halfSpacedMessage">
|
||||
<span>
|
||||
<FormattedMessage {...messages.randomizationSettingText} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={randomization}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{Object.values(RandomizationTypesKeys).map((randomizationType) => (
|
||||
<option
|
||||
key={randomizationType}
|
||||
value={randomizationType}
|
||||
>
|
||||
{intl.formatMessage(RandomizationTypes[randomizationType])}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
|
||||
</SettingsOption>
|
||||
);
|
||||
};
|
||||
|
||||
RandomizationCard.propTypes = {
|
||||
randomization: PropTypes.string.isRequired,
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RandomizationCard);
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { formatMessage } from '../../../../../../../../testUtils';
|
||||
import { RandomizationCard } from './index';
|
||||
import { useRandomizationSettingStatus } from './hooks';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useRandomizationSettingStatus: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('RandomizationCard', () => {
|
||||
const props = {
|
||||
randomization: 'sOmE_vAlUE',
|
||||
updateSettings: jest.fn().mockName('args.updateSettings'),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
|
||||
const randomizationCardHooksProps = {
|
||||
summary: { message: { defaultMessage: 'sUmmary' } },
|
||||
handleChange: jest.fn().mockName('randomizationCardHooks.handleChange'),
|
||||
};
|
||||
|
||||
useRandomizationSettingStatus.mockReturnValue(randomizationCardHooksProps);
|
||||
|
||||
describe('behavior', () => {
|
||||
it(' calls useRandomizationSettingStatus when initialized', () => {
|
||||
shallow(<RandomizationCard {...props} />);
|
||||
expect(useRandomizationSettingStatus).toHaveBeenCalledWith(
|
||||
{ updateSettings: props.updateSettings, randomization: props.randomization },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('snapshot: renders reset true setting card', () => {
|
||||
expect(shallow(<RandomizationCard {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
export const messages = {
|
||||
randomizationSettingTitle: {
|
||||
id: 'authoring.problemeditor.settings.randomization.SettingTitle',
|
||||
defaultMessage: 'Randomization',
|
||||
description: 'Settings Title for Randomization widget',
|
||||
},
|
||||
randomizationSettingText: {
|
||||
id: 'authoring.problemeditor.settings.randomization.SettingText',
|
||||
defaultMessage: 'Defines when to randomize the variables specified in the associated Python script. For problems that do not randomize values, specify "Never".',
|
||||
description: 'Description of Possibilities for value in Randomization widget',
|
||||
},
|
||||
noRandomizationSummary: {
|
||||
id: 'authoring.problemeditor.settings.randomization.noRandomizationSummary',
|
||||
defaultMessage: 'No Python based randomization is present in this problem.',
|
||||
description: 'text shown when no randomization option is given',
|
||||
},
|
||||
};
|
||||
export default messages;
|
||||
@@ -373,6 +373,24 @@ export class OLXParser {
|
||||
return problemType;
|
||||
}
|
||||
|
||||
getGeneralFeedback({ answers, problemType }) {
|
||||
/* Feedback is Generalized for a Problem IFF:
|
||||
1. The problem is of Types: Single Select or Dropdown.
|
||||
2. All the problem's incorrect, if Selected answers are equivalent strings, and there is no other feedback.
|
||||
*/
|
||||
if (problemType === ProblemTypeKeys.SINGLESELECT || problemType === ProblemTypeKeys.DROPDOWN) {
|
||||
const firstIncorrectAnswerText = answers.find(answer => answer.correct === false).selectedFeedback;
|
||||
const isAllIncorrectSelectedFeedbackTheSame = answers.every(answer => (answer.correct
|
||||
? true
|
||||
: answer?.selectedFeedback === firstIncorrectAnswerText
|
||||
));
|
||||
if (isAllIncorrectSelectedFeedbackTheSame) {
|
||||
return firstIncorrectAnswerText;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getParsedOLXData() {
|
||||
if (_.isEmpty(this.problem)) {
|
||||
return {};
|
||||
@@ -410,7 +428,7 @@ export class OLXParser {
|
||||
// if problem is unset, return null
|
||||
return {};
|
||||
}
|
||||
|
||||
const generalFeedback = this.getGeneralFeedback({ answers: answersObject.answers, problemType });
|
||||
if (_.has(answersObject, 'additionalStringAttributes')) {
|
||||
additionalAttributes = { ...answersObject.additionalStringAttributes };
|
||||
}
|
||||
@@ -428,6 +446,7 @@ export class OLXParser {
|
||||
answers,
|
||||
problemType,
|
||||
additionalAttributes,
|
||||
generalFeedback,
|
||||
groupFeedbackList,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,7 +65,21 @@ class ReactStateOLXParser {
|
||||
const choice = [];
|
||||
let compoundhint = [];
|
||||
let widget = {};
|
||||
const { answers } = this.problemState;
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { answers, generalFeedback } = this.problemState;
|
||||
// general feedback replaces selected feedback if all incorrect selected feedback is the same.
|
||||
if (generalFeedback !== ''
|
||||
&& answers.every(
|
||||
answer => (
|
||||
answer.correct
|
||||
? true
|
||||
: answer?.selectedFeedback === answers.find(a => a.correct === false).selectedFeedback
|
||||
),
|
||||
)) {
|
||||
answers = answers.map(answer => (!answer?.correct
|
||||
? { ...answer, selectedFeedback: generalFeedback }
|
||||
: answer));
|
||||
}
|
||||
answers.forEach((answer) => {
|
||||
const feedback = [];
|
||||
let singleAnswer = {};
|
||||
|
||||
@@ -16,6 +16,7 @@ class ReactStateSettingsParser {
|
||||
settings = popuplateItem(settings, 'afterAttempts', 'attempts_before_showanswer_button', stateSettings.showAnswer);
|
||||
settings = popuplateItem(settings, 'showResetButton', 'show_reset_button', stateSettings);
|
||||
settings = popuplateItem(settings, 'timeBetween', 'submission_wait_seconds', stateSettings);
|
||||
settings = popuplateItem(settings, 'randomization', 'rerandomize', stateSettings);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
import { ShowAnswerTypes } from '../../../data/constants/problem';
|
||||
import { ShowAnswerTypes, RandomizationTypesKeys } from '../../../data/constants/problem';
|
||||
|
||||
export const popuplateItem = (parentObject, itemName, statekey, metadata) => {
|
||||
let parent = parentObject;
|
||||
@@ -57,8 +57,13 @@ export const parseSettings = (metadata) => {
|
||||
if (!_.isEmpty(showAnswer)) {
|
||||
settings = { ...settings, showAnswer };
|
||||
}
|
||||
|
||||
const randomizationType = _.get(metadata, 'rerandomize', {});
|
||||
if (!_.isEmpty(randomizationType) && Object.values(RandomizationTypesKeys).includes(randomizationType)) {
|
||||
settings = popuplateItem(settings, 'rerandomize', 'randomization', metadata);
|
||||
}
|
||||
|
||||
settings = popuplateItem(settings, 'show_reset_button', 'showResetButton', metadata);
|
||||
settings = popuplateItem(settings, 'submission_wait_seconds', 'timeBetween', metadata);
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
@@ -184,3 +184,30 @@ export const ShowAnswerTypes = StrictDict({
|
||||
defaultMessage: 'Attempted',
|
||||
},
|
||||
});
|
||||
|
||||
export const RandomizationTypesKeys = StrictDict({
|
||||
ALWAYS: 'always',
|
||||
NEVER: 'never',
|
||||
ONRESET: 'on_reset',
|
||||
PERSTUDENT: 'per_student',
|
||||
});
|
||||
export const RandomizationTypes = StrictDict({
|
||||
[RandomizationTypesKeys.ALWAYS]:
|
||||
{
|
||||
id: 'authoring.problemeditor.settings.RandomizationTypes.always',
|
||||
defaultMessage: 'Always',
|
||||
},
|
||||
[RandomizationTypesKeys.NEVER]:
|
||||
{
|
||||
id: 'authoring.problemeditor.settings.RandomizationTypes.never',
|
||||
defaultMessage: 'Never',
|
||||
},
|
||||
[RandomizationTypesKeys.ONRESET]: {
|
||||
id: 'authoring.problemeditor.settings.RandomizationTypes.onreset',
|
||||
defaultMessage: 'On Reset',
|
||||
},
|
||||
[RandomizationTypesKeys.PERSTUDENT]: {
|
||||
id: 'authoring.problemeditor.settings.RandomizationTypes.perstudent',
|
||||
defaultMessage: 'Per Student',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,8 +12,10 @@ const initialState = {
|
||||
answers: [],
|
||||
correctAnswerCount: 0,
|
||||
groupFeedbackList: [],
|
||||
generalFeedback: '',
|
||||
additionalAttributes: {},
|
||||
settings: {
|
||||
randomization: null,
|
||||
scoring: {
|
||||
weight: 0,
|
||||
attempts: {
|
||||
|
||||
@@ -5,6 +5,8 @@ export const problemState = (state) => state.problem;
|
||||
const mkSimpleSelector = (cb) => createSelector([module.problemState], cb);
|
||||
export const simpleSelectors = {
|
||||
problemType: mkSimpleSelector(problemData => problemData.problemType),
|
||||
generalFeedback: mkSimpleSelector(problemData => problemData.generalFeedback),
|
||||
groupFeedbackList: mkSimpleSelector(problemData => problemData.groupFeedbackList),
|
||||
answers: mkSimpleSelector(problemData => problemData.answers),
|
||||
correctAnswerCount: mkSimpleSelector(problemData => problemData.correctAnswerCount),
|
||||
settings: mkSimpleSelector(problemData => problemData.settings),
|
||||
|
||||
Reference in New Issue
Block a user