From d69d3e1ce7c5c12e5cda9df4f4c3773f3fe6d5a8 Mon Sep 17 00:00:00 2001
From: connorhaugh <49422820+connorhaugh@users.noreply.github.com>
Date: Fri, 10 Feb 2023 08:50:32 -0500
Subject: [PATCH] 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.
---
.../__snapshots__/index.test.jsx.snap | 42 +++--
.../EditProblemView/SettingsWidget/index.jsx | 51 +++++-
.../SettingsWidget/index.test.jsx | 12 ++
.../SettingsWidget/messages.js | 1 -
.../__snapshots__/index.test.jsx.snap | 34 ++++
.../GeneralFeedback/hooks.js | 35 ++++
.../GeneralFeedback/hooks.test.js | 43 +++++
.../GeneralFeedback/index.jsx | 44 +++++
.../GeneralFeedback/index.test.jsx | 39 ++++
.../GeneralFeedback/messages.js | 23 +++
.../GroupFeedback/GroupFeedbackRow.jsx | 76 ++++++++
.../GroupFeedback/GroupFeedbackRow.test.jsx | 22 +++
.../GroupFeedbackRow.test.jsx.snap | 60 +++++++
.../__snapshots__/index.test.jsx.snap | 168 ++++++++++++++++++
.../settingsComponents/GroupFeedback/hooks.js | 95 ++++++++++
.../GroupFeedback/hooks.test.js | 92 ++++++++++
.../GroupFeedback/index.jsx | 65 +++++++
.../GroupFeedback/index.test.jsx | 82 +++++++++
.../GroupFeedback/messages.js | 28 +++
.../__snapshots__/index.test.jsx.snap | 55 ++++++
.../settingsComponents/Randomization/hooks.js | 24 +++
.../Randomization/hooks.test.js | 43 +++++
.../Randomization/index.jsx | 56 ++++++
.../Randomization/index.test.jsx | 39 ++++
.../Randomization/messages.js | 18 ++
.../ProblemEditor/data/OLXParser.js | 21 ++-
.../ProblemEditor/data/ReactStateOLXParser.js | 16 +-
.../data/ReactStateSettingsParser.js | 1 +
.../ProblemEditor/data/SettingsParser.js | 9 +-
src/editors/data/constants/problem.js | 27 +++
src/editors/data/redux/problem/reducers.js | 2 +
src/editors/data/redux/problem/selectors.js | 2 +
32 files changed, 1303 insertions(+), 22 deletions(-)
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/__snapshots__/index.test.jsx.snap
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.js
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.test.js
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/index.jsx
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/index.test.jsx
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/messages.js
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/GroupFeedbackRow.jsx
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/GroupFeedbackRow.test.jsx
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/__snapshots__/GroupFeedbackRow.test.jsx.snap
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/__snapshots__/index.test.jsx.snap
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/hooks.js
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/hooks.test.js
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/index.jsx
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/index.test.jsx
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/messages.js
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/__snapshots__/index.test.jsx.snap
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/hooks.js
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/hooks.test.js
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/index.jsx
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/index.test.jsx
create mode 100644 src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/messages.js
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/__snapshots__/index.test.jsx.snap
index e0cedda10..b32579568 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/__snapshots__/index.test.jsx.snap
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/__snapshots__/index.test.jsx.snap
@@ -7,19 +7,24 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page 1`] = `
-
-
+
-
+
+
+
+
-
+
-
+
-
+
-
@@ -87,19 +92,24 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page advanced
-
-
+
-
+
+
+
+
-
+
-
+
-
+
-
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx
index 109cb92e2..0a61d122b 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx
@@ -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 (
+
+
+ );
+ }
+ return (
+
+
+ );
+ };
+
return (
@@ -52,7 +82,7 @@ export const SettingsWidget = ({
-
+ {feedbackCard()}
@@ -80,6 +110,13 @@ export const SettingsWidget = ({
+ {
+ problemType === ProblemTypeKeys.ADVANCED && (
+
+
+
+ )
+ }
@@ -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),
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.jsx
index d2daba09b..8d1ea256f 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.jsx
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.test.jsx
@@ -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,
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js
index 09a6007cf..8fa7dfaed 100644
--- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/messages.js
@@ -205,5 +205,4 @@ export const messages = {
description: 'Solution Explanation text',
},
};
-
export default messages;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/__snapshots__/index.test.jsx.snap
new file mode 100644
index 000000000..dd2179a6b
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RandomizationCard snapshot snapshot: renders general feedback setting card 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.js
new file mode 100644
index 000000000..813d2b48f
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.js
@@ -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,
+ };
+};
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.test.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.test.js
new file mode 100644
index 000000000..0cd35cfee
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/hooks.test.js
@@ -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 });
+ });
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/index.jsx
new file mode 100644
index 000000000..168cd2e6c
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/index.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+GeneralFeedbackCard.propTypes = {
+ generalFeedback: PropTypes.string.isRequired,
+ updateSettings: PropTypes.func.isRequired,
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(GeneralFeedbackCard);
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/index.test.jsx
new file mode 100644
index 000000000..572ce2034
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/index.test.jsx
@@ -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();
+ expect(generalFeedbackHooks).toHaveBeenCalledWith(
+ props.generalFeedback, props.updateSettings,
+ );
+ });
+ });
+
+ describe('snapshot', () => {
+ test('snapshot: renders general feedback setting card', () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/messages.js
new file mode 100644
index 000000000..a0fd27af2
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GeneralFeedback/messages.js
@@ -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;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/GroupFeedbackRow.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/GroupFeedbackRow.jsx
new file mode 100644
index 000000000..6f7016573
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/GroupFeedbackRow.jsx
@@ -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,
+}) => (
+
+
+
+
+
+
+
+
+
+
+ {answers.map((letter) => (
+ {letter.id}
+
+ ))}
+
+
+
+
+);
+
+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);
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/GroupFeedbackRow.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/GroupFeedbackRow.test.jsx
new file mode 100644
index 000000000..9cd2c5efb
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/GroupFeedbackRow.test.jsx
@@ -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()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/__snapshots__/GroupFeedbackRow.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/__snapshots__/GroupFeedbackRow.test.jsx.snap
new file mode 100644
index 000000000..19fecd6e9
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/__snapshots__/GroupFeedbackRow.test.jsx.snap
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GroupFeedbackRow snapshot snapshot: renders hints row 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/__snapshots__/index.test.jsx.snap
new file mode 100644
index 000000000..4fe5b19af
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,168 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`HintsCard snapshot snapshot: renders groupFeedbacks setting card multiple groupFeedbacks 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`HintsCard snapshot snapshot: renders groupFeedbacks setting card no groupFeedbacks 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`HintsCard snapshot snapshot: renders groupFeedbacks setting card one groupFeedback 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/hooks.js
new file mode 100644
index 000000000..3824e53fd
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/hooks.js
@@ -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,
+ };
+};
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/hooks.test.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/hooks.test.js
new file mode 100644
index 000000000..553d6b8f1
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/hooks.test.js
@@ -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: [] },
+ );
+ });
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/index.jsx
new file mode 100644
index 000000000..7c8d08f8c
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/index.jsx
@@ -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 (
+
+
+
+
+ {groupFeedbacks.map((groupFeedback) => (
+
+ ))}
+
+
+ );
+};
+
+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);
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/index.test.jsx
new file mode 100644
index 000000000..118529d23
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/index.test.jsx
@@ -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();
+ 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()).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()).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()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/messages.js
new file mode 100644
index 000000000..454f312e8
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/GroupFeedback/messages.js
@@ -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;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/__snapshots__/index.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/__snapshots__/index.test.jsx.snap
new file mode 100644
index 000000000..5b5ced5d8
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,55 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RandomizationCard snapshot snapshot: renders reset true setting card 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/hooks.js
new file mode 100644
index 000000000..ed875f2c4
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/hooks.js
@@ -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;
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/hooks.test.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/hooks.test.js
new file mode 100644
index 000000000..7999c56f9
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/hooks.test.js
@@ -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 });
+ });
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/index.jsx
new file mode 100644
index 000000000..ff590a8ff
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/index.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ {Object.values(RandomizationTypesKeys).map((randomizationType) => (
+
+ ))}
+
+
+
+
+ );
+};
+
+RandomizationCard.propTypes = {
+ randomization: PropTypes.string.isRequired,
+ updateSettings: PropTypes.func.isRequired,
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(RandomizationCard);
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/index.test.jsx
new file mode 100644
index 000000000..f028ca012
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/index.test.jsx
@@ -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();
+ expect(useRandomizationSettingStatus).toHaveBeenCalledWith(
+ { updateSettings: props.updateSettings, randomization: props.randomization },
+ );
+ });
+ });
+
+ describe('snapshot', () => {
+ test('snapshot: renders reset true setting card', () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/messages.js
new file mode 100644
index 000000000..52e980519
--- /dev/null
+++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/Randomization/messages.js
@@ -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;
diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.js b/src/editors/containers/ProblemEditor/data/OLXParser.js
index 44d70e73a..3e793d448 100644
--- a/src/editors/containers/ProblemEditor/data/OLXParser.js
+++ b/src/editors/containers/ProblemEditor/data/OLXParser.js
@@ -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,
};
}
diff --git a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js
index 0f7681335..e818fdcc8 100644
--- a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js
+++ b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js
@@ -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 = {};
diff --git a/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js b/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js
index e022bcb7c..05a8f25c7 100644
--- a/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js
+++ b/src/editors/containers/ProblemEditor/data/ReactStateSettingsParser.js
@@ -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;
}
diff --git a/src/editors/containers/ProblemEditor/data/SettingsParser.js b/src/editors/containers/ProblemEditor/data/SettingsParser.js
index 1de0708a0..8e6d99bab 100644
--- a/src/editors/containers/ProblemEditor/data/SettingsParser.js
+++ b/src/editors/containers/ProblemEditor/data/SettingsParser.js
@@ -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;
};
diff --git a/src/editors/data/constants/problem.js b/src/editors/data/constants/problem.js
index 6a4623882..392a200d6 100644
--- a/src/editors/data/constants/problem.js
+++ b/src/editors/data/constants/problem.js
@@ -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',
+ },
+});
diff --git a/src/editors/data/redux/problem/reducers.js b/src/editors/data/redux/problem/reducers.js
index 9c3d7afd1..0849df4ad 100644
--- a/src/editors/data/redux/problem/reducers.js
+++ b/src/editors/data/redux/problem/reducers.js
@@ -12,8 +12,10 @@ const initialState = {
answers: [],
correctAnswerCount: 0,
groupFeedbackList: [],
+ generalFeedback: '',
additionalAttributes: {},
settings: {
+ randomization: null,
scoring: {
weight: 0,
attempts: {
diff --git a/src/editors/data/redux/problem/selectors.js b/src/editors/data/redux/problem/selectors.js
index a9c267600..bb77a1fa6 100644
--- a/src/editors/data/redux/problem/selectors.js
+++ b/src/editors/data/redux/problem/selectors.js
@@ -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),