feat:problem editor

Co-authored-by: Farhaan Bukhsh <farhaan@opencraft.com>
Co-authored-by: Navin Karkera <navin@disroot.org>
Co-authored-by: Kaustav Banerjee <kaustav@opencraft.com>
This commit is contained in:
connorhaugh
2022-12-20 14:52:20 -05:00
committed by GitHub
parent 6f82e87574
commit 8dea72de99
96 changed files with 58659 additions and 69792 deletions

View File

@@ -8,6 +8,7 @@ const config = createConfig('eslint', {
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
'react-hooks/rules-of-hooks': 'off',
'react-hooks/exhaustive-deps': 'off',
'radix': 'off',
},
});

View File

@@ -52,7 +52,7 @@ This guide presumes you have a functioning devstack.
4. Open a terminal
1. `cd ${DEVSTACK_WORKSPACE}/src/frontend-lib-content-components`
1. run `$ npm install`
2. run `$ npm build` when you want to see your changes.
2. run `$ make build` when you want to see your changes.
5. In devstack run `make studio-static` followed by `$ make dev.down.frontend-app-course-authoring` and `$ make dev.up.frontend-app-course-authoring`.

73727
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -53,7 +53,8 @@
"react-router-dom": "5.3.0",
"react-test-renderer": "16.14.0",
"reactifex": "1.1.1",
"redux-saga": "1.1.3"
"redux-saga": "1.1.3",
"webpack-cli": "4.10.0"
},
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
@@ -64,6 +65,7 @@
"babel-polyfill": "6.26.0",
"canvas": "^2.10.1",
"codemirror": "^6.0.0",
"fast-xml-parser": "^4.0.10",
"lodash-es": "^4.17.21",
"react-redux": "^7.2.8",
"react-responsive": "8.2.0",

View File

@@ -12,7 +12,7 @@ jest.mock('./hooks', () => ({
jest.mock('./containers/TextEditor', () => 'TextEditor');
jest.mock('./containers/VideoEditor', () => 'VideoEditor');
jest.mock('./containers/ProblemEditor/ProblemEditor', () => 'ProblemEditor');
jest.mock('./containers/ProblemEditor', () => 'ProblemEditor');
const initData = {
blockId: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',

View File

@@ -1,9 +0,0 @@
import React from 'react';
export default function ProblemEditor() {
return (
<div className="problem-editor">
<span>Problem</span>
</div>
);
}

View File

@@ -1,8 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import ProblemEditor from './ProblemEditor';
test('Videoeditor: Basic Render', () => {
render(<ProblemEditor />);
expect(screen.findByText('Problem')).toBeTruthy();
});

View File

@@ -0,0 +1,188 @@
import React, { memo } from 'react';
import { connect, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
Col, Collapsible, Icon, IconButton, Form, Row,
} from '@edx/paragon';
import { AddComment, Delete } from '@edx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { selectors } from '../../../../../data/redux';
import { answerOptionProps } from '../../../../../data/services/cms/types';
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
import * as hooks from './hooks';
const Checker = ({
hasSingleAnswer, answer, setAnswer,
}) => {
let CheckerType = Form.Checkbox;
if (hasSingleAnswer) {
CheckerType = Form.Radio;
}
return (
<CheckerType
className="pl-4 mt-3"
value={answer.id}
onChange={(e) => setAnswer({ correct: e.target.checked })}
checked={answer.correct}
>
{answer.id}
</CheckerType>
);
};
const FeedbackControl = ({
feedback, onChange, labelMessage, labelMessageBoldUnderline, key, answer, intl,
}) => (
<Form.Group key={key}>
<Form.Label className="mb-3">
<FormattedMessage
{...labelMessage}
values={{
answerId: answer.id,
boldunderline: <b><u><FormattedMessage {...labelMessageBoldUnderline} /></u></b>,
}}
/>
</Form.Label>
<Form.Control
placeholder={intl.formatMessage(messages.feedbackPlaceholder)}
value={feedback}
onChange={onChange}
/>
</Form.Group>
);
export const AnswerOption = ({
answer,
hasSingleAnswer,
// injected
intl,
// redux
problemType,
}) => {
const dispatch = useDispatch();
const removeAnswer = hooks.removeAnswer({ answer, dispatch });
const setAnswer = hooks.setAnswer({ answer, hasSingleAnswer, dispatch });
const { isFeedbackVisible, toggleFeedback } = hooks.prepareFeedback(answer);
const displayFeedbackControl = (answerObject) => {
if (problemType !== ProblemTypeKeys.MULTISELECT) {
return FeedbackControl({
key: `feedback-${answerObject.id}`,
feedback: answerObject.feedback,
onChange: (e) => setAnswer({ feedback: e.target.value }),
labelMessage: messages.selectedFeedbackLabel,
labelMessageBoldUnderline: messages.selectedFeedbackLabelBoldUnderlineText,
answer: answerObject,
intl,
});
}
return [
FeedbackControl({
key: `selectedfeedback-${answerObject.id}`,
feedback: answerObject.selectedFeedback,
onChange: (e) => setAnswer({ selectedFeedback: e.target.value }),
labelMessage: messages.selectedFeedbackLabel,
labelMessageBoldUnderline: messages.selectedFeedbackLabelBoldUnderlineText,
answer: answerObject,
intl,
}),
FeedbackControl({
key: `unselectedfeedback-${answerObject.id}`,
feedback: answerObject.unselectedFeedback,
onChange: (e) => setAnswer({ unselectedFeedback: e.target.value }),
labelMessage: messages.unSelectedFeedbackLabel,
labelMessageBoldUnderline: messages.unSelectedFeedbackLabelBoldUnderlineText,
answer: answerObject,
intl,
}),
];
};
return (
<Collapsible.Advanced
open={isFeedbackVisible}
onToggle={toggleFeedback}
className="collapsible-card"
>
<Row className="my-2">
<Col xs={1}>
<Checker
hasSingleAnswer={hasSingleAnswer}
answer={answer}
setAnswer={setAnswer}
/>
</Col>
<Col xs={10}>
<Form.Control
as="textarea"
rows={1}
value={answer.title}
onChange={(e) => { setAnswer({ title: e.target.value }); }}
placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
/>
<Collapsible.Body>
<div className="bg-dark-100 p-4 mt-3">
{displayFeedbackControl(answer)}
</div>
</Collapsible.Body>
</Col>
<Col xs={1} className="d-inline-flex mt-1">
<Collapsible.Trigger>
<IconButton
src={AddComment}
iconAs={Icon}
alt={intl.formatMessage(messages.feedbackToggleIconAltText)}
variant="primary"
/>
</Collapsible.Trigger>
<IconButton
src={Delete}
iconAs={Icon}
alt={intl.formatMessage(messages.answerDeleteIconAltText)}
onClick={removeAnswer}
variant="primary"
/>
</Col>
</Row>
</Collapsible.Advanced>
);
};
AnswerOption.propTypes = {
answer: answerOptionProps.isRequired,
hasSingleAnswer: PropTypes.bool.isRequired,
// injected
intl: intlShape.isRequired,
// redux
problemType: PropTypes.string.isRequired,
};
FeedbackControl.propTypes = {
feedback: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
labelMessage: PropTypes.string.isRequired,
labelMessageBoldUnderline: PropTypes.string.isRequired,
key: PropTypes.string.isRequired,
answer: answerOptionProps.isRequired,
intl: intlShape.isRequired,
};
Checker.propTypes = {
hasSingleAnswer: PropTypes.bool.isRequired,
answer: answerOptionProps.isRequired,
setAnswer: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
problemType: selectors.problem.problemType(state),
});
export const mapDispatchToProps = {};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(memo(AnswerOption)));

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../testUtils';
import { AnswerOption } from './AnswerOption';
describe('AnswerOption', () => {
const answerWithOnlyFeedback = {
id: 'A',
title: 'Answer 1',
correct: true,
feedback: 'some feedback',
};
const answerWithSelectedUnselectedFeedback = {
id: 'A',
title: 'Answer 1',
correct: true,
selectedFeedback: 'selected feedback',
unselectedFeedback: 'unselected feedback',
};
const props = {
hasSingleAnswer: false,
answer: answerWithOnlyFeedback,
// inject
intl: { formatMessage },
deleteAnswer: jest.fn(),
updateAnswer: jest.fn(),
};
describe('render', () => {
test('snapshot: renders correct option with feedback', () => {
expect(shallow(<AnswerOption {...props} />)).toMatchSnapshot();
});
test('snapshot: renders correct option with selected unselected feedback', () => {
expect(shallow(<AnswerOption {...props} answer={answerWithSelectedUnselectedFeedback} />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
import { Add } from '@edx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { initializeAnswerContainer } from '../../../hooks';
import { actions, selectors } from '../../../../../data/redux';
import { answerOptionProps } from '../../../../../data/services/cms/types';
import AnswerOption from './AnswerOption';
export const AnswersContainer = ({
problemType,
// Redux
answers,
addAnswer,
}) => {
const { hasSingleAnswer } = initializeAnswerContainer(problemType);
return (
<div>
{answers.map((answer) => (
<AnswerOption
key={answer.id}
hasSingleAnswer={hasSingleAnswer}
answer={answer}
/>
))}
<Button
className="my-3 ml-2"
iconBefore={Add}
variant="tertiary"
onClick={addAnswer}
>
<FormattedMessage {...messages.addAnswerButtonText} />
</Button>
</div>
);
};
AnswersContainer.propTypes = {
problemType: PropTypes.string.isRequired,
answers: PropTypes.arrayOf(answerOptionProps).isRequired,
addAnswer: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
answers: selectors.problem.answers(state),
});
export const mapDispatchToProps = {
addAnswer: actions.problem.addAnswer,
};
export default connect(mapStateToProps, mapDispatchToProps)(AnswersContainer);

View File

@@ -0,0 +1,193 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnswerOption render snapshot: renders correct option with feedback 1`] = `
<Advanced
className="collapsible-card"
onToggle={[Function]}
open={false}
>
<Row
className="my-2"
>
<Component
xs={1}
>
<Checker
answer={
Object {
"correct": true,
"feedback": "some feedback",
"id": "A",
"title": "Answer 1",
}
}
hasSingleAnswer={false}
setAnswer={[Function]}
/>
</Component>
<Component
xs={10}
>
<Form.Control
as="textarea"
onChange={[Function]}
placeholder="Enter an answer"
rows={1}
value="Answer 1"
/>
<Body>
<div
className="bg-dark-100 p-4 mt-3"
>
<Form.Group
key="feedback-A"
>
<Form.Label
className="mb-3"
>
<FormattedMessage
defaultMessage="Show following feedback when {answerId} {boldunderline}:"
description="Label text for feedback if option is selected"
id="authoring.answerwidget.feedback.selected.label"
values={
Object {
"answerId": "A",
"boldunderline": <b>
<u>
<FormattedMessage
defaultMessage="is selected"
description="Bold & underlined text for feedback if option is selected"
id="authoring.answerwidget.feedback.selected.label.boldunderline"
/>
</u>
</b>,
}
}
/>
</Form.Label>
<Form.Control
onChange={[Function]}
placeholder="Feedback message"
value="some feedback"
/>
</Form.Group>
</div>
</Body>
</Component>
<Component
className="d-inline-flex mt-1"
xs={1}
>
<Trigger>
<IconButton
alt="Toggle feedback"
iconAs="Icon"
variant="primary"
/>
</Trigger>
<IconButton
alt="Delete answer"
iconAs="Icon"
onClick={[Function]}
variant="primary"
/>
</Component>
</Row>
</Advanced>
`;
exports[`AnswerOption render snapshot: renders correct option with selected unselected feedback 1`] = `
<Advanced
className="collapsible-card"
onToggle={[Function]}
open={false}
>
<Row
className="my-2"
>
<Component
xs={1}
>
<Checker
answer={
Object {
"correct": true,
"id": "A",
"selectedFeedback": "selected feedback",
"title": "Answer 1",
"unselectedFeedback": "unselected feedback",
}
}
hasSingleAnswer={false}
setAnswer={[Function]}
/>
</Component>
<Component
xs={10}
>
<Form.Control
as="textarea"
onChange={[Function]}
placeholder="Enter an answer"
rows={1}
value="Answer 1"
/>
<Body>
<div
className="bg-dark-100 p-4 mt-3"
>
<Form.Group
key="feedback-A"
>
<Form.Label
className="mb-3"
>
<FormattedMessage
defaultMessage="Show following feedback when {answerId} {boldunderline}:"
description="Label text for feedback if option is selected"
id="authoring.answerwidget.feedback.selected.label"
values={
Object {
"answerId": "A",
"boldunderline": <b>
<u>
<FormattedMessage
defaultMessage="is selected"
description="Bold & underlined text for feedback if option is selected"
id="authoring.answerwidget.feedback.selected.label.boldunderline"
/>
</u>
</b>,
}
}
/>
</Form.Label>
<Form.Control
onChange={[Function]}
placeholder="Feedback message"
/>
</Form.Group>
</div>
</Body>
</Component>
<Component
className="d-inline-flex mt-1"
xs={1}
>
<Trigger>
<IconButton
alt="Toggle feedback"
iconAs="Icon"
variant="primary"
/>
</Trigger>
<IconButton
alt="Delete answer"
iconAs="Icon"
onClick={[Function]}
variant="primary"
/>
</Component>
</Row>
</Advanced>
`;

View File

@@ -0,0 +1,55 @@
import { useEffect } from 'react';
import { MockUseState } from '../../../../../../testUtils';
import * as module from './hooks';
jest.mock('react', () => {
const updateState = jest.fn();
return {
updateState,
useEffect: jest.fn(),
useState: jest.fn(val => ([{ state: val }, (newVal) => updateState({ val, newVal })])),
};
});
jest.mock('../../../../../data/redux', () => ({
actions: {
problem: {
deleteAnswer: (args) => ({ deleteAnswer: args }),
updateAnswer: (args) => ({ updateAnswer: args }),
},
},
}));
const state = new MockUseState(module);
let output;
const answerWithOnlyFeedback = {
id: 'A',
title: 'Answer 1',
correct: true,
feedback: 'some feedback',
};
describe('Answer Options Hooks', () => {
beforeEach(() => { jest.clearAllMocks(); });
describe('state hooks', () => {
state.testGetter(state.keys.isFeedbackVisible);
});
describe('prepareFeedback hook', () => {
beforeEach(() => { state.mock(); });
afterEach(() => { state.restore(); });
test('test default state is false', () => {
output = module.prepareFeedback(answerWithOnlyFeedback);
expect(output.isFeedbackVisible).toBeFalsy();
});
test('when useEffect triggers, isFeedbackVisible is set to true', () => {
const key = state.keys.isFeedbackVisible;
output = module.prepareFeedback(answerWithOnlyFeedback);
expect(state.setState[key]).not.toHaveBeenCalled();
const [cb, prereqs] = useEffect.mock.calls[0];
expect(prereqs[0]).toStrictEqual(answerWithOnlyFeedback);
cb();
expect(state.setState[key]).toHaveBeenCalledWith(true);
});
});
});

View File

@@ -0,0 +1,42 @@
import { useState, useEffect } from 'react';
import { StrictDict } from '../../../../../utils';
import * as module from './hooks';
import { actions } from '../../../../../data/redux';
export const state = StrictDict({
isFeedbackVisible: (val) => useState(val),
});
export const removeAnswer = ({ answer, dispatch }) => () => {
dispatch(actions.problem.deleteAnswer({ id: answer.id }));
};
export const setAnswer = ({ answer, hasSingleAnswer, dispatch }) => (payload) => {
dispatch(actions.problem.updateAnswer({ id: answer.id, hasSingleAnswer, ...payload }));
};
export const prepareFeedback = (answer) => {
const [isFeedbackVisible, setIsFeedbackVisible] = module.state.isFeedbackVisible(false);
useEffect(() => {
// Show feedback fields if feedback is present
const isVisible = !!answer.selectedFeedback || !!answer.unselectedFeedback || !!answer.feedback;
setIsFeedbackVisible(isVisible);
}, [answer]);
const toggleFeedback = (open) => {
// Do not allow to hide if feedback is added
if (!!answer.selectedFeedback || !!answer.unselectedFeedback || !!answer.feedback) {
setIsFeedbackVisible(true);
return;
}
setIsFeedbackVisible(open);
};
return {
isFeedbackVisible,
toggleFeedback,
};
};
export default {
state, removeAnswer, setAnswer, prepareFeedback,
};

View File

@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { ProblemTypes } from '../../../../../data/constants/problem';
import AnswersContainer from './AnswersContainer';
import './index.scss';
// This widget should be connected, grab all answers from store, update them as needed.
const AnswerWidget = ({
// Redux
problemType,
}) => {
const problemStaticData = ProblemTypes[problemType];
return (
<div>
<div className="problem-answer">
<div className="problem-answer-title">
<FormattedMessage {...messages.answerWidgetTitle} />
</div>
<div className="problem-answer-description">
{problemStaticData.description}
</div>
</div>
<AnswersContainer problemType={problemType} />
</div>
);
};
AnswerWidget.propTypes = {
problemType: PropTypes.string.isRequired,
};
export default AnswerWidget;

View File

@@ -0,0 +1,11 @@
.problem-answer {
padding: 12px;
color: #00262B;
.problem-answer-title {
font-weight: bold;
}
.problem-answer-description {
font-size: 0.9rem;
}
}

View File

@@ -0,0 +1,54 @@
export const messages = {
answerWidgetTitle: {
id: 'authoring.answerwidget.answer.answerWidgetTitle',
defaultMessage: 'Answers',
description: 'Main title for Answers widget',
},
addAnswerButtonText: {
id: 'authoring.answerwidget.answer.addAnswerButton',
defaultMessage: 'Add answer',
description: 'Button text to add answer',
},
answerTextboxPlaceholder: {
id: 'authoring.answerwidget.answer.placeholder',
defaultMessage: 'Enter an answer',
description: 'Placeholder text for answer option text',
},
feedbackPlaceholder: {
id: 'authoring.answerwidget.feedback.placeholder',
defaultMessage: 'Feedback message',
description: 'Placeholder text for feedback text',
},
feedbackToggleIconAltText: {
id: 'authoring.answerwidget.feedback.icon.alt',
defaultMessage: 'Toggle feedback',
description: 'Alt text for feedback toggle icon',
},
answerDeleteIconAltText: {
id: 'authoring.answerwidget.answer.delete.icon.alt',
defaultMessage: 'Delete answer',
description: 'Alt text for delete icon',
},
selectedFeedbackLabel: {
id: 'authoring.answerwidget.feedback.selected.label',
defaultMessage: 'Show following feedback when {answerId} {boldunderline}:',
description: 'Label text for feedback if option is selected',
},
selectedFeedbackLabelBoldUnderlineText: {
id: 'authoring.answerwidget.feedback.selected.label.boldunderline',
defaultMessage: 'is selected',
description: 'Bold & underlined text for feedback if option is selected',
},
unSelectedFeedbackLabel: {
id: 'authoring.answerwidget.feedback.unselected.label',
defaultMessage: 'Show following feedback when {answerId} {boldunderline}:',
description: 'Label text for feedback if option is not selected',
},
unSelectedFeedbackLabelBoldUnderlineText: {
id: 'authoring.answerwidget.feedback.unselected.label.boldunderline',
defaultMessage: 'is not selected',
description: 'Bold & underlined text for feedback if option is not selected',
},
};
export default messages;

View File

@@ -0,0 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Editor } from '@tinymce/tinymce-react';
import { connect } from 'react-redux';
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import * as hooks from '../../../hooks';
import { selectors, actions } from '../../../../../data/redux';
import { messages } from './messages';
// This widget should be connected, grab all questions from store, update them as needed.
export const QuestionWidget = ({
question,
updateQuestion,
}) => {
const { editorRef, refReady, setEditorRef } = hooks.prepareEditorRef();
if (!refReady) { return null; }
return (
<div>
<div>
<h1>
<FormattedMessage {...messages.questionWidgetTitle} />
</h1>
<Editor {
...hooks.problemEditorConfig({
setEditorRef,
editorRef,
question,
updateQuestion,
})
}
/>
</div>
</div>
);
};
QuestionWidget.propTypes = {
question: PropTypes.string.isRequired,
updateQuestion: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
question: selectors.problem.question(state),
});
export const mapDispatchToProps = {
updateQuestion: actions.problem.updateQuestion,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(QuestionWidget));

View File

@@ -0,0 +1,9 @@
export const messages = {
questionWidgetTitle: {
id: 'authoring.questionwidget.question.questionWidgetTitle',
defaultMessage: 'Question',
description: 'Question Title',
},
};
export default messages;

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { Collapsible, Icon, Card } from '@edx/paragon';
import { KeyboardArrowUp, KeyboardArrowDown } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import { showFullCard } from './hooks';
export const SettingsOption = ({
title,
summary,
children,
}) => {
const { isCardCollapsed, toggleCardCollapse } = showFullCard();
return (
<Card>
<Card.Section className="settingsCardTitleSection">
<Collapsible.Advanced
open={isCardCollapsed}
onToggle={toggleCardCollapse}
>
<Collapsible.Trigger className="collapsible-trigger d-flex">
<span className="flex-grow-1">{title}</span>
<Collapsible.Visible whenClosed>
<Icon src={KeyboardArrowDown} />
</Collapsible.Visible>
<Collapsible.Visible whenOpen>
<Icon src={KeyboardArrowUp} />
</Collapsible.Visible>
</Collapsible.Trigger>
</Collapsible.Advanced>
</Card.Section>
<Card.Section>
<Collapsible.Advanced
open={!isCardCollapsed}
>
<Collapsible.Body className="collapsible-body">
<span>{summary}</span>
</Collapsible.Body>
</Collapsible.Advanced>
<Collapsible.Advanced
open={isCardCollapsed}
>
<Collapsible.Body className="collapsible-body">
{children}
</Collapsible.Body>
</Collapsible.Advanced>
</Card.Section>
</Card>
);
};
SettingsOption.propTypes = {
title: PropTypes.string.isRequired,
summary: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
export default SettingsOption;

View File

@@ -0,0 +1,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import SettingsOption from './SettingsOption';
describe('SettingsOption', () => {
describe('render', () => {
const testContent = (<h1>My test content</h1>);
test('snapshot: renders correct', () => {
expect(shallow(<SettingsOption title="Settings Option Title" summary="Settings Option Summary">{testContent}</SettingsOption>)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SettingsOption render snapshot: renders correct 1`] = `
<Card>
<Card.Section
className="settingsCardTitleSection"
>
<Advanced
onToggle={[Function]}
open={false}
>
<Trigger
className="collapsible-trigger d-flex"
>
<span
className="flex-grow-1"
>
Settings Option Title
</span>
<Visible
whenClosed={true}
>
<Icon />
</Visible>
<Visible
whenOpen={true}
>
<Icon />
</Visible>
</Trigger>
</Advanced>
</Card.Section>
<Card.Section>
<Advanced
open={true}
>
<Body
className="collapsible-body"
>
<span>
Settings Option Summary
</span>
</Body>
</Advanced>
<Advanced
open={false}
>
<Body
className="collapsible-body"
>
<h1>
My test content
</h1>
</Body>
</Advanced>
</Card.Section>
</Card>
`;

View File

@@ -0,0 +1,173 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SettingsWidget snapshot snapshot: renders Settings widget page 1`] = `
<div>
<div>
<h3>
<FormattedMessage
defaultMessage="Settings"
description="Settings Title"
id="authoring.problemeditor.settings.settingsWidgetTitle"
/>
</h3>
<Container>
<Row>
<Component>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent)
problemType="stringresponse"
/>
</Row>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row
className="mt-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row>
<Advanced
open={true}
>
<Body
className="collapsible-body"
>
<Button
className="my-3 ml-2"
size="inline"
variant="link"
>
<FormattedMessage
defaultMessage="Show advanced settings"
description="Button text to show advanced settings"
id="authoring.problemeditor.settings.showAdvancedButton"
/>
</Button>
</Body>
</Advanced>
</Row>
<Advanced
open={false}
>
<Body
className="collapsible-body"
>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
</Body>
</Advanced>
</Component>
</Row>
</Container>
</div>
</div>
`;
exports[`SettingsWidget snapshot snapshot: renders Settings widget page advanced settings visible 1`] = `
<div>
<div>
<h3>
<FormattedMessage
defaultMessage="Settings"
description="Settings Title"
id="authoring.problemeditor.settings.settingsWidgetTitle"
/>
</h3>
<Container>
<Row>
<Component>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent)
problemType="stringresponse"
/>
</Row>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row
className="mt-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row>
<Advanced
open={false}
>
<Body
className="collapsible-body"
>
<Button
className="my-3 ml-2"
size="inline"
variant="link"
>
<FormattedMessage
defaultMessage="Show advanced settings"
description="Button text to show advanced settings"
id="authoring.problemeditor.settings.showAdvancedButton"
/>
</Button>
</Body>
</Advanced>
</Row>
<Advanced
open={true}
>
<Body
className="collapsible-body"
>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
<Row
className="my-2"
>
<injectIntl(ShimmedIntlComponent) />
</Row>
</Body>
</Advanced>
</Component>
</Row>
</Container>
</div>
</div>
`;

View File

@@ -0,0 +1,193 @@
import { useState, useEffect } from 'react';
import _ from 'lodash-es';
import * as module from './hooks';
import messages from './messages';
import { ShowAnswerTypesKeys } from '../../../../../data/constants/problem';
export const state = {
showAdvanced: (val) => useState(val),
cardCollapsed: (val) => useState(val),
summary: (val) => useState(val),
showAttempts: (val) => useState(val),
};
export const showAdvancedSettingsCards = () => {
const [isAdvancedCardsVisible, setIsAdvancedCardsVisible] = module.state.showAdvanced(false);
return {
isAdvancedCardsVisible,
showAdvancedCards: () => setIsAdvancedCardsVisible(true),
};
};
export const showFullCard = () => {
const [isCardCollapsed, setIsCardCollapsed] = module.state.cardCollapsed(false);
return {
isCardCollapsed,
toggleCardCollapse: () => setIsCardCollapsed(!isCardCollapsed),
};
};
export const hintsCardHooks = (hints, updateSettings) => {
const [summary, setSummary] = module.state.summary({ message: messages.noHintSummary, values: {} });
useEffect(() => {
const hintsNumber = hints.length;
if (hintsNumber === 0) {
setSummary({ message: messages.noHintSummary, values: {} });
} else {
setSummary({ message: messages.hintSummary, values: { hint: hints[0].value, count: (hintsNumber - 1) } });
}
}, [hints]);
const handleAdd = () => {
let newId = 0;
if (!_.isEmpty(hints)) {
newId = Math.max(...hints.map(hint => hint.id)) + 1;
}
const hint = { id: newId, value: '' };
const modifiedHints = [...hints, hint];
updateSettings({ hints: modifiedHints });
};
return {
summary,
handleAdd,
};
};
export const hintsRowHooks = (id, hints, updateSettings) => {
const handleChange = (event) => {
const { value } = event.target;
const modifiedHints = hints.map(hint => {
if (hint.id === id) {
return { ...hint, value };
}
return hint;
});
updateSettings({ hints: modifiedHints });
};
const handleDelete = () => {
const modifiedHints = hints.filter((hint) => (hint.id !== id));
updateSettings({ hints: modifiedHints });
};
return {
handleChange,
handleDelete,
};
};
export const matlabCardHooks = (matLabApiKey, updateSettings) => {
const [summary, setSummary] = module.state.summary({ message: '', values: {}, intl: false });
useEffect(() => {
if (_.isEmpty(matLabApiKey)) {
setSummary({ message: messages.matlabNoKeySummary, values: {}, intl: true });
} else {
setSummary({ message: matLabApiKey, values: {}, intl: false });
}
}, [matLabApiKey]);
const handleChange = (event) => {
updateSettings({ matLabApiKey: event.target.value });
};
return {
summary,
handleChange,
};
};
export const resetCardHooks = (updateSettings) => {
const setReset = (value) => {
updateSettings({ showResetButton: value });
};
return {
setResetTrue: () => setReset(true),
setResetFalse: () => setReset(false),
};
};
export const scoringCardHooks = (scoring, updateSettings) => {
const handleMaxAttemptChange = (event) => {
let unlimitedAttempts = true;
let attemptNumber = parseInt(event.target.value);
if (_.isNaN(attemptNumber)) {
attemptNumber = 0;
}
if (attemptNumber > 0) {
unlimitedAttempts = false;
}
updateSettings({ scoring: { ...scoring, attempts: { number: attemptNumber, unlimited: unlimitedAttempts } } });
};
const handleWeightChange = (event) => {
let weight = parseFloat(event.target.value);
if (_.isNaN(weight)) {
weight = 0;
}
updateSettings({ scoring: { ...scoring, weight } });
};
return {
handleMaxAttemptChange,
handleWeightChange,
};
};
export const showAnswerCardHooks = (showAnswer, updateSettings) => {
const [showAttempts, setShowAttempts] = module.state.showAttempts(false);
const numberOfAttemptsChoice = [
ShowAnswerTypesKeys.AFTER_SOME_NUMBER_OF_ATTEMPTS,
ShowAnswerTypesKeys.AFTER_ALL_ATTEMPTS,
ShowAnswerTypesKeys.AFTER_ALL_ATTEMPTS_OR_CORRECT,
];
useEffect(() => {
setShowAttempts(_.includes(numberOfAttemptsChoice, showAnswer.on));
}, [showAttempts]);
const handleShowAnswerChange = (event) => {
const { value } = event.target;
setShowAttempts(_.includes(numberOfAttemptsChoice, value));
updateSettings({ showAnswer: { ...showAnswer, on: value } });
};
const handleAttemptsChange = (event) => {
let attempts = parseInt(event.target.value);
if (_.isNaN(attempts)) {
attempts = 0;
}
updateSettings({ showAnswer: { ...showAnswer, afterAttempts: attempts } });
};
return {
handleShowAnswerChange,
handleAttemptsChange,
showAttempts,
};
};
export const timerCardHooks = (updateSettings) => ({
handleChange: (event) => {
let time = parseInt(event.target.value);
if (_.isNaN(time)) {
time = 0;
}
updateSettings({ timeBetween: time });
},
});
export const typeRowHooks = (typeKey, updateField) => {
const onClick = () => {
updateField({ problemType: typeKey });
};
return {
onClick,
};
};

View File

@@ -0,0 +1,227 @@
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 })])),
};
});
jest.mock('../../../../../data/redux', () => ({
actions: {
problem: {
updateSettings: (args) => ({ updateSettings: args }),
updateField: (args) => ({ updateField: args }),
},
},
}));
const state = new MockUseState(hooks);
describe('Problem settings hooks', () => {
let output;
let updateSettings;
beforeEach(() => {
updateSettings = jest.fn();
state.mock();
});
afterEach(() => {
state.restore();
useEffect.mockClear();
});
describe('Show advanced settings', () => {
beforeEach(() => {
output = hooks.showAdvancedSettingsCards();
});
test('test default state is false', () => {
expect(output.isAdvancedCardsVisible).toBeFalsy();
});
test('test showAdvancedCards sets state to true', () => {
output.showAdvancedCards();
expect(state.setState[state.keys.showAdvanced]).toHaveBeenCalledWith(true);
});
});
describe('Show full card', () => {
beforeEach(() => {
output = hooks.showFullCard();
});
test('test default state is false', () => {
expect(output.isCardCollapsed).toBeFalsy();
});
test('test toggleCardCollapse to true', () => {
output.toggleCardCollapse();
expect(state.setState[state.keys.cardCollapsed]).toHaveBeenCalledWith(true);
});
});
describe('Hint card hooks', () => {
test('test useEffect triggers set hints summary no hint', () => {
const hints = [];
hooks.hintsCardHooks(hints, updateSettings);
expect(state.setState[state.keys.summary]).not.toHaveBeenCalled();
const [cb, prereqs] = useEffect.mock.calls[0];
expect(prereqs).toStrictEqual([[]]);
cb();
expect(state.setState[state.keys.summary])
.toHaveBeenCalledWith({ message: messages.noHintSummary, values: {} });
});
test('test useEffect triggers set hints summary', () => {
const hints = [{ id: 1, value: 'hint1' }];
output = hooks.hintsCardHooks(hints, updateSettings);
expect(state.setState[state.keys.summary]).not.toHaveBeenCalled();
const [cb, prereqs] = useEffect.mock.calls[0];
expect(prereqs).toStrictEqual([[{ id: 1, value: 'hint1' }]]);
cb();
expect(state.setState[state.keys.summary])
.toHaveBeenCalledWith({
message: messages.hintSummary,
values: { hint: hints[0].value, count: (hints.length - 1) },
});
});
test('test handleAdd triggers updateSettings', () => {
const hint1 = { id: 1, value: 'hint1' };
const hint2 = { id: 2, value: '' };
const hints = [hint1];
output = hooks.hintsCardHooks(hints, updateSettings);
output.handleAdd();
expect(updateSettings).toHaveBeenCalledWith({ hints: [hint1, hint2] });
});
});
describe('Hint rows hooks', () => {
const hint1 = { id: 1, value: 'hint1' };
const hint2 = { id: 2, value: 'hint2' };
const value = 'modifiedHint';
const modifiedHint = { id: 2, value };
const hints = [hint1, hint2];
beforeEach(() => {
output = hooks.hintsRowHooks(2, hints, updateSettings);
});
test('test handleChange', () => {
output.handleChange({ target: { value } });
expect(updateSettings).toHaveBeenCalledWith({ hints: [hint1, modifiedHint] });
});
test('test handleDelete', () => {
output.handleDelete();
expect(updateSettings).toHaveBeenCalledWith({ hints: [hint1] });
});
});
describe('Matlab card hooks', () => {
test('test useEffect triggers set summary', () => {
const apiKey = 'matlab_api_key';
hooks.matlabCardHooks(apiKey, updateSettings);
expect(state.setState[state.keys.summary]).not.toHaveBeenCalled();
const [cb, prereqs] = useEffect.mock.calls[0];
expect(prereqs).toStrictEqual([apiKey]);
cb();
expect(state.setState[state.keys.summary])
.toHaveBeenCalledWith({ message: apiKey, values: {}, intl: false });
});
test('test useEffect triggers set summary no key', () => {
hooks.matlabCardHooks('', updateSettings);
expect(state.setState[state.keys.summary]).not.toHaveBeenCalled();
const [cb, prereqs] = useEffect.mock.calls[0];
expect(prereqs).toStrictEqual(['']);
cb();
expect(state.setState[state.keys.summary])
.toHaveBeenCalledWith({ message: messages.matlabNoKeySummary, values: {}, intl: true });
});
test('test handleChange', () => {
const apiKey = 'matlab_api_key';
const value = 'new_matlab_api_key';
output = hooks.matlabCardHooks(apiKey, updateSettings);
output.handleChange({ target: { value } });
expect(updateSettings).toHaveBeenCalledWith({ matLabApiKey: value });
});
});
describe('Reset card hooks', () => {
beforeEach(() => {
output = hooks.resetCardHooks(updateSettings);
});
test('test setResetTrue', () => {
output.setResetTrue();
expect(updateSettings).toHaveBeenCalledWith({ showResetButton: true });
});
test('test setResetFalse', () => {
output.setResetFalse();
expect(updateSettings).toHaveBeenCalledWith({ showResetButton: false });
});
});
describe('Scoring card hooks', () => {
const scoring = {
weight: 1.5,
attempts: {
unlimited: false,
number: 5,
},
};
beforeEach(() => {
output = hooks.scoringCardHooks(scoring, updateSettings);
});
test('test handleMaxAttemptChange', () => {
const value = 6;
output.handleMaxAttemptChange({ target: { value } });
expect(updateSettings)
.toHaveBeenCalledWith({ scoring: { ...scoring, attempts: { number: value, unlimited: false } } });
});
test('test handleMaxAttemptChange set attempts to zero', () => {
const value = 0;
output.handleMaxAttemptChange({ target: { value } });
expect(updateSettings)
.toHaveBeenCalledWith({ scoring: { ...scoring, attempts: { number: value, unlimited: true } } });
});
test('test handleWeightChange', () => {
const value = 2;
output.handleWeightChange({ target: { value } });
expect(updateSettings).toHaveBeenCalledWith({ scoring: { ...scoring, weight: parseFloat(value) } });
});
});
describe('Show answer card hooks', () => {
const showAnswer = {
on: 'after_attempts',
afterAttempts: 5,
};
beforeEach(() => {
output = hooks.showAnswerCardHooks(showAnswer, updateSettings);
});
test('test handleShowAnswerChange', () => {
const value = 'always';
output.handleShowAnswerChange({ target: { value } });
expect(updateSettings).toHaveBeenCalledWith({ showAnswer: { ...showAnswer, on: value } });
});
test('test handleAttemptsChange', () => {
const value = 3;
output.handleAttemptsChange({ target: { value } });
expect(updateSettings).toHaveBeenCalledWith({ showAnswer: { ...showAnswer, afterAttempts: parseInt(value) } });
});
});
describe('Timer card hooks', () => {
test('test handleChange', () => {
output = hooks.timerCardHooks(updateSettings);
const value = 5;
output.handleChange({ target: { value } });
expect(updateSettings).toHaveBeenCalledWith({ timeBetween: value });
});
});
describe('Type row hooks', () => {
test('test onClick', () => {
const typekey = 'TEXTINPUT';
const updateField = jest.fn();
output = hooks.typeRowHooks(typekey, updateField);
output.onClick();
expect(updateField).toHaveBeenCalledWith({ problemType: typekey });
});
});
});

View File

@@ -0,0 +1,106 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { connect } from 'react-redux';
import {
Button, Col, Collapsible, Container, Row,
} from '@edx/paragon';
import { selectors, actions } from '../../../../../data/redux';
import ScoringCard from './settingsComponents/ScoringCard';
import ShowAnswerCard from './settingsComponents/ShowAnswerCard';
import HintsCard from './settingsComponents/HintsCard';
import ResetCard from './settingsComponents/ResetCard';
import MatlabCard from './settingsComponents/MatlabCard';
import TimerCard from './settingsComponents/TimerCard';
import TypeCard from './settingsComponents/TypeCard';
import messages from './messages';
import { showAdvancedSettingsCards } from './hooks';
import './index.scss';
// This widget should be connected, grab all settings from store, update them as needed.
export const SettingsWidget = ({
problemType,
// redux
settings,
updateSettings,
updateField,
}) => {
const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards();
return (
<div>
<div>
<h3>
<FormattedMessage {...messages.settingsWidgetTitle} />
</h3>
<Container>
<Row>
<Col>
<Row className="my-2">
<TypeCard problemType={problemType} updateField={updateField} />
</Row>
<Row className="my-2">
<ScoringCard scoring={settings.scoring} updateSettings={updateSettings} />
</Row>
<Row className="mt-2">
<HintsCard hints={settings.hints} updateSettings={updateSettings} />
</Row>
<Row>
<Collapsible.Advanced open={!isAdvancedCardsVisible}>
<Collapsible.Body className="collapsible-body">
<Button
className="my-3 ml-2"
variant="link"
size="inline"
onClick={showAdvancedCards}
>
<FormattedMessage {...messages.showAdvanceSettingsButtonText} />
</Button>
</Collapsible.Body>
</Collapsible.Advanced>
</Row>
<Collapsible.Advanced open={isAdvancedCardsVisible}>
<Collapsible.Body className="collapsible-body">
<Row className="my-2">
<ShowAnswerCard showAnswer={settings.showAnswer} updateSettings={updateSettings} />
</Row>
<Row className="my-2">
<ResetCard showResetButton={settings.showResetButton} updateSettings={updateSettings} />
</Row>
<Row className="my-2">
<TimerCard timeBetween={settings.timeBetween} updateSettings={updateSettings} />
</Row>
<Row className="my-2">
<MatlabCard matLabApiKey={settings.matLabApiKey} updateSettings={updateSettings} />
</Row>
</Collapsible.Body>
</Collapsible.Advanced>
</Col>
</Row>
</Container>
</div>
</div>
);
};
SettingsWidget.propTypes = {
problemType: PropTypes.string.isRequired,
updateField: PropTypes.func.isRequired,
updateSettings: PropTypes.func.isRequired,
// eslint-disable-next-line
settings: PropTypes.any.isRequired,
};
const mapStateToProps = (state) => ({
settings: selectors.problem.settings(state),
});
export const mapDispatchToProps = {
updateSettings: actions.problem.updateSettings,
updateField: actions.problem.updateField,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SettingsWidget));

View File

@@ -0,0 +1,11 @@
.settingsCardTitleSection {
padding-bottom: 0rem;
}
.halfSpacedMessage {
padding-bottom: .5rem;
}
.spacedMessage {
padding-bottom: 1.5rem;
}

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { shallow } from 'enzyme';
import { showAdvancedSettingsCards } from './hooks';
import { SettingsWidget, mapDispatchToProps } from '.';
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
import { actions } from '../../../../../data/redux';
jest.mock('./hooks', () => ({
showAdvancedSettingsCards: jest.fn(),
}));
describe('SettingsWidget', () => {
const props = {
problemType: ProblemTypeKeys.TEXTINPUT,
settings: {},
};
describe('behavior', () => {
it(' calls showAdvancedSettingsCards when initialized', () => {
const showAdvancedSettingsCardsProps = {
isAdvancedCardsVisible: false,
setResetTrue: jest.fn().mockName('showAdvancedSettingsCards.setResetTrue'),
};
showAdvancedSettingsCards.mockReturnValue(showAdvancedSettingsCardsProps);
shallow(<SettingsWidget {...props} />);
expect(showAdvancedSettingsCards).toHaveBeenCalledWith();
});
});
describe('snapshot', () => {
test('snapshot: renders Settings widget page', () => {
const showAdvancedSettingsCardsProps = {
isAdvancedCardsVisible: false,
setResetTrue: jest.fn().mockName('showAdvancedSettingsCards.setResetTrue'),
};
showAdvancedSettingsCards.mockReturnValue(showAdvancedSettingsCardsProps);
expect(shallow(<SettingsWidget {...props} />)).toMatchSnapshot();
});
test('snapshot: renders Settings widget page advanced settings visible', () => {
const showAdvancedSettingsCardsProps = {
isAdvancedCardsVisible: true,
setResetTrue: jest.fn().mockName('showAdvancedSettingsCards.setResetTrue'),
};
showAdvancedSettingsCards.mockReturnValue(showAdvancedSettingsCardsProps);
expect(shallow(<SettingsWidget {...props} />)).toMatchSnapshot();
});
});
describe('mapDispatchToProps', () => {
test('updateSettings from actions.problem.updateSettings', () => {
expect(mapDispatchToProps.updateSettings).toEqual(actions.problem.updateSettings);
});
});
describe('mapDispatchToProps', () => {
test('updateField from actions.problem.updateField', () => {
expect(mapDispatchToProps.updateField).toEqual(actions.problem.updateField);
});
});
});

View File

@@ -0,0 +1,154 @@
export const messages = {
settingsWidgetTitle: {
id: 'authoring.problemeditor.settings.settingsWidgetTitle',
defaultMessage: 'Settings',
description: 'Settings Title',
},
showAdvanceSettingsButtonText: {
id: 'authoring.problemeditor.settings.showAdvancedButton',
defaultMessage: 'Show advanced settings',
description: 'Button text to show advanced settings',
},
settingsDeleteIconAltText: {
id: 'authoring.problemeditor.settings.delete.icon.alt',
defaultMessage: 'Delete answer',
description: 'Alt text for delete icon',
},
advancedSettingsLinkText: {
id: 'authoring.problemeditor.settings.advancedSettingLink.text',
defaultMessage: 'Set a default value in advanced settings',
description: 'Advanced settings link text',
},
hintSettingTitle: {
id: 'authoring.problemeditor.settings.hint.title',
defaultMessage: 'Hints',
description: 'Hint settings card title',
},
hintInputLabel: {
id: 'authoring.problemeditor.settings.hint.inputLabel',
defaultMessage: 'Hint',
description: 'Hint text input label',
},
addHintButtonText: {
id: 'authoring.problemeditor.settings.hint.addHintButton',
defaultMessage: 'Add hint',
description: 'Add hint button text',
},
noHintSummary: {
id: 'authoring.problemeditor.settings.hint.noHintSummary',
defaultMessage: 'No Hints',
description: 'Summary text for no hints',
},
hintSummary: {
id: 'authoring.problemeditor.settings.hint.summary',
defaultMessage: '{hint} {count, plural, =0 {} other {(+# more)}}',
description: 'Summary text for hint settings',
},
matlabSettingTitle: {
id: 'authoring.problemeditor.settings.matlab.title',
defaultMessage: 'MATLAB API Key',
description: 'Matlab settings card title',
},
matlabSettingText1: {
id: 'authoring.problemeditor.settings.matlab.text.one',
defaultMessage: 'Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. This key is granted for exclusive use by this course for the specified duration.',
description: 'Matlab settings card text 1',
},
matlabSettingText2: {
id: 'authoring.problemeditor.settings.matlab.text.two',
defaultMessage: 'Please do not share the API key with other courses and notify MathWorks immediately if you believe the key is exposed or compromised. To obtain a key for your course, or to report an issue please contact',
description: 'Matlab settings card text 2',
},
matlabInputLabel: {
id: 'authoring.problemeditor.settings.matlab.inputLabel',
defaultMessage: 'API Key',
description: 'Matlab text input label',
},
matlabNoKeySummary: {
id: 'authoring.problemeditor.settings.matlab.noKeySummary',
defaultMessage: 'None',
description: 'Matlab no key summary',
},
resetSettingsTitle: {
id: 'authoring.problemeditor.settings.reset.title',
defaultMessage: 'Show reset option',
description: 'Reset settings card title',
},
resetSettingsTrue: {
id: 'authoring.problemeditor.settings.reset.true',
defaultMessage: 'True',
description: 'True option for reset',
},
resetSettingsFalse: {
id: 'authoring.problemeditor.settings.reset.false',
defaultMessage: 'False',
description: 'False option for reset',
},
resetSettingText: {
id: 'authoring.problemeditor.settings.reset.text',
defaultMessage: "Determines whether a 'Reset' button is shown so the user may reset their answer, generally for use in practice or formative assessments.",
description: 'Reset settings card text',
},
scoringSettingsTitle: {
id: 'authoring.problemeditor.settings.scoring.title',
defaultMessage: 'Scoring',
description: 'Scoring settings card title',
},
scoringAttemptsInputLabel: {
id: 'authoring.problemeditor.settings.scoring.attempts.inputLabel',
defaultMessage: 'Attempts',
description: 'Scoring attempts text input label',
},
scoringWeightInputLabel: {
id: 'authoring.problemeditor.settings.scoring.weight.inputLabel',
defaultMessage: 'Points',
description: 'Scoring weight input label',
},
scoringSummary: {
id: 'authoring.problemeditor.settings.scoring.summary',
defaultMessage: '{attempts, plural, =0 {Unlimited} other {#}} attempts - {weight, plural, =0 {Ungraded} other {# points}}',
description: 'Summary text for scoring settings',
},
showAnswerSettingsTitle: {
id: 'authoring.problemeditor.settings.showAnswer.title',
defaultMessage: 'Show answer',
description: 'Show Answer settings card title',
},
showAnswerAttemptsInputLabel: {
id: 'authoring.problemeditor.settings.showAnswer.attempts.inputLabel',
defaultMessage: 'Number of Attempts',
description: 'Show Answer attempts text input label',
},
showAnswerSettingText: {
id: 'authoring.problemeditor.settings.showAnswer.text',
defaultMessage: 'Define when learners can see the correct answer.',
description: 'Show Answer settings card text',
},
timerSettingsTitle: {
id: 'authoring.problemeditor.settings.timer.title',
defaultMessage: 'Time between attempts',
description: 'Timer settings card title',
},
timerSummary: {
id: 'authoring.problemeditor.settings.timer.summary',
defaultMessage: '{time} seconds',
description: 'Summary text for timer settings',
},
timerSettingText: {
id: 'authoring.problemeditor.settings.timer.text',
defaultMessage: 'Seconds a student must wait between submissions for a problem with multiple attempts.',
description: 'Timer settings card text',
},
timerInputLabel: {
id: 'authoring.problemeditor.settings.timer.inputLabel',
defaultMessage: 'Attempts',
description: 'Timer text input label',
},
typeSettingTitle: {
id: 'authoring.problemeditor.settings.type.title',
defaultMessage: 'Type',
description: 'Type settings card title',
},
};
export default messages;

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Col, Container, Form, Icon, IconButton, Row,
} from '@edx/paragon';
import { Delete } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import messages from '../messages';
export const HintRow = ({
value,
handleChange,
handleDelete,
// inject
intl,
}) => (
<Container fluid>
<Row>
<Col xs={10}>
<Form.Group>
<Form.Control
value={value}
onChange={handleChange}
floatingLabel={intl.formatMessage(messages.hintInputLabel)}
/>
</Form.Group>
</Col>
<Col xs={2}>
<IconButton
src={Delete}
iconAs={Icon}
alt={intl.formatMessage(messages.settingsDeleteIconAltText)}
onClick={handleDelete}
variant="secondary"
/>
</Col>
</Row>
</Container>
);
HintRow.propTypes = {
value: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
handleDelete: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(HintRow);

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
import { HintRow } from './HintRow';
describe('HintRow', () => {
const props = {
value: 'hint_1',
handleChange: jest.fn(),
handleDelete: jest.fn(),
intl: { formatMessage },
};
describe('snapshot', () => {
test('snapshot: renders hints row', () => {
expect(shallow(<HintRow {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { Add } from '@edx/paragon/icons';
import SettingsOption from '../SettingsOption';
import messages from '../messages';
import { hintsCardHooks, hintsRowHooks } from '../hooks';
import HintRow from './HintRow';
export const HintsCard = ({
hints,
updateSettings,
// inject
intl,
}) => {
const { summary, handleAdd } = hintsCardHooks(hints, updateSettings);
return (
<SettingsOption
title={intl.formatMessage(messages.hintSettingTitle)}
summary={intl.formatMessage(summary.message, { ...summary.values })}
>
{hints.map((hint) => (
<HintRow
key={hint.id}
id={hint.id}
value={hint.value}
{...hintsRowHooks(hint.id, hints, updateSettings)}
/>
))}
<Button
className="my-3 ml-2"
iconBefore={Add}
variant="tertiary"
onClick={handleAdd}
>
<FormattedMessage {...messages.addHintButtonText} />
</Button>
</SettingsOption>
);
};
HintsCard.propTypes = {
intl: intlShape.isRequired,
hints: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
})).isRequired,
updateSettings: PropTypes.func.isRequired,
};
export default injectIntl(HintsCard);

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
import { HintsCard } from './HintsCard';
import { hintsCardHooks, hintsRowHooks } from '../hooks';
import messages from '../messages';
jest.mock('../hooks', () => ({
hintsCardHooks: jest.fn(),
hintsRowHooks: jest.fn(),
}));
describe('HintsCard', () => {
const hint1 = { id: 1, value: 'hint1' };
const hint2 = { id: 2, value: '' };
const hints0 = [];
const hints1 = [hint1];
const hints2 = [hint1, hint2];
const props = {
intl: { formatMessage },
hints: hints0,
updateSettings: jest.fn().mockName('args.updateSettings'),
};
const hintsRowHooksProps = {
handleChange: jest.fn().mockName('hintsRowHooks.handleChange'),
handleDelete: jest.fn().mockName('hintsRowHooks.handleDelete'),
};
hintsRowHooks.mockReturnValue(hintsRowHooksProps);
describe('behavior', () => {
it(' calls hintsCardHooks when initialized', () => {
const hintsCardHooksProps = {
summary: { message: messages.noHintSummary, values: {} },
handleAdd: jest.fn().mockName('hintsCardHooks.handleAdd'),
};
hintsCardHooks.mockReturnValue(hintsCardHooksProps);
shallow(<HintsCard {...props} />);
expect(hintsCardHooks).toHaveBeenCalledWith(hints0, props.updateSettings);
});
});
describe('snapshot', () => {
test('snapshot: renders hints setting card no hints', () => {
const hintsCardHooksProps = {
summary: { message: messages.noHintSummary, values: {} },
handleAdd: jest.fn().mockName('hintsCardHooks.handleAdd'),
};
hintsCardHooks.mockReturnValue(hintsCardHooksProps);
expect(shallow(<HintsCard {...props} />)).toMatchSnapshot();
});
test('snapshot: renders hints setting card one hint', () => {
const hintsCardHooksProps = {
summary: {
message: messages.hintSummary,
values: { hint: hint1.value, count: 1 },
},
handleAdd: jest.fn().mockName('hintsCardHooks.handleAdd'),
};
hintsCardHooks.mockReturnValue(hintsCardHooksProps);
expect(shallow(<HintsCard {...props} hints={hints1} />)).toMatchSnapshot();
});
test('snapshot: renders hints setting card multiple hints', () => {
const hintsCardHooksProps = {
summary: {
message: messages.hintSummary,
values: { hint: hint2.value, count: 2 },
},
handleAdd: jest.fn().mockName('hintsCardHooks.handleAdd'),
};
hintsCardHooks.mockReturnValue(hintsCardHooksProps);
expect(shallow(<HintsCard {...props} hints={hints2} />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import { Form, MailtoLink } from '@edx/paragon';
import PropTypes from 'prop-types';
import SettingsOption from '../SettingsOption';
import messages from '../messages';
import { matlabCardHooks } from '../hooks';
export const MatlabCard = ({
matLabApiKey,
updateSettings,
// inject
intl,
}) => {
const { summary, handleChange } = matlabCardHooks(matLabApiKey, updateSettings);
return (
<SettingsOption
title={intl.formatMessage(messages.matlabSettingTitle)}
summary={summary.intl ? intl.formatMessage(summary.message, { ...summary.values }) : summary.message}
>
<div className="halfSpacedMessage">
<span>
<FormattedMessage {...messages.matlabSettingText1} />
</span>
</div>
<div className="spacedMessage">
<span>
<FormattedMessage {...messages.matlabSettingText2} />&nbsp;
<MailtoLink to="moocsupport@mathworks.com">
moocsupport@mathworks.com
</MailtoLink>
</span>
</div>
<Form.Group>
<Form.Control
value={matLabApiKey}
onChange={handleChange}
floatingLabel={intl.formatMessage(messages.matlabInputLabel)}
/>
</Form.Group>
</SettingsOption>
);
};
MatlabCard.propTypes = {
matLabApiKey: PropTypes.string.isRequired,
updateSettings: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(MatlabCard);

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
import { matlabCardHooks } from '../hooks';
import { MatlabCard } from './MatlabCard';
import messages from '../messages';
jest.mock('../hooks', () => ({
matlabCardHooks: jest.fn(),
}));
describe('MatlabCard', () => {
const matLabApiKey = 'matlab_api_key';
const props = {
matLabApiKey,
updateSettings: jest.fn().mockName('args.updateSettings'),
intl: { formatMessage },
};
describe('behavior', () => {
it(' calls resetCardHooks when initialized', () => {
const matlabCardHooksProps = {
summary: { message: matLabApiKey, values: {}, intl: false },
handleChange: jest.fn().mockName('matlabCardHooks.handleChange'),
};
matlabCardHooks.mockReturnValue(matlabCardHooksProps);
shallow(<MatlabCard {...props} />);
expect(matlabCardHooks).toHaveBeenCalledWith(matLabApiKey, props.updateSettings);
});
});
describe('snapshot', () => {
test('snapshot: renders matlab setting card', () => {
const matlabCardHooksProps = {
summary: { message: matLabApiKey, values: {}, intl: false },
handleChange: jest.fn().mockName('matlabCardHooks.handleChange'),
};
matlabCardHooks.mockReturnValue(matlabCardHooksProps);
expect(shallow(<MatlabCard {...props} />)).toMatchSnapshot();
});
test('snapshot: renders matlab setting card no key', () => {
const matlabCardHooksProps = {
summary: { message: messages.matlabNoKeySummary, values: {}, intl: true },
handleChange: jest.fn().mockName('matlabCardHooks.handleChange'),
};
matlabCardHooks.mockReturnValue(matlabCardHooksProps);
expect(shallow(<MatlabCard {...props} matLabApiKey="" />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, ButtonGroup, Hyperlink } from '@edx/paragon';
import PropTypes from 'prop-types';
import SettingsOption from '../SettingsOption';
import messages from '../messages';
import { resetCardHooks } from '../hooks';
export const ResetCard = ({
showResetButton,
updateSettings,
// inject
intl,
}) => {
const { setResetTrue, setResetFalse } = resetCardHooks(updateSettings);
return (
<SettingsOption
title={intl.formatMessage(messages.resetSettingsTitle)}
summary={showResetButton
? intl.formatMessage(messages.resetSettingsTrue) : intl.formatMessage(messages.resetSettingsFalse)}
>
<div className="halfSpacedMessage">
<span>
<FormattedMessage {...messages.resetSettingText} />
</span>
</div>
<div className="spacedMessage">
<Hyperlink destination="#" target="_blank">
<FormattedMessage {...messages.advancedSettingsLinkText} />
</Hyperlink>
</div>
<ButtonGroup size="lg" className="mb-2">
<Button variant={showResetButton ? 'outline-primary' : 'primary'} onClick={setResetFalse}>
<FormattedMessage {...messages.resetSettingsFalse} />
</Button>
<Button variant={showResetButton ? 'primary' : 'outline-primary'} onClick={setResetTrue}>
<FormattedMessage {...messages.resetSettingsTrue} />
</Button>
</ButtonGroup>
</SettingsOption>
);
};
ResetCard.propTypes = {
showResetButton: PropTypes.bool.isRequired,
updateSettings: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(ResetCard);

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
import { ResetCard } from './ResetCard';
import { resetCardHooks } from '../hooks';
jest.mock('../hooks', () => ({
resetCardHooks: jest.fn(),
}));
describe('ResetCard', () => {
const props = {
showResetButton: false,
updateSettings: jest.fn().mockName('args.updateSettings'),
intl: { formatMessage },
};
const resetCardHooksProps = {
setResetTrue: jest.fn().mockName('resetCardHooks.setResetTrue'),
setResetFalse: jest.fn().mockName('resetCardHooks.setResetFalse'),
};
resetCardHooks.mockReturnValue(resetCardHooksProps);
describe('behavior', () => {
it(' calls resetCardHooks when initialized', () => {
shallow(<ResetCard {...props} />);
expect(resetCardHooks).toHaveBeenCalledWith(props.updateSettings);
});
});
describe('snapshot', () => {
test('snapshot: renders reset true setting card', () => {
expect(shallow(<ResetCard {...props} />)).toMatchSnapshot();
});
test('snapshot: renders reset true setting card', () => {
expect(shallow(<ResetCard {...props} showResetButton />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Form } from '@edx/paragon';
import SettingsOption from '../SettingsOption';
import messages from '../messages';
import { scoringCardHooks } from '../hooks';
export const ScoringCard = ({
scoring,
updateSettings,
// inject
intl,
}) => {
const { handleMaxAttemptChange, handleWeightChange } = scoringCardHooks(scoring, updateSettings);
return (
<SettingsOption
title={intl.formatMessage(messages.scoringSettingsTitle)}
summary={intl.formatMessage(messages.scoringSummary,
{ attempts: scoring.attempts.number, weight: scoring.weight })}
>
<Form.Group>
<Form.Control
type="number"
value={scoring.attempts.number}
onChange={handleMaxAttemptChange}
floatingLabel={intl.formatMessage(messages.scoringAttemptsInputLabel)}
/>
</Form.Group>
<Form.Group>
<Form.Control
type="number"
value={scoring.weight}
onChange={handleWeightChange}
floatingLabel={intl.formatMessage(messages.scoringWeightInputLabel)}
/>
</Form.Group>
</SettingsOption>
);
};
ScoringCard.propTypes = {
intl: intlShape.isRequired,
// eslint-disable-next-line
scoring: PropTypes.any.isRequired,
updateSettings: PropTypes.func.isRequired,
};
export default injectIntl(ScoringCard);

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
import { scoringCardHooks } from '../hooks';
import { ScoringCard } from './ScoringCard';
jest.mock('../hooks', () => ({
scoringCardHooks: jest.fn(),
}));
describe('ScoringCard', () => {
const scoring = {
weight: 1.5,
attempts: {
unlimited: false,
number: 5,
},
updateSettings: jest.fn().mockName('args.updateSettings'),
intl: { formatMessage },
};
const props = {
scoring,
intl: { formatMessage },
};
const scoringCardHooksProps = {
handleMaxAttemptChange: jest.fn().mockName('scoringCardHooks.handleMaxAttemptChange'),
handleWeightChange: jest.fn().mockName('scoringCardHooks.handleWeightChange'),
};
scoringCardHooks.mockReturnValue(scoringCardHooksProps);
describe('behavior', () => {
it(' calls scoringCardHooks when initialized', () => {
shallow(<ScoringCard {...props} />);
expect(scoringCardHooks).toHaveBeenCalledWith(scoring, props.updateSettings);
});
});
describe('snapshot', () => {
test('snapshot: scoring setting card', () => {
expect(shallow(<ScoringCard {...props} />)).toMatchSnapshot();
});
test('snapshot: scoring setting card zero zero weight', () => {
expect(shallow(<ScoringCard
{...props}
scoring={{
...scoring,
weight: 0,
}}
/>)).toMatchSnapshot();
});
test('snapshot: scoring setting card max attempts', () => {
expect(shallow(<ScoringCard
{...props}
scoring={{
...scoring,
attempts: {
unlimited: true,
number: 0,
},
}}
/>)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,74 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import { Form, Hyperlink } from '@edx/paragon';
import SettingsOption from '../SettingsOption';
import { ShowAnswerTypes, ShowAnswerTypesKeys } from '../../../../../../data/constants/problem';
import messages from '../messages';
import { showAnswerCardHooks } from '../hooks';
export const ShowAnswerCard = ({
showAnswer,
updateSettings,
// inject
intl,
}) => {
const {
handleShowAnswerChange,
handleAttemptsChange,
showAttempts,
} = showAnswerCardHooks(showAnswer, updateSettings);
return (
<SettingsOption
title={intl.formatMessage(messages.showAnswerSettingsTitle)}
summary={intl.formatMessage(ShowAnswerTypes[showAnswer.on])}
>
<div className="halfSpacedMessage">
<span>
<FormattedMessage {...messages.showAnswerSettingText} />
</span>
</div>
<div className="spacedMessage">
<Hyperlink destination="#" target="_blank">
<FormattedMessage {...messages.advancedSettingsLinkText} />
</Hyperlink>
</div>
<Form.Group>
<Form.Control
as="select"
value={showAnswer.on}
onChange={handleShowAnswerChange}
>
{Object.values(ShowAnswerTypesKeys).map((answerType) => (
<option
key={answerType}
value={answerType}
>
{intl.formatMessage(ShowAnswerTypes[answerType])}
</option>
))}
</Form.Control>
</Form.Group>
{ showAttempts
&& (
<Form.Group>
<Form.Control
type="number"
value={showAnswer.afterAttempts}
onChange={handleAttemptsChange}
floatingLabel={intl.formatMessage(messages.showAnswerAttemptsInputLabel)}
/>
</Form.Group>
)}
</SettingsOption>
);
};
ShowAnswerCard.propTypes = {
intl: intlShape.isRequired,
// eslint-disable-next-line
showAnswer: PropTypes.any.isRequired,
updateSettings: PropTypes.func.isRequired,
};
export default injectIntl(ShowAnswerCard);

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
import { ShowAnswerCard } from './ShowAnswerCard';
import { showAnswerCardHooks } from '../hooks';
jest.mock('../hooks', () => ({
showAnswerCardHooks: jest.fn(),
}));
describe('ShowAnswerCard', () => {
const showAnswer = {
on: 'after_attempts',
afterAttempts: 5,
updateSettings: jest.fn().mockName('args.updateSettings'),
intl: { formatMessage },
};
const props = {
showAnswer,
intl: { formatMessage },
};
const showAnswerCardHooksProps = {
handleShowAnswerChange: jest.fn().mockName('showAnswerCardHooks.handleShowAnswerChange'),
handleAttemptsChange: jest.fn().mockName('showAnswerCardHooks.handleAttemptsChange'),
};
showAnswerCardHooks.mockReturnValue(showAnswerCardHooksProps);
describe('behavior', () => {
it(' calls showAnswerCardHooks when initialized', () => {
shallow(<ShowAnswerCard {...props} />);
expect(showAnswerCardHooks).toHaveBeenCalledWith(showAnswer, props.updateSettings);
});
});
describe('snapshot', () => {
test('snapshot: show answer setting card', () => {
expect(shallow(<ShowAnswerCard {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,45 @@
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 { timerCardHooks } from '../hooks';
export const TimerCard = ({
timeBetween,
updateSettings,
// inject
intl,
}) => {
const { handleChange } = timerCardHooks(updateSettings);
return (
<SettingsOption
title={intl.formatMessage(messages.timerSettingsTitle)}
summary={intl.formatMessage(messages.timerSummary, { time: timeBetween })}
>
<div className="spacedMessage">
<span>
<FormattedMessage {...messages.timerSettingText} />
</span>
</div>
<Form.Group>
<Form.Control
type="number"
value={timeBetween}
onChange={handleChange}
floatingLabel={intl.formatMessage(messages.timerInputLabel)}
/>
</Form.Group>
</SettingsOption>
);
};
TimerCard.propTypes = {
timeBetween: PropTypes.number.isRequired,
updateSettings: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(TimerCard);

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
import { TimerCard } from './TimerCard';
import { timerCardHooks } from '../hooks';
jest.mock('../hooks', () => ({
timerCardHooks: jest.fn(),
}));
describe('TimerCard', () => {
const props = {
timeBetween: 5,
updateSettings: jest.fn().mockName('args.updateSettings'),
intl: { formatMessage },
};
const timerCardHooksProps = {
handleChange: jest.fn().mockName('timerCardHooks.handleChange'),
};
timerCardHooks.mockReturnValue(timerCardHooksProps);
describe('behavior', () => {
it(' calls timerCardHooks when initialized', () => {
shallow(<TimerCard {...props} />);
expect(timerCardHooks).toHaveBeenCalledWith(props.updateSettings);
});
});
describe('snapshot', () => {
test('snapshot: renders reset true setting card', () => {
expect(shallow(<TimerCard {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import SettingsOption from '../SettingsOption';
import { ProblemTypeKeys, ProblemTypes } from '../../../../../../data/constants/problem';
import messages from '../messages';
import TypeRow from './TypeRow';
export const TypeCard = ({
problemType,
updateField,
// inject
intl,
}) => {
const problemTypeKeysArray = Object.values(ProblemTypeKeys);
return (
<SettingsOption
title={intl.formatMessage(messages.typeSettingTitle)}
summary={ProblemTypes[problemType].title}
>
{problemTypeKeysArray.map((typeKey, i) => (
<TypeRow
key={typeKey}
typeKey={typeKey}
label={ProblemTypes[typeKey].title}
selected={typeKey !== problemType}
lastRow={(i + 1) === problemTypeKeysArray.length}
updateField={updateField}
/>
))}
</SettingsOption>
);
};
TypeCard.propTypes = {
intl: intlShape.isRequired,
problemType: PropTypes.string.isRequired,
updateField: PropTypes.func.isRequired,
};
export default injectIntl(TypeCard);

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
import { TypeCard } from './TypeCard';
import { ProblemTypeKeys } from '../../../../../../data/constants/problem';
describe('TypeCard', () => {
const props = {
problemType: ProblemTypeKeys.TEXTINPUT,
updateField: jest.fn().mockName('args.updateField'),
intl: { formatMessage },
};
describe('snapshot', () => {
test('snapshot: renders type setting card', () => {
expect(shallow(<TypeCard {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { Container, Icon } from '@edx/paragon';
import PropTypes from 'prop-types';
import { Check } from '@edx/paragon/icons';
import { typeRowHooks } from '../hooks';
export const TypeRow = ({
typeKey,
label,
selected,
lastRow,
updateField,
}) => {
const { onClick } = typeRowHooks(typeKey, updateField);
return (
<>
<Container size="xl" onClick={onClick} role="button" className="d-flex" fluid>
<span className="flex-grow-1">{label}</span>
<span hidden={selected}><Icon src={Check} className="text-success" /></span>
</Container>
<hr className={lastRow ? 'd-none' : 'd-block'} />
</>
);
};
TypeRow.propTypes = {
typeKey: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
selected: PropTypes.bool.isRequired,
lastRow: PropTypes.bool.isRequired,
updateField: PropTypes.func.isRequired,
};
export default TypeRow;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TypeRow } from './TypeRow';
import { typeRowHooks } from '../hooks';
jest.mock('../hooks', () => ({
typeRowHooks: jest.fn(),
}));
describe('TypeRow', () => {
const typeKey = 'TEXTINPUT';
const props = {
typeKey,
label: 'Text Input Problem',
selected: true,
lastRow: false,
updateField: jest.fn().mockName('args.updateField'),
};
const typeRowHooksProps = {
onClick: jest.fn().mockName('typeRowHooks.onClick'),
};
typeRowHooks.mockReturnValue(typeRowHooksProps);
describe('behavior', () => {
it(' calls typeRowHooks when initialized', () => {
shallow(<TypeRow {...props} />);
expect(typeRowHooks).toHaveBeenCalledWith(typeKey, props.updateField);
});
});
describe('snapshot', () => {
test('snapshot: renders type row setting card', () => {
expect(shallow(<TypeRow {...props} />)).toMatchSnapshot();
});
test('snapshot: renders type row setting card not selected', () => {
expect(shallow(<TypeRow {...props} selected={false} />)).toMatchSnapshot();
});
test('snapshot: renders type row setting card last row', () => {
expect(shallow(<TypeRow {...props} lastRow />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HintRow snapshot snapshot: renders hints row 1`] = `
<Container
fluid={true}
>
<Row>
<Component
xs={10}
>
<Form.Group>
<Form.Control
floatingLabel="Hint"
onChange={[MockFunction]}
value="hint_1"
/>
</Form.Group>
</Component>
<Component
xs={2}
>
<IconButton
alt="Delete answer"
iconAs="Icon"
onClick={[MockFunction]}
variant="secondary"
/>
</Component>
</Row>
</Container>
`;

View File

@@ -0,0 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HintsCard snapshot snapshot: renders hints setting card multiple hints 1`] = `
<SettingsOption
summary=" {count, plural, =0 {} other {(+# more)}}"
title="Hints"
>
<injectIntl(ShimmedIntlComponent)
handleChange={[MockFunction hintsRowHooks.handleChange]}
handleDelete={[MockFunction hintsRowHooks.handleDelete]}
id={1}
key="1"
value="hint1"
/>
<injectIntl(ShimmedIntlComponent)
handleChange={[MockFunction hintsRowHooks.handleChange]}
handleDelete={[MockFunction hintsRowHooks.handleDelete]}
id={2}
key="2"
value=""
/>
<Button
className="my-3 ml-2"
onClick={[MockFunction hintsCardHooks.handleAdd]}
variant="tertiary"
>
<FormattedMessage
defaultMessage="Add hint"
description="Add hint button text"
id="authoring.problemeditor.settings.hint.addHintButton"
/>
</Button>
</SettingsOption>
`;
exports[`HintsCard snapshot snapshot: renders hints setting card no hints 1`] = `
<SettingsOption
summary="No Hints"
title="Hints"
>
<Button
className="my-3 ml-2"
onClick={[MockFunction hintsCardHooks.handleAdd]}
variant="tertiary"
>
<FormattedMessage
defaultMessage="Add hint"
description="Add hint button text"
id="authoring.problemeditor.settings.hint.addHintButton"
/>
</Button>
</SettingsOption>
`;
exports[`HintsCard snapshot snapshot: renders hints setting card one hint 1`] = `
<SettingsOption
summary="hint1 {count, plural, =0 {} other {(+# more)}}"
title="Hints"
>
<injectIntl(ShimmedIntlComponent)
handleChange={[MockFunction hintsRowHooks.handleChange]}
handleDelete={[MockFunction hintsRowHooks.handleDelete]}
id={1}
key="1"
value="hint1"
/>
<Button
className="my-3 ml-2"
onClick={[MockFunction hintsCardHooks.handleAdd]}
variant="tertiary"
>
<FormattedMessage
defaultMessage="Add hint"
description="Add hint button text"
id="authoring.problemeditor.settings.hint.addHintButton"
/>
</Button>
</SettingsOption>
`;

View File

@@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MatlabCard snapshot snapshot: renders matlab setting card 1`] = `
<SettingsOption
summary="matlab_api_key"
title="MATLAB API Key"
>
<div
className="halfSpacedMessage"
>
<span>
<FormattedMessage
defaultMessage="Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. This key is granted for exclusive use by this course for the specified duration."
description="Matlab settings card text 1"
id="authoring.problemeditor.settings.matlab.text.one"
/>
</span>
</div>
<div
className="spacedMessage"
>
<span>
<FormattedMessage
defaultMessage="Please do not share the API key with other courses and notify MathWorks immediately if you believe the key is exposed or compromised. To obtain a key for your course, or to report an issue please contact"
description="Matlab settings card text 2"
id="authoring.problemeditor.settings.matlab.text.two"
/>
 
<MailtoLink
to="moocsupport@mathworks.com"
>
moocsupport@mathworks.com
</MailtoLink>
</span>
</div>
<Form.Group>
<Form.Control
floatingLabel="API Key"
onChange={[MockFunction matlabCardHooks.handleChange]}
value="matlab_api_key"
/>
</Form.Group>
</SettingsOption>
`;
exports[`MatlabCard snapshot snapshot: renders matlab setting card no key 1`] = `
<SettingsOption
summary="None"
title="MATLAB API Key"
>
<div
className="halfSpacedMessage"
>
<span>
<FormattedMessage
defaultMessage="Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. This key is granted for exclusive use by this course for the specified duration."
description="Matlab settings card text 1"
id="authoring.problemeditor.settings.matlab.text.one"
/>
</span>
</div>
<div
className="spacedMessage"
>
<span>
<FormattedMessage
defaultMessage="Please do not share the API key with other courses and notify MathWorks immediately if you believe the key is exposed or compromised. To obtain a key for your course, or to report an issue please contact"
description="Matlab settings card text 2"
id="authoring.problemeditor.settings.matlab.text.two"
/>
 
<MailtoLink
to="moocsupport@mathworks.com"
>
moocsupport@mathworks.com
</MailtoLink>
</span>
</div>
<Form.Group>
<Form.Control
floatingLabel="API Key"
onChange={[MockFunction matlabCardHooks.handleChange]}
value=""
/>
</Form.Group>
</SettingsOption>
`;

View File

@@ -0,0 +1,117 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResetCard snapshot snapshot: renders reset true setting card 1`] = `
<SettingsOption
summary="False"
title="Show reset option"
>
<div
className="halfSpacedMessage"
>
<span>
<FormattedMessage
defaultMessage="Determines whether a 'Reset' button is shown so the user may reset their answer, generally for use in practice or formative assessments."
description="Reset settings card text"
id="authoring.problemeditor.settings.reset.text"
/>
</span>
</div>
<div
className="spacedMessage"
>
<Hyperlink
destination="#"
target="_blank"
>
<FormattedMessage
defaultMessage="Set a default value in advanced settings"
description="Advanced settings link text"
id="authoring.problemeditor.settings.advancedSettingLink.text"
/>
</Hyperlink>
</div>
<ButtonGroup
className="mb-2"
size="lg"
>
<Button
onClick={[MockFunction resetCardHooks.setResetFalse]}
variant="primary"
>
<FormattedMessage
defaultMessage="False"
description="False option for reset"
id="authoring.problemeditor.settings.reset.false"
/>
</Button>
<Button
onClick={[MockFunction resetCardHooks.setResetTrue]}
variant="outline-primary"
>
<FormattedMessage
defaultMessage="True"
description="True option for reset"
id="authoring.problemeditor.settings.reset.true"
/>
</Button>
</ButtonGroup>
</SettingsOption>
`;
exports[`ResetCard snapshot snapshot: renders reset true setting card 2`] = `
<SettingsOption
summary="True"
title="Show reset option"
>
<div
className="halfSpacedMessage"
>
<span>
<FormattedMessage
defaultMessage="Determines whether a 'Reset' button is shown so the user may reset their answer, generally for use in practice or formative assessments."
description="Reset settings card text"
id="authoring.problemeditor.settings.reset.text"
/>
</span>
</div>
<div
className="spacedMessage"
>
<Hyperlink
destination="#"
target="_blank"
>
<FormattedMessage
defaultMessage="Set a default value in advanced settings"
description="Advanced settings link text"
id="authoring.problemeditor.settings.advancedSettingLink.text"
/>
</Hyperlink>
</div>
<ButtonGroup
className="mb-2"
size="lg"
>
<Button
onClick={[MockFunction resetCardHooks.setResetFalse]}
variant="outline-primary"
>
<FormattedMessage
defaultMessage="False"
description="False option for reset"
id="authoring.problemeditor.settings.reset.false"
/>
</Button>
<Button
onClick={[MockFunction resetCardHooks.setResetTrue]}
variant="primary"
>
<FormattedMessage
defaultMessage="True"
description="True option for reset"
id="authoring.problemeditor.settings.reset.true"
/>
</Button>
</ButtonGroup>
</SettingsOption>
`;

View File

@@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScoringCard snapshot snapshot: scoring setting card 1`] = `
<SettingsOption
summary="{attempts, plural, =0 {Unlimited} other {#}} attempts - {weight, plural, =0 {Ungraded} other {# points}}"
title="Scoring"
>
<Form.Group>
<Form.Control
floatingLabel="Attempts"
onChange={[MockFunction scoringCardHooks.handleMaxAttemptChange]}
type="number"
value={5}
/>
</Form.Group>
<Form.Group>
<Form.Control
floatingLabel="Points"
onChange={[MockFunction scoringCardHooks.handleWeightChange]}
type="number"
value={1.5}
/>
</Form.Group>
</SettingsOption>
`;
exports[`ScoringCard snapshot snapshot: scoring setting card max attempts 1`] = `
<SettingsOption
summary="{attempts, plural, =0 {Unlimited} other {#}} attempts - {weight, plural, =0 {Ungraded} other {# points}}"
title="Scoring"
>
<Form.Group>
<Form.Control
floatingLabel="Attempts"
onChange={[MockFunction scoringCardHooks.handleMaxAttemptChange]}
type="number"
value={0}
/>
</Form.Group>
<Form.Group>
<Form.Control
floatingLabel="Points"
onChange={[MockFunction scoringCardHooks.handleWeightChange]}
type="number"
value={1.5}
/>
</Form.Group>
</SettingsOption>
`;
exports[`ScoringCard snapshot snapshot: scoring setting card zero zero weight 1`] = `
<SettingsOption
summary="{attempts, plural, =0 {Unlimited} other {#}} attempts - {weight, plural, =0 {Ungraded} other {# points}}"
title="Scoring"
>
<Form.Group>
<Form.Control
floatingLabel="Attempts"
onChange={[MockFunction scoringCardHooks.handleMaxAttemptChange]}
type="number"
value={5}
/>
</Form.Group>
<Form.Group>
<Form.Control
floatingLabel="Points"
onChange={[MockFunction scoringCardHooks.handleWeightChange]}
type="number"
value={0}
/>
</Form.Group>
</SettingsOption>
`;

View File

@@ -0,0 +1,114 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShowAnswerCard snapshot snapshot: show answer setting card 1`] = `
<SettingsOption
summary="After Some Number of Attempts"
title="Show answer"
>
<div
className="halfSpacedMessage"
>
<span>
<FormattedMessage
defaultMessage="Define when learners can see the correct answer."
description="Show Answer settings card text"
id="authoring.problemeditor.settings.showAnswer.text"
/>
</span>
</div>
<div
className="spacedMessage"
>
<Hyperlink
destination="#"
target="_blank"
>
<FormattedMessage
defaultMessage="Set a default value in advanced settings"
description="Advanced settings link text"
id="authoring.problemeditor.settings.advancedSettingLink.text"
/>
</Hyperlink>
</div>
<Form.Group>
<Form.Control
as="select"
onChange={[MockFunction showAnswerCardHooks.handleShowAnswerChange]}
value="after_attempts"
>
<option
key="always"
value="always"
>
Always
</option>
<option
key="answered"
value="answered"
>
Answered
</option>
<option
key="attempted"
value="attempted"
>
Attempted or Past Due
</option>
<option
key="closed"
value="closed"
>
Closed
</option>
<option
key="finished"
value="finished"
>
Finished
</option>
<option
key="correct_or_past_due"
value="correct_or_past_due"
>
Correct or Past Due
</option>
<option
key="past_due"
value="past_due"
>
Past Due
</option>
<option
key="never"
value="never"
>
Never
</option>
<option
key="after_attempts"
value="after_attempts"
>
After Some Number of Attempts
</option>
<option
key="after_all_attempts"
value="after_all_attempts"
>
After All Attempts
</option>
<option
key="after_all_attempts_or_correct"
value="after_all_attempts_or_correct"
>
After All Attempts or Correct
</option>
<option
key="attempted_no_past_due"
value="attempted_no_past_due"
>
Attempted
</option>
</Form.Control>
</Form.Group>
</SettingsOption>
`;

View File

@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TimerCard snapshot snapshot: renders reset true setting card 1`] = `
<SettingsOption
summary="5 seconds"
title="Time between attempts"
>
<div
className="spacedMessage"
>
<span>
<FormattedMessage
defaultMessage="Seconds a student must wait between submissions for a problem with multiple attempts."
description="Timer settings card text"
id="authoring.problemeditor.settings.timer.text"
/>
</span>
</div>
<Form.Group>
<Form.Control
floatingLabel="Attempts"
onChange={[MockFunction timerCardHooks.handleChange]}
type="number"
value={5}
/>
</Form.Group>
</SettingsOption>
`;

View File

@@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TypeCard snapshot snapshot: renders type setting card 1`] = `
<SettingsOption
summary="Text Input Problem"
title="Type"
>
<TypeRow
key="stringresponse"
label="Text Input Problem"
lastRow={false}
selected={false}
typeKey="stringresponse"
updateField={[MockFunction args.updateField]}
/>
<TypeRow
key="numericalresponse"
label="Numeric Response Problem"
lastRow={false}
selected={true}
typeKey="numericalresponse"
updateField={[MockFunction args.updateField]}
/>
<TypeRow
key="optionresponse"
label="Dropdown Problem"
lastRow={false}
selected={true}
typeKey="optionresponse"
updateField={[MockFunction args.updateField]}
/>
<TypeRow
key="choiceresponse"
label="Multi Select Problem"
lastRow={false}
selected={true}
typeKey="choiceresponse"
updateField={[MockFunction args.updateField]}
/>
<TypeRow
key="multiplechoiceresponse"
label="Single Select Problem"
lastRow={true}
selected={true}
typeKey="multiplechoiceresponse"
updateField={[MockFunction args.updateField]}
/>
</SettingsOption>
`;

View File

@@ -0,0 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TypeRow snapshot snapshot: renders type row setting card 1`] = `
<Fragment>
<Container
className="d-flex"
fluid={true}
onClick={[MockFunction typeRowHooks.onClick]}
role="button"
size="xl"
>
<span
className="flex-grow-1"
>
Text Input Problem
</span>
<span
hidden={true}
>
<Icon
className="text-success"
/>
</span>
</Container>
<hr
className="d-block"
/>
</Fragment>
`;
exports[`TypeRow snapshot snapshot: renders type row setting card last row 1`] = `
<Fragment>
<Container
className="d-flex"
fluid={true}
onClick={[MockFunction typeRowHooks.onClick]}
role="button"
size="xl"
>
<span
className="flex-grow-1"
>
Text Input Problem
</span>
<span
hidden={true}
>
<Icon
className="text-success"
/>
</span>
</Container>
<hr
className="d-none"
/>
</Fragment>
`;
exports[`TypeRow snapshot snapshot: renders type row setting card not selected 1`] = `
<Fragment>
<Container
className="d-flex"
fluid={true}
onClick={[MockFunction typeRowHooks.onClick]}
role="button"
size="xl"
>
<span
className="flex-grow-1"
>
Text Input Problem
</span>
<span
hidden={false}
>
<Icon
className="text-success"
/>
</span>
</Container>
<hr
className="d-block"
/>
</Fragment>
`;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Col, Container, Row } from '@edx/paragon';
import AnswerWidget from './AnswerWidget';
import SettingsWidget from './SettingsWidget';
import QuestionWidget from './QuestionWidget';
import { EditorContainer } from '../../../EditorContainer';
import { selectors } from '../../../../data/redux';
import ReactStateSettingsParser from '../../data/ReactStateSettingsParser';
import ReactStateOLXParser from '../../data/ReactStateOLXParser';
export const EditProblemView = ({
problemType,
problemState,
}) => {
const parseState = (problem) => () => {
const reactSettingsParser = new ReactStateSettingsParser(problem);
const reactOLXParser = new ReactStateOLXParser({ problem });
return {
settings: reactSettingsParser.getSettings(),
olx: reactOLXParser.buildOLX(),
};
};
return (
<EditorContainer getContent={parseState(problemState)}>
<Container fluid>
<Row>
<Col xs={9}>
<QuestionWidget />
<AnswerWidget problemType={problemType} />
</Col>
<Col xs={3}>
<SettingsWidget problemType={problemType} />
</Col>
</Row>
</Container>
</EditorContainer>
);
};
EditProblemView.propTypes = {
problemType: PropTypes.string.isRequired,
// eslint-disable-next-line
problemState: PropTypes.any.isRequired,
};
export const mapStateToProps = (state) => ({
problemType: selectors.problem.problemType(state),
problemState: selectors.problem.completeState(state),
});
export default connect(mapStateToProps)(EditProblemView);

View File

@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
ActionRow,
Button,
ModalDialog,
} from '@edx/paragon';
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
import { actions } from '../../../../../data/redux';
export const SelectTypeFooter = ({
selected,
onCancel,
// Redux
onSelect,
}) => (
<div className="editor-footer" style={{ position: 'sticky', bottom: 0 }}>
<ModalDialog.Footer className="border-top-0">
<ActionRow>
<ActionRow.Spacer />
<Button
aria-label="TODO: CANCEL"
variant="tertiary"
onClick={onCancel}
>
<FormattedMessage {...'TODO-CANCEL'} />
</Button>
<Button
aria-label="TODO: SELECT"
onClick={onSelect(selected)}
disabled={!selected}
>
<FormattedMessage {...'TODO- SELECT'} />
</Button>
</ActionRow>
</ModalDialog.Footer>
</div>
);
SelectTypeFooter.defaultProps = {
selected: null,
};
SelectTypeFooter.propTypes = {
onCancel: PropTypes.func.isRequired,
selected: PropTypes.string,
onSelect: PropTypes.func.isRequired,
};
export const mapStateToProps = () => ({
});
export const mapDispatchToProps = {
initializeEditor: actions.problem.onSelect,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SelectTypeFooter));

View File

@@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Icon, ModalDialog, IconButton } from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import SelectTypeFooter from './SelectTypeFooter';
import * as hooks from '../../../../EditorContainer/hooks';
export const SelectTypeWrapper = ({
selected,
onClose,
children,
}) => {
const handleCancelClicked = hooks.handleCancelClicked({ onClose });
return (
<div>
<ModalDialog.Header>
<ModalDialog.Title>
<p>Select Problem type</p>
<div className="pgn__modal-close-container">
<IconButton
src={Close}
iconAs={Icon}
onClick={handleCancelClicked}
/>
</div>
</ModalDialog.Title>
</ModalDialog.Header>
{children}
<SelectTypeFooter
selected={selected}
onCancel={handleCancelClicked}
/>
</div>
);
};
SelectTypeWrapper.defaultProps = {
onClose: null,
};
SelectTypeWrapper.propTypes = {
selected: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
onClose: PropTypes.func,
};
export default SelectTypeWrapper;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ProblemTypes } from '../../../../../data/constants/problem';
const Preview = ({
problemType,
}) => {
if (problemType === null) {
return null;
}
const data = ProblemTypes[problemType];
return (
<div>
<div>
<p>{data.title}</p>
</div>
<div>
{data.preview}
</div>
<div>
<p>{data.description}</p>
</div>
<div>
<p>{data.helpLink}</p>
</div>
</div>
);
};
Preview.defaultProps = {
problemType: null,
};
Preview.propTypes = {
problemType: PropTypes.string,
};
export default Preview;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form } from '@edx/paragon';
import { ProblemTypes } from '../../../../../data/constants/problem';
// TODO: problemtype
const ProblemTypeSelect = ({
// redux
setSelected,
}) => {
const handleChange = e => setSelected(e.target.value);
return (
<Form.Group>
<Form.RadioSet
name="problemtype"
onChange={handleChange}
>
<Form.Radio value={ProblemTypes.SINGLESELECT}>{ProblemTypes.SINGLESELECT.title}</Form.Radio>
<Form.Radio value={ProblemTypes.MULTISELECT}>{ProblemTypes.MULTISELECT.title}</Form.Radio>
<Form.Radio value={ProblemTypes.DROPDOWN}>{ProblemTypes.DROPDOWN.title}</Form.Radio>
<Form.Radio value={ProblemTypes.NUMERIC}>{ProblemTypes.NUMERIC.title}</Form.Radio>
<Form.Radio value={ProblemTypes.TEXTINPUT}>{ProblemTypes.TEXTINPUT.title}</Form.Radio>
</Form.RadioSet>
</Form.Group>
);
};
ProblemTypeSelect.propTypes = {
setSelected: PropTypes.func.isRequired,
};
export default ProblemTypeSelect;

View File

@@ -0,0 +1,10 @@
import {
useState,
} from 'react';
import { StrictDict } from '../../../../utils';
export const state = StrictDict({
selected: (val) => useState(val),
});
export default { state };

View File

@@ -0,0 +1,23 @@
import React from 'react';
import ProblemTypeSelect from './content/ProblemTypeSelect';
import Preview from './content/Preview';
import SelectTypeWrapper from './SelectTypeWrapper';
import * as hooks from './hooks';
export const SelectTypeModal = () => {
const { selected, setSelected } = hooks.state.selected(null);
return (
<div>
<SelectTypeWrapper selected={selected}>
<ProblemTypeSelect setSelected={setSelected} />
<Preview
problemType={selected}
/>
</SelectTypeWrapper>
</div>
);
};
export default SelectTypeModal;

View File

@@ -0,0 +1,397 @@
// Parse OLX to JavaScript objects.
/* eslint no-eval: 0 */
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
import _ from 'lodash-es';
import { ProblemTypeKeys } from '../../../data/constants/problem';
export const indexToLetterMap = [...Array(26)].map((val, i) => String.fromCharCode(i + 65));
export const nonQuestionKeys = [
'responseparam',
'formulaequationinput',
'correcthint',
'@_answer',
'optioninput',
'checkboxgroup',
'choicegroup',
'additional_answer',
'stringequalhint',
'textline',
'@_type',
'formulaequationinput',
'numericalresponse',
'demandhint',
];
export class OLXParser {
constructor(olxString) {
this.problem = {};
const options = {
ignoreAttributes: false,
alwaysCreateTextNode: true,
// preserveOrder: true
};
const parser = new XMLParser(options);
this.parsedOLX = parser.parse(olxString);
if (_.has(this.parsedOLX, 'problem')) {
this.problem = this.parsedOLX.problem;
}
}
parseMultipleChoiceAnswers(problemType, widgetName, option) {
const answers = [];
let data = {};
const widget = _.get(this.problem, `${problemType}.${widgetName}`);
const choice = _.get(widget, option);
if (_.isArray(choice)) {
choice.forEach((element, index) => {
const title = element['#text'];
const correct = eval(element['@_correct'].toLowerCase());
const id = indexToLetterMap[index];
const feedback = this.getAnswerFeedback(element, `${option}hint`);
answers.push(
{
id,
title,
correct,
...feedback,
},
);
});
} else {
const feedback = this.getAnswerFeedback(choice, `${option}hint`);
answers.push({
title: choice['#text'],
correct: eval(choice['@_correct'].toLowerCase()),
id: indexToLetterMap[answers.length],
...feedback,
});
}
data = { answers };
const groupFeedbackList = this.getGroupedFeedback(widget);
if (groupFeedbackList.length) {
data = {
...data,
groupFeedbackList,
};
}
return data;
}
getAnswerFeedback(choice, hintKey) {
let feedback = {};
let feedbackKeys = 'feedback';
if (_.has(choice, hintKey)) {
const answerFeedback = choice[hintKey];
if (_.isArray(answerFeedback)) {
answerFeedback.forEach((element) => {
if (_.has(element, '@_selected')) {
feedbackKeys = eval(element['@_selected'].toLowerCase()) ? 'selectedFeedback' : 'unselectedFeedback';
}
feedback = {
...feedback,
[feedbackKeys]: element['#text'],
};
});
} else {
if (_.has(answerFeedback, '@_selected')) {
feedbackKeys = eval(answerFeedback['@_selected'].toLowerCase()) ? 'selectedFeedback' : 'unselectedFeedback';
}
feedback = {
[feedbackKeys]: answerFeedback['#text'],
};
}
}
return feedback;
}
getGroupedFeedback(choices) {
const groupFeedback = [];
if (_.has(choices, 'compoundhint')) {
const groupFeedbackArray = choices.compoundhint;
if (_.isArray(groupFeedbackArray)) {
groupFeedbackArray.forEach((element) => {
groupFeedback.push({
id: groupFeedback.length,
answers: element['@_value'].split(' '),
feedback: element['#text'],
});
});
} else {
groupFeedback.push({
id: groupFeedback.length,
answers: groupFeedbackArray['@_value'].split(' '),
feedback: groupFeedbackArray['#text'],
});
}
}
return groupFeedback;
}
parseStringResponse() {
const { stringresponse } = this.problem;
const answers = [];
let answerFeedback = '';
let additionalStringAttributes = {};
let data = {};
const feedback = this.getFeedback(stringresponse);
answers.push({
id: indexToLetterMap[answers.length],
title: stringresponse['@_answer'],
correct: true,
feedback,
});
// Parsing additional_answer for string response.
const additionalAnswer = _.get(stringresponse, 'additional_answer', []);
if (_.isArray(additionalAnswer)) {
additionalAnswer.forEach((newAnswer) => {
answerFeedback = this.getFeedback(newAnswer);
answers.push({
id: indexToLetterMap[answers.length],
title: newAnswer['@_answer'],
correct: true,
feedback: answerFeedback,
});
});
} else {
answerFeedback = this.getFeedback(additionalAnswer);
answers.push({
id: indexToLetterMap[answers.length],
title: additionalAnswer['@_answer'],
correct: true,
feedback: answerFeedback,
});
}
// Parsing stringequalhint for string response.
const stringEqualHint = _.get(stringresponse, 'stringequalhint', []);
if (_.isArray(stringEqualHint)) {
stringEqualHint.forEach((newAnswer) => {
answers.push({
id: indexToLetterMap[answers.length],
title: newAnswer['@_answer'],
correct: false,
feedback: newAnswer['#text'],
});
});
} else {
answers.push({
id: indexToLetterMap[answers.length],
title: stringEqualHint['@_answer'],
correct: false,
feedback: stringEqualHint['#text'],
});
}
// TODO: Support multiple types.
additionalStringAttributes = {
type: _.get(stringresponse, '@_type'),
textline: {
size: _.get(stringresponse, 'textline.@_size'),
},
};
data = {
answers,
additionalStringAttributes,
};
return data;
}
parseNumericResponse() {
const { numericalresponse } = this.problem;
let answers = [];
let subAnswers = [];
let data = {};
// TODO: Find a way to add answers using additional_answers v/s numericalresponse
if (_.isArray(numericalresponse)) {
numericalresponse.forEach((numericalAnswer) => {
subAnswers = this.parseNumericResponseObject(numericalAnswer, answers.length);
answers = _.concat(answers, subAnswers);
});
} else {
subAnswers = this.parseNumericResponseObject(numericalresponse, answers.length);
answers = _.concat(answers, subAnswers);
}
data = {
answers,
};
return data;
}
parseNumericResponseObject(numericalresponse, answerOffset) {
let answerFeedback = '';
const answers = [];
let responseParam = {};
// TODO: UI needs to be added to support adding tolerence in numeric response.
const feedback = this.getFeedback(numericalresponse);
if (_.has(numericalresponse, 'responseparam')) {
const type = _.get(numericalresponse, 'responseparam.@_type');
const defaultValue = _.get(numericalresponse, 'responseparam.@_default');
responseParam = {
[type]: defaultValue,
};
}
answers.push({
id: indexToLetterMap[answers.length + answerOffset],
title: numericalresponse['@_answer'],
correct: true,
feedback,
...responseParam,
});
// Parsing additional_answer for numerical response.
const additionalAnswer = _.get(numericalresponse, 'additional_answer', []);
if (_.isArray(additionalAnswer)) {
additionalAnswer.forEach((newAnswer) => {
answerFeedback = this.getFeedback(newAnswer);
answers.push({
id: indexToLetterMap[answers.length + answerOffset],
title: newAnswer['@_answer'],
correct: true,
feedback: answerFeedback,
});
});
} else {
answerFeedback = this.getFeedback(additionalAnswer);
answers.push({
id: indexToLetterMap[answers.length + answerOffset],
title: additionalAnswer['@_answer'],
correct: true,
feedback: answerFeedback,
});
}
return answers;
}
parseQuestions(problemType) {
const builder = new XMLBuilder();
const problemObject = _.get(this.problem, problemType);
let questionObject = {};
/* TODO: How do we uniquely identify the label and description?
In order to parse label and description, there should be two states
and settings should be introduced to edit the label and description.
In turn editing the settings update the state and then it can be added to
the parsed OLX.
*/
const tagMap = {
label: 'bold',
description: 'em',
};
/* Only numerical response has different ways to generate OLX, test with
numericInputWithFeedbackAndHintsOLXException and numericInputWithFeedbackAndHintsOLX
shows the different ways the olx can be generated.
*/
if (_.isArray(problemObject)) {
questionObject = _.omitBy(problemObject[0], (value, key) => _.includes(nonQuestionKeys, key));
} else {
questionObject = _.omitBy(problemObject, (value, key) => _.includes(nonQuestionKeys, key));
}
// Check if problem tag itself will have question and descriptions.
if (_.isEmpty(questionObject)) {
questionObject = _.omitBy(this.problem, (value, key) => _.includes(nonQuestionKeys, key));
}
const serializedQuestion = _.mapKeys(questionObject, (value, key) => _.get(tagMap, key, key));
const questionString = builder.build(serializedQuestion);
return questionString;
}
getHints() {
const hintsObject = [];
if (_.has(this.problem, 'demandhint.hint')) {
const hint = _.get(this.problem, 'demandhint.hint');
if (_.isArray(hint)) {
hint.forEach(element => {
hintsObject.push({
id: hintsObject.length,
value: element['#text'],
});
});
} else {
hintsObject.push({
id: hintsObject.length,
value: hint['#text'],
});
}
}
return hintsObject;
}
getFeedback(xmlElement) {
return _.has(xmlElement, 'correcthint') ? _.get(xmlElement, 'correcthint.#text') : '';
}
getProblemType() {
const problemKeys = Object.keys(this.problem);
const intersectedProblems = _.intersection(Object.values(ProblemTypeKeys), problemKeys);
if (intersectedProblems.length > 1) {
const errorMessage = {
code: 500,
message: 'More than one problem type is not supported!',
};
throw errorMessage;
}
const problemType = intersectedProblems[0];
return problemType;
}
getParsedOLXData() {
if (_.isEmpty(this.problem)) {
return {};
}
let answersObject = {};
let additionalAttributes = {};
let groupFeedbackList = [];
const problemType = this.getProblemType();
const hints = this.getHints();
const question = this.parseQuestions(problemType);
const errorMessage = {
code: 500,
message: 'The problem type is not supported!',
};
switch (problemType) {
case ProblemTypeKeys.DROPDOWN:
answersObject = this.parseMultipleChoiceAnswers(ProblemTypeKeys.DROPDOWN, 'optioninput', 'option');
break;
case ProblemTypeKeys.TEXTINPUT:
answersObject = this.parseStringResponse();
break;
case ProblemTypeKeys.NUMERIC:
answersObject = this.parseNumericResponse();
break;
case ProblemTypeKeys.MULTISELECT:
answersObject = this.parseMultipleChoiceAnswers(ProblemTypeKeys.MULTISELECT, 'checkboxgroup', 'choice');
break;
case ProblemTypeKeys.SINGLESELECT:
answersObject = this.parseMultipleChoiceAnswers(ProblemTypeKeys.SINGLESELECT, 'choicegroup', 'choice');
break;
default:
throw errorMessage;
}
if (_.has(answersObject, 'additionalStringAttributes')) {
additionalAttributes = { ...answersObject.additionalStringAttributes };
}
if (_.has(answersObject, 'groupFeedbackList')) {
groupFeedbackList = answersObject.groupFeedbackList;
}
const { answers } = answersObject;
const settings = { hints };
return {
question,
settings,
answers,
problemType,
additionalAttributes,
groupFeedbackList,
};
}
}

View File

@@ -0,0 +1,138 @@
import { OLXParser } from './OLXParser';
import {
checkboxesOLXWithFeedbackAndHintsOLX,
dropdownOLXWithFeedbackAndHintsOLX,
numericInputWithFeedbackAndHintsOLX,
numericInputWithFeedbackAndHintsOLXException,
textInputWithFeedbackAndHintsOLX,
mutlipleChoiceWithFeedbackAndHintsOLX,
textInputWithFeedbackAndHintsOLXWithMultipleAnswers,
} from './mockData/olxTestData';
import { ProblemTypeKeys } from '../../../data/constants/problem';
describe('Check OLXParser problem type', () => {
test('Test checkbox with feedback and hints problem type', () => {
const olxparser = new OLXParser(checkboxesOLXWithFeedbackAndHintsOLX.rawOLX);
const problemType = olxparser.getProblemType();
expect(problemType).toBe(ProblemTypeKeys.MULTISELECT);
});
test('Test numeric problem type', () => {
const olxparser = new OLXParser(numericInputWithFeedbackAndHintsOLX.rawOLX);
const problemType = olxparser.getProblemType();
expect(problemType).toBe(ProblemTypeKeys.NUMERIC);
});
test('Test dropdown with feedback and hints problem type', () => {
const olxparser = new OLXParser(dropdownOLXWithFeedbackAndHintsOLX.rawOLX);
const problemType = olxparser.getProblemType();
expect(problemType).toBe(ProblemTypeKeys.DROPDOWN);
});
test('Test multiple choice with feedback and hints problem type', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const problemType = olxparser.getProblemType();
expect(problemType).toBe(ProblemTypeKeys.SINGLESELECT);
});
test('Test textual problem type', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLX.rawOLX);
const problemType = olxparser.getProblemType();
expect(problemType).toBe(ProblemTypeKeys.TEXTINPUT);
});
});
describe('Check OLXParser hints', () => {
test('Test checkbox hints', () => {
const olxparser = new OLXParser(checkboxesOLXWithFeedbackAndHintsOLX.rawOLX);
const hints = olxparser.getHints();
expect(hints).toEqual(checkboxesOLXWithFeedbackAndHintsOLX.hints);
});
test('Test numeric hints', () => {
const olxparser = new OLXParser(numericInputWithFeedbackAndHintsOLX.rawOLX);
const hints = olxparser.getHints();
expect(hints).toEqual(numericInputWithFeedbackAndHintsOLX.hints);
});
test('Test dropdown with feedback and hints problem type', () => {
const olxparser = new OLXParser(dropdownOLXWithFeedbackAndHintsOLX.rawOLX);
const hints = olxparser.getHints();
expect(hints).toEqual(dropdownOLXWithFeedbackAndHintsOLX.hints);
});
test('Test multiple choice with feedback and hints problem type', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const hints = olxparser.getHints();
expect(hints).toEqual(mutlipleChoiceWithFeedbackAndHintsOLX.hints);
});
test('Test textual problem type', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLX.rawOLX);
const hints = olxparser.getHints();
expect(hints).toEqual(textInputWithFeedbackAndHintsOLX.hints);
});
});
describe('Check OLXParser for answer parsing', () => {
test('Test checkbox answer', () => {
const olxparser = new OLXParser(checkboxesOLXWithFeedbackAndHintsOLX.rawOLX);
const answer = olxparser.parseMultipleChoiceAnswers('choiceresponse', 'checkboxgroup', 'choice');
expect(answer).toEqual(checkboxesOLXWithFeedbackAndHintsOLX.data);
});
test('Test dropdown answer', () => {
const olxparser = new OLXParser(dropdownOLXWithFeedbackAndHintsOLX.rawOLX);
const answer = olxparser.parseMultipleChoiceAnswers('optionresponse', 'optioninput', 'option');
expect(answer).toEqual(dropdownOLXWithFeedbackAndHintsOLX.data);
});
test('Test multiple choice single select', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const answer = olxparser.parseMultipleChoiceAnswers('multiplechoiceresponse', 'choicegroup', 'choice');
expect(answer).toEqual(mutlipleChoiceWithFeedbackAndHintsOLX.data);
});
test('Test string response answers', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLX.rawOLX);
const answer = olxparser.parseStringResponse();
expect(answer).toEqual(textInputWithFeedbackAndHintsOLX.data);
});
test('Test string response answers with multiple answers', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLXWithMultipleAnswers.rawOLX);
const answer = olxparser.parseStringResponse();
expect(answer).toEqual(textInputWithFeedbackAndHintsOLXWithMultipleAnswers.data);
});
test('Test numerical response answers', () => {
const olxparser = new OLXParser(numericInputWithFeedbackAndHintsOLX.rawOLX);
const answer = olxparser.parseNumericResponse();
expect(answer).toEqual(numericInputWithFeedbackAndHintsOLX.data);
});
test('Test numerical response answers exception', () => {
const olxparser = new OLXParser(numericInputWithFeedbackAndHintsOLXException.rawOLX);
const answer = olxparser.parseNumericResponse();
expect(answer).toEqual(numericInputWithFeedbackAndHintsOLXException.data);
});
});
describe('Check OLXParser for question parsing', () => {
test('Test checkbox question', () => {
const olxparser = new OLXParser(checkboxesOLXWithFeedbackAndHintsOLX.rawOLX);
const question = olxparser.parseQuestions('choiceresponse');
expect(question).toEqual(checkboxesOLXWithFeedbackAndHintsOLX.question);
});
test('Test dropdown question', () => {
const olxparser = new OLXParser(dropdownOLXWithFeedbackAndHintsOLX.rawOLX);
const question = olxparser.parseQuestions('optionresponse');
expect(question).toEqual(dropdownOLXWithFeedbackAndHintsOLX.question);
});
test('Test multiple choice single select question', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const question = olxparser.parseQuestions('multiplechoiceresponse');
expect(question).toEqual(mutlipleChoiceWithFeedbackAndHintsOLX.question);
});
test('Test string response question', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLX.rawOLX);
const question = olxparser.parseQuestions('stringresponse');
expect(question).toEqual(textInputWithFeedbackAndHintsOLX.question);
});
test('Test numerical response question', () => {
const olxparser = new OLXParser(numericInputWithFeedbackAndHintsOLX.rawOLX);
const question = olxparser.parseQuestions('numericalresponse');
expect(question).toEqual(numericInputWithFeedbackAndHintsOLX.question);
});
test('Test numerical response question exception', () => {
const olxparser = new OLXParser(numericInputWithFeedbackAndHintsOLXException.rawOLX);
const question = olxparser.parseQuestions('numericalresponse');
expect(question).toEqual(numericInputWithFeedbackAndHintsOLXException.question);
});
});

View File

@@ -0,0 +1,281 @@
import _ from 'lodash-es';
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
import { ProblemTypeKeys } from '../../../data/constants/problem';
class ReactStateOLXParser {
constructor(problemState) {
const parserOptions = {
ignoreAttributes: false,
alwaysCreateTextNode: true,
// preserveOrder: true
};
const builderOptions = {
ignoreAttributes: false,
attributeNamePrefix: '@_',
suppressBooleanAttributes: false,
format: true,
};
this.parser = new XMLParser(parserOptions);
this.builder = new XMLBuilder(builderOptions);
this.problemState = problemState.problem;
}
addHints() {
const hintsArray = [];
const hints = _.get(this.problemState, 'settings.hints', []);
hints.forEach(element => {
hintsArray.push({
'#text': element.value,
});
});
const demandhint = {
demandhint: {
hint: hintsArray,
},
};
return demandhint;
}
addMultiSelectAnswers(option) {
const choice = [];
let compoundhint = [];
let widget = {};
const { answers } = this.problemState;
answers.forEach((answer) => {
const feedback = [];
let singleAnswer = {};
if (this.hasAttributeWithValue(answer, 'title')) {
if (this.hasAttributeWithValue(answer, 'selectedFeedback')) {
feedback.push({
'#text': _.get(answer, 'selectedFeedback'),
'@_selected': true,
});
}
if (this.hasAttributeWithValue(answer, 'unselectedFeedback')) {
feedback.push({
'#text': _.get(answer, 'unselectedFeedback'),
'@_selected': false,
});
}
if (this.hasAttributeWithValue(answer, 'feedback')) {
feedback.push({
'#text': _.get(answer, 'feedback'),
});
}
if (feedback.length) {
singleAnswer[`${option}hint`] = feedback;
}
singleAnswer = {
'#text': answer.title,
'@_correct': answer.correct,
...singleAnswer,
};
choice.push(singleAnswer);
}
});
widget = { [option]: choice };
if (_.has(this.problemState, 'groupFeedbackList')) {
compoundhint = this.addGroupFeedbackList();
widget = {
...widget,
compoundhint,
};
}
return widget;
}
addGroupFeedbackList() {
const compoundhint = [];
const { groupFeedbackList } = this.problemState;
groupFeedbackList.forEach((element) => {
compoundhint.push({
'#text': element.feedback,
'@_value': element.answers.join(' '),
});
});
return compoundhint;
}
addQuestion() {
const { question } = this.problemState;
const questionObject = this.parser.parse(question);
return questionObject;
}
buildMultiSelectProblem(problemType, widget, option) {
const question = this.addQuestion();
const widgetObject = this.addMultiSelectAnswers(option);
const demandhint = this.addHints();
const problemObject = {
problem: {
[problemType]: {
...question,
[widget]: widgetObject,
},
...demandhint,
},
};
return this.builder.build(problemObject);
}
buildTextInput() {
const question = this.addQuestion();
const demandhint = this.addHints();
const answerObject = this.buildTextInputAnswersFeedback();
const problemObject = {
problem: {
[ProblemTypeKeys.TEXTINPUT]: {
...question,
...answerObject,
},
...demandhint,
},
};
return this.builder.build(problemObject);
}
buildTextInputAnswersFeedback() {
const { answers } = this.problemState;
let answerObject = {};
const additionAnswers = [];
const wrongAnswers = [];
let firstCorrectAnswerParsed = false;
answers.forEach((answer) => {
const correcthint = this.getAnswerHints(answer);
if (this.hasAttributeWithValue(answer, 'title')) {
if (answer.correct && firstCorrectAnswerParsed) {
additionAnswers.push({
'@_answer': answer.title,
...correcthint,
});
} else if (answer.correct && !firstCorrectAnswerParsed) {
firstCorrectAnswerParsed = true;
answerObject = {
'@_answer': answer.title,
...correcthint,
};
} else if (!answer.correct) {
wrongAnswers.push({
'@_answer': answer.title,
'#text': answer.feedback,
});
}
}
});
answerObject = {
...answerObject,
additional_answer: additionAnswers,
stringequalhint: wrongAnswers,
'@_type': _.get(this.problemState, 'additionalAttributes.type', 'ci'),
textline: {
'@_size': _.get(this.problemState, 'additionalAttributes.textline.size', 20),
},
};
return answerObject;
}
buildNumericInput() {
const question = this.addQuestion();
const demandhint = this.addHints();
const answerObject = this.buildNumericalResponse();
const problemObject = {
problem: {
...question,
[ProblemTypeKeys.NUMERIC]: answerObject,
...demandhint,
},
};
return this.builder.build(problemObject);
}
buildNumericalResponse() {
const { answers } = this.problemState;
let answerObject = {};
const additionalAnswers = [];
let firstCorrectAnswerParsed = false;
/*
TODO: Need to figure out how to add multiple numericalresponse,
the parser right now converts all the other right answers into
additional answers.
*/
answers.forEach((answer) => {
const correcthint = this.getAnswerHints(answer);
if (this.hasAttributeWithValue(answer, 'title')) {
if (answer.correct && !firstCorrectAnswerParsed) {
firstCorrectAnswerParsed = true;
let responseParam = {};
if (_.has(answer, 'tolerance')) {
responseParam = {
responseparam: {
'@_type': 'tolerance',
'@_default': _.get(answer, 'tolerance', 0),
},
};
}
answerObject = {
'@_answer': answer.title,
...responseParam,
...correcthint,
};
} else if (answer.correct && firstCorrectAnswerParsed) {
additionalAnswers.push({
'@_answer': answer.title,
...correcthint,
});
}
}
});
answerObject = {
...answerObject,
additional_answer: additionalAnswers,
formulaequationinput: {
'#text': '',
},
};
return answerObject;
}
getAnswerHints(elementObject) {
const feedback = elementObject?.feedback;
let correcthint = {};
if (feedback !== undefined && feedback !== '') {
correcthint = {
correcthint: {
'#text': feedback,
},
};
}
return correcthint;
}
hasAttributeWithValue(obj, attr) {
return _.has(obj, attr) && _.get(obj, attr, '').trim() !== '';
}
buildOLX() {
const { problemType } = this.problemState;
let problemString = '';
switch (problemType) {
case ProblemTypeKeys.MULTISELECT:
problemString = this.buildMultiSelectProblem(ProblemTypeKeys.MULTISELECT, 'checkboxgroup', 'choice');
break;
case ProblemTypeKeys.DROPDOWN:
problemString = this.buildMultiSelectProblem(ProblemTypeKeys.DROPDOWN, 'optioninput', 'option');
break;
case ProblemTypeKeys.SINGLESELECT:
problemString = this.buildMultiSelectProblem(ProblemTypeKeys.SINGLESELECT, 'choicegroup', 'choice');
break;
case ProblemTypeKeys.TEXTINPUT:
problemString = this.buildTextInput();
break;
case ProblemTypeKeys.NUMERIC:
problemString = this.buildNumericInput();
break;
default:
break;
}
return problemString;
}
}
export default ReactStateOLXParser;

View File

@@ -0,0 +1,63 @@
import { OLXParser } from './OLXParser';
import {
checkboxesOLXWithFeedbackAndHintsOLX,
dropdownOLXWithFeedbackAndHintsOLX,
numericInputWithFeedbackAndHintsOLX,
numericInputWithFeedbackAndHintsOLXException,
textInputWithFeedbackAndHintsOLX,
mutlipleChoiceWithFeedbackAndHintsOLX,
textInputWithFeedbackAndHintsOLXWithMultipleAnswers,
} from './mockData/olxTestData';
import ReactStateOLXParser from './ReactStateOLXParser';
describe('Check React Sate OLXParser problem', () => {
test('Test checkbox with feedback and hints problem type', () => {
const olxparser = new OLXParser(checkboxesOLXWithFeedbackAndHintsOLX.rawOLX);
const problem = olxparser.getParsedOLXData();
const stateParser = new ReactStateOLXParser({ problem });
const buildOLX = stateParser.buildOLX();
expect(buildOLX).toEqual(checkboxesOLXWithFeedbackAndHintsOLX.buildOLX);
});
test('Test dropdown with feedback and hints problem type', () => {
const olxparser = new OLXParser(dropdownOLXWithFeedbackAndHintsOLX.rawOLX);
const problem = olxparser.getParsedOLXData();
const stateParser = new ReactStateOLXParser({ problem });
const buildOLX = stateParser.buildOLX();
expect(buildOLX).toEqual(dropdownOLXWithFeedbackAndHintsOLX.buildOLX);
});
test('Test string response with feedback and hints problem type', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLX.rawOLX);
const problem = olxparser.getParsedOLXData();
const stateParser = new ReactStateOLXParser({ problem });
const buildOLX = stateParser.buildOLX();
expect(buildOLX).toEqual(textInputWithFeedbackAndHintsOLX.buildOLX);
});
test('Test multiple choice with feedback and hints problem type', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const problem = olxparser.getParsedOLXData();
const stateParser = new ReactStateOLXParser({ problem });
const buildOLX = stateParser.buildOLX();
expect(buildOLX).toEqual(mutlipleChoiceWithFeedbackAndHintsOLX.buildOLX);
});
test('Test numerical response with feedback and hints problem type', () => {
const olxparser = new OLXParser(numericInputWithFeedbackAndHintsOLX.rawOLX);
const problem = olxparser.getParsedOLXData();
const stateParser = new ReactStateOLXParser({ problem });
const buildOLX = stateParser.buildOLX();
expect(buildOLX).toEqual(numericInputWithFeedbackAndHintsOLX.buildOLX);
});
test('Test numerical response with feedback and hints problem type with exception', () => {
const olxparser = new OLXParser(numericInputWithFeedbackAndHintsOLXException.rawOLX);
const problem = olxparser.getParsedOLXData();
const stateParser = new ReactStateOLXParser({ problem });
const buildOLX = stateParser.buildOLX();
expect(buildOLX).toEqual(numericInputWithFeedbackAndHintsOLXException.buildOLX);
});
test('Test string response with feedback and hints, multiple answers', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLXWithMultipleAnswers.rawOLX);
const problem = olxparser.getParsedOLXData();
const stateParser = new ReactStateOLXParser({ problem });
const buildOLX = stateParser.buildOLX();
expect(buildOLX).toEqual(textInputWithFeedbackAndHintsOLXWithMultipleAnswers.buildOLX);
});
});

View File

@@ -0,0 +1,24 @@
import { popuplateItem } from './SettingsParser';
class ReactStateSettingsParser {
constructor(problemState) {
this.problemState = problemState;
}
getSettings() {
let settings = {};
const stateSettings = this.problemState.settings;
settings = popuplateItem(settings, 'matLabApiKey', 'matlab_api_key', stateSettings);
settings = popuplateItem(settings, 'number', 'max_attempts', stateSettings.scoring.attempts);
settings = popuplateItem(settings, 'weight', 'weight', stateSettings.scoring);
settings = popuplateItem(settings, 'on', 'showanswer', stateSettings.showAnswer);
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);
return settings;
}
}
export default ReactStateSettingsParser;

View File

@@ -0,0 +1,12 @@
import ReactStateSettingsParser from './ReactStateSettingsParser';
import {
checklistWithFeebackHints,
} from './mockData/problemTestData';
describe('Test State to Settings Parser', () => {
test('Test settings parsed from react state', () => {
const settings = new ReactStateSettingsParser(checklistWithFeebackHints.state).getSettings();
const { markdown, ...settingsPayload } = checklistWithFeebackHints.metadata;
expect(settings).toStrictEqual(settingsPayload);
});
});

View File

@@ -0,0 +1,67 @@
import _ from 'lodash-es';
import { ShowAnswerTypes } from '../../../data/constants/problem';
export const popuplateItem = (parentObject, itemName, statekey, metadata) => {
let parent = parentObject;
const item = _.get(metadata, itemName, null);
if (!_.isNil(item)) {
parent = { ...parentObject, [statekey]: item };
}
return parent;
};
export const parseScoringSettings = (metadata) => {
let scoring = {};
let attempts = popuplateItem({}, 'max_attempts', 'number', metadata);
if (!_.isEmpty(attempts)) {
let unlimited = true;
if (attempts.number > 0) {
unlimited = false;
}
attempts = { ...attempts, unlimited };
scoring = { ...scoring, attempts };
}
scoring = popuplateItem(scoring, 'weight', 'weight', metadata);
return scoring;
};
export const parseShowAnswer = (metadata) => {
let showAnswer = {};
const showAnswerType = _.get(metadata, 'showanswer', {});
if (!_.isNil(showAnswerType) && showAnswerType in ShowAnswerTypes) {
showAnswer = { ...showAnswer, on: showAnswerType };
}
showAnswer = popuplateItem(showAnswer, 'attempts_before_showanswer_button', 'afterAttempts', metadata);
return showAnswer;
};
export const parseSettings = (metadata) => {
let settings = {};
if (_.isNil(metadata) || _.isEmpty(metadata)) {
return settings;
}
settings = popuplateItem(settings, 'matlab_api_key', 'matLabApiKey', metadata);
const scoring = parseScoringSettings(metadata);
if (!_.isEmpty(scoring)) {
settings = { ...settings, scoring };
}
const showAnswer = parseShowAnswer(metadata);
if (!_.isEmpty(showAnswer)) {
settings = { ...settings, showAnswer };
}
settings = popuplateItem(settings, 'show_reset_button', 'showResetButton', metadata);
settings = popuplateItem(settings, 'submission_wait_seconds', 'timeBetween', metadata);
return settings;
};

View File

@@ -0,0 +1,69 @@
import { parseScoringSettings, parseSettings, parseShowAnswer } from './SettingsParser';
import {
checklistWithFeebackHints,
dropdownWithFeedbackHints,
numericWithHints,
textInputWithHints,
sigleSelectWithHints,
} from './mockData/problemTestData';
describe('Test Settings to State Parser', () => {
test('Test all fields populated', () => {
const settings = parseSettings(checklistWithFeebackHints.metadata);
const { hints, ...settingsPayload } = checklistWithFeebackHints.state.settings;
expect(settings).toStrictEqual(settingsPayload);
});
test('Test partial fields populated', () => {
const settings = parseSettings(dropdownWithFeedbackHints.metadata);
const { hints, ...settingsPayload } = dropdownWithFeedbackHints.state.settings;
expect(settings).not.toStrictEqual(settingsPayload);
const { randomization, matLabApiKey, ...settingsPayloadPartial } = settingsPayload;
expect(settings).toStrictEqual(settingsPayloadPartial);
});
test('Test score settings', () => {
const scoreSettings = parseScoringSettings(checklistWithFeebackHints.metadata);
expect(scoreSettings).toStrictEqual(checklistWithFeebackHints.state.settings.scoring);
});
test('Test score settings zero attempts', () => {
const scoreSettings = parseScoringSettings(numericWithHints.metadata);
expect(scoreSettings).toStrictEqual(numericWithHints.state.settings.scoring);
});
test('Test score settings attempts missing', () => {
const scoreSettings = parseScoringSettings(textInputWithHints.metadata);
expect(scoreSettings.attempts).toBeUndefined();
});
test('Test score settings missing', () => {
const settings = parseSettings(sigleSelectWithHints.metadata);
expect(settings.scoring).toBeUndefined();
});
test('Test invalid randomization', () => {
const settings = parseSettings(numericWithHints.metadata);
expect(settings.randomization).toBeUndefined();
});
test('Test invalid show answer', () => {
const showAnswerSettings = parseShowAnswer(numericWithHints.metadata);
expect(showAnswerSettings.on).toBeUndefined();
});
test('Test show answer settings missing', () => {
const settings = parseShowAnswer(textInputWithHints.metadata);
expect(settings.showAnswer).toBeUndefined();
});
test('Test empty metadata', () => {
const scoreSettings = parseSettings({});
expect(scoreSettings).toStrictEqual({});
});
test('Test null metadata', () => {
const scoreSettings = parseSettings(null);
expect(scoreSettings).toStrictEqual({});
});
});

View File

@@ -0,0 +1,555 @@
export const checkboxesOLXWithFeedbackAndHintsOLX = {
rawOLX: `<problem>
<choiceresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this.</description>
<checkboxgroup>
<choice correct="true">a correct answer
<choicehint selected="true">You can specify optional feedback that appears after the learner selects and submits this answer.</choicehint>
<choicehint selected="false">You can specify optional feedback that appears after the learner clears and submits this answer.</choicehint>
</choice>
<choice correct="false">an incorrect answer</choice>
<choice correct="false">an incorrect answer
<choicehint selected="true">You can specify optional feedback for none, all, or a subset of the answers.</choicehint>
<choicehint selected="false">You can specify optional feedback for selected answers, cleared answers, or both.</choicehint>
</choice>
<choice correct="true">a correct answer</choice>
<compoundhint value="A B D">You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted.</compoundhint>
<compoundhint value="A B C D">You can specify optional feedback for one, several, or all answer combinations.</compoundhint>
</checkboxgroup>
</choiceresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>`,
hints: [{
id: 0,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 1,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
data: {
answers: [
{
id: 'A',
title: 'a correct answer',
correct: true,
selectedFeedback: 'You can specify optional feedback that appears after the learner selects and submits this answer.',
unselectedFeedback: 'You can specify optional feedback that appears after the learner clears and submits this answer.',
},
{
id: 'B',
title: 'an incorrect answer',
correct: false,
},
{
id: 'C',
title: 'an incorrect answer',
correct: false,
selectedFeedback: 'You can specify optional feedback for none, all, or a subset of the answers.',
unselectedFeedback: 'You can specify optional feedback for selected answers, cleared answers, or both.',
},
{
id: 'D',
title: 'a correct answer',
correct: true,
},
],
groupFeedbackList: [
{
id: 0,
answers: [
'A',
'B',
'D',
],
feedback: 'You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted.',
},
{
id: 1,
answers: [
'A',
'B',
'C',
'D',
],
feedback: 'You can specify optional feedback for one, several, or all answer combinations.',
},
],
},
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><bold>Add the question text, or prompt, here. This text is required.</bold><em>You can add an optional tip or note related to the prompt like this.</em>',
buildOLX: `<problem>
<choiceresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<bold>Add the question text, or prompt, here. This text is required.</bold>
<em>You can add an optional tip or note related to the prompt like this.</em>
<checkboxgroup>
<choice correct="true">
a correct answer <choicehint selected="true">You can specify optional feedback that appears after the learner selects and submits this answer.</choicehint>
<choicehint selected="false">You can specify optional feedback that appears after the learner clears and submits this answer.</choicehint>
</choice>
<choice correct="false">an incorrect answer</choice>
<choice correct="false">
an incorrect answer <choicehint selected="true">You can specify optional feedback for none, all, or a subset of the answers.</choicehint>
<choicehint selected="false">You can specify optional feedback for selected answers, cleared answers, or both.</choicehint>
</choice>
<choice correct="true">a correct answer</choice>
<compoundhint value="A B D">You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted.</compoundhint>
<compoundhint value="A B C D">You can specify optional feedback for one, several, or all answer combinations.</compoundhint>
</checkboxgroup>
</choiceresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>
`,
};
export const dropdownOLXWithFeedbackAndHintsOLX = {
rawOLX: `<problem>
<optionresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<optioninput>
<option correct="false">an incorrect answer <optionhint>You can specify optional feedback like this, which appears after this answer is submitted.</optionhint>
</option>
<option correct="true">the correct answer</option>
<option correct="false">an incorrect answer <optionhint>You can specify optional feedback for none, a subset, or all of the answers.</optionhint>
</option>
</optioninput>
</optionresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>`,
hints: [{
id: 0,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 1,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
data: {
answers: [
{
id: 'A',
title: 'an incorrect answer',
correct: false,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
},
{
id: 'B',
title: 'the correct answer',
correct: true,
},
{
id: 'C',
title: 'an incorrect answer',
correct: false,
feedback: 'You can specify optional feedback for none, a subset, or all of the answers.',
},
],
},
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><bold>Add the question text, or prompt, here. This text is required.</bold><em>You can add an optional tip or note related to the prompt like this.</em>',
buildOLX: `<problem>
<optionresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<bold>Add the question text, or prompt, here. This text is required.</bold>
<em>You can add an optional tip or note related to the prompt like this.</em>
<optioninput>
<option correct="false">
an incorrect answer <optionhint>You can specify optional feedback like this, which appears after this answer is submitted.</optionhint>
</option>
<option correct="true">the correct answer</option>
<option correct="false">
an incorrect answer <optionhint>You can specify optional feedback for none, a subset, or all of the answers.</optionhint>
</option>
</optioninput>
</optionresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>
`,
};
export const mutlipleChoiceWithFeedbackAndHintsOLX = {
rawOLX: `<problem>
<multiplechoiceresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<choicegroup type="MultipleChoice">
<choice correct="false">an incorrect answer <choicehint>You can specify optional feedback like this, which appears after this answer is submitted.</choicehint>
</choice>
<choice correct="true">the correct answer</choice>
<choice correct="false">an incorrect answer <choicehint>You can specify optional feedback for none, a subset, or all of the answers.</choicehint>
</choice>
</choicegroup>
</multiplechoiceresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>`,
hints: [{
id: 0,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 1,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
data: {
answers: [
{
id: 'A',
title: 'an incorrect answer',
correct: false,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
},
{
id: 'B',
title: 'the correct answer',
correct: true,
},
{
id: 'C',
title: 'an incorrect answer',
correct: false,
feedback: 'You can specify optional feedback for none, a subset, or all of the answers.',
},
],
},
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><bold>Add the question text, or prompt, here. This text is required.</bold><em>You can add an optional tip or note related to the prompt like this.</em>',
buildOLX: `<problem>
<multiplechoiceresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for multiple choice with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<bold>Add the question text, or prompt, here. This text is required.</bold>
<em>You can add an optional tip or note related to the prompt like this.</em>
<choicegroup>
<choice correct="false">
an incorrect answer <choicehint>You can specify optional feedback like this, which appears after this answer is submitted.</choicehint>
</choice>
<choice correct="true">the correct answer</choice>
<choice correct="false">
an incorrect answer <choicehint>You can specify optional feedback for none, a subset, or all of the answers.</choicehint>
</choice>
</choicegroup>
</multiplechoiceresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>
`,
};
export const numericInputWithFeedbackAndHintsOLX = {
rawOLX: `<problem>
<numericalresponse answer="100">
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<responseparam type="tolerance" default="5"/>
<formulaequationinput/>
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
<additional_answer answer="200"><correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint></additional_answer>
</numericalresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>`,
hints: [{
id: 0,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 1,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
data: {
answers: [
{
id: 'A',
title: '100',
correct: true,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
tolerance: '5',
},
{
id: 'B',
title: '200',
correct: true,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
},
],
},
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><bold>Add the question text, or prompt, here. This text is required.</bold><em>You can add an optional tip or note related to the prompt like this.</em>',
buildOLX: `<problem>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<bold>Add the question text, or prompt, here. This text is required.</bold>
<em>You can add an optional tip or note related to the prompt like this.</em>
<numericalresponse answer="100">
<responseparam type="tolerance" default="5"></responseparam>
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
<additional_answer answer="200">
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
</additional_answer>
<formulaequationinput></formulaequationinput>
</numericalresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>
`,
};
export const textInputWithFeedbackAndHintsOLX = {
rawOLX: `<problem>
<stringresponse answer="the correct answer" type="ci">
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
<additional_answer answer="optional acceptable variant of the correct answer"/>
<stringequalhint answer="optional incorrect answer such as a frequent misconception">You can specify optional feedback for none, a subset, or all of the answers.</stringequalhint>
<textline size="20"/>
</stringresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>`,
hints: [{
id: 0,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 1,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
data: {
answers: [
{
id: 'A',
title: 'the correct answer',
correct: true,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
},
{
id: 'B',
title: 'optional acceptable variant of the correct answer',
correct: true,
feedback: '',
},
{
id: 'C',
title: 'optional incorrect answer such as a frequent misconception',
correct: false,
feedback: 'You can specify optional feedback for none, a subset, or all of the answers.',
},
],
additionalStringAttributes: {
type: 'ci',
textline: {
size: '20',
},
},
},
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><bold>Add the question text, or prompt, here. This text is required.</bold><em>You can add an optional tip or note related to the prompt like this.</em>',
buildOLX: `<problem>
<stringresponse answer="the correct answer" type="ci">
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<bold>Add the question text, or prompt, here. This text is required.</bold>
<em>You can add an optional tip or note related to the prompt like this.</em>
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
<additional_answer answer="optional acceptable variant of the correct answer"></additional_answer>
<stringequalhint answer="optional incorrect answer such as a frequent misconception">You can specify optional feedback for none, a subset, or all of the answers.</stringequalhint>
<textline size="20"></textline>
</stringresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>
`,
};
export const textInputWithFeedbackAndHintsOLXWithMultipleAnswers = {
rawOLX: `<problem>
<stringresponse answer="the correct answer" type="ci">
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
<additional_answer answer="300"><correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint> </additional_answer>
<additional_answer answer="400"><correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint> </additional_answer>
<stringequalhint answer="optional incorrect answer such as a frequent misconception">You can specify optional feedback for none, a subset, or all of the answers.</stringequalhint>
<textline size="20"/>
</stringresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>`,
hints: [{
id: 0,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 1,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
data: {
answers: [
{
id: 'A',
title: 'the correct answer',
correct: true,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
},
{
id: 'B',
title: '300',
correct: true,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
},
{
correct: true,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
id: 'C',
title: '400',
},
{
id: 'D',
title: 'optional incorrect answer such as a frequent misconception',
correct: false,
feedback: 'You can specify optional feedback for none, a subset, or all of the answers.',
},
],
additionalStringAttributes: {
type: 'ci',
textline: {
size: '20',
},
},
},
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><bold>Add the question text, or prompt, here. This text is required.</bold><em>You can add an optional tip or note related to the prompt like this.</em>',
buildOLX: `<problem>
<stringresponse answer="the correct answer" type="ci">
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<bold>Add the question text, or prompt, here. This text is required.</bold>
<em>You can add an optional tip or note related to the prompt like this.</em>
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
<additional_answer answer="300">
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
</additional_answer>
<additional_answer answer="400">
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
</additional_answer>
<stringequalhint answer="optional incorrect answer such as a frequent misconception">You can specify optional feedback for none, a subset, or all of the answers.</stringequalhint>
<textline size="20"></textline>
</stringresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>
`,
};
export const numericInputWithFeedbackAndHintsOLXException = {
rawOLX: `<problem>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<numericalresponse answer="300">
<formulaequationinput />
</numericalresponse>
<numericalresponse answer="100">
<responseparam type="tolerance" default="5" />
<formulaequationinput />
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
</numericalresponse>
<numericalresponse answer="200">
<responseparam type="tolerance" default="4" />
<additional_answer answer="500"><correcthint>This is one feedback!</correcthint></additional_answer>
<formulaequationinput />
</numericalresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>`,
data: {
answers: [
{
id: 'A',
title: '300',
correct: true,
feedback: '',
},
{
id: 'B',
title: '100',
correct: true,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
tolerance: '5',
},
{
id: 'C',
title: '200',
correct: true,
feedback: '',
tolerance: '4',
},
{
id: 'D',
title: '500',
correct: true,
feedback: 'This is one feedback!',
},
],
},
question: '<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p><bold>Add the question text, or prompt, here. This text is required.</bold><em>You can add an optional tip or note related to the prompt like this.</em>',
buildOLX: `<problem>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>
<bold>Add the question text, or prompt, here. This text is required.</bold>
<em>You can add an optional tip or note related to the prompt like this.</em>
<numericalresponse answer="300">
<additional_answer answer="100">
<correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>
</additional_answer>
<additional_answer answer="200"></additional_answer>
<additional_answer answer="500">
<correcthint>This is one feedback!</correcthint>
</additional_answer>
<formulaequationinput></formulaequationinput>
</numericalresponse>
<demandhint>
<hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>
<hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>
</demandhint>
</problem>
`,
};

View File

@@ -0,0 +1,389 @@
export const checklistWithFeebackHints = {
state: {
rawOLX: '<problem>\n <choiceresponse>\n <p>You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>\n <label>Add the question text, or prompt, here. This text is required.</label>\n <description>You can add an optional tip or note related to the prompt like this.</description>\n <checkboxgroup>\n <choice correct="true">a correct answer\n <choicehint selected="true">You can specify optional feedback that appears after the learner selects and submits this answer.</choicehint>\n <choicehint selected="false">You can specify optional feedback that appears after the learner clears and submits this answer.</choicehint>\n </choice>\n <choice correct="false">an incorrect answer\n </choice>\n <choice correct="false">an incorrect answer\n <choicehint selected="true">You can specify optional feedback for none, all, or a subset of the answers.</choicehint>\n <choicehint selected="false">You can specify optional feedback for selected answers, cleared answers, or both.</choicehint>\n </choice>\n <choice correct="true">a correct answer\n </choice>\n <compoundhint value="A B D">You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted.</compoundhint>\n <compoundhint value="A B C D">You can specify optional feedback for one, several, or all answer combinations.</compoundhint>\n </checkboxgroup>\n </choiceresponse>\n\n <demandhint>\n <hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>\n <hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>\n </demandhint>\n</problem>\n',
problemType: 'MULTISELECT',
question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.\n\n<p>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this.</p>\n\n',
answers: [
{
id: 'A',
title: 'a correct answer',
correct: true,
selectedFeedback: ' You can specify optional feedback that appears after the learner selects and submits this answer.',
unselectedFeedback: 'You can specify optional feedback that appears after the learner clears and submits this answer.',
},
{
id: 'B',
title: 'an incorrect answer',
correct: false,
selectedFeedback: '',
unselectedFeedback: '',
},
{
id: 'C',
title: 'an incorrect answer',
correct: false,
selectedFeedback: ' You can specify optional feedback for none, all, or a subset of the answers.',
unselectedFeedback: 'You can specify optional feedback for selected answers, cleared answers, or both.',
},
{
id: 'D',
title: 'a correct answer',
correct: true,
selectedFeedback: '',
unselectedFeedback: '',
},
],
groupFeedbackList: [
{
id: 3,
answers: [
'A',
'B',
'D',
],
feedback: 'You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted.',
},
{
id: 4,
answers: [
'A',
'B',
'C',
'D',
],
feedback: 'You can specify optional feedback for one, several, or all answer combinations.',
},
],
settings: {
hints: [
{
id: 14,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 15,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
scoring: {
weight: 2.5,
attempts: {
unlimited: false,
number: 5,
},
},
timeBetween: 3,
matLabApiKey: 'sample_matlab_api_key',
showAnswer: {
on: 'after_attempts',
afterAttempts: 2,
},
showResetButton: true,
},
},
metadata: {
markdown: `You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.
>>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this.<<
[x] a correct answer{{selected: You can specify optional feedback that appears after the learner selects and submits this answer.},{unselected: You can specify optional feedback that appears after the learner clears and submits this answer.}}
[ ] an incorrect answer
[ ] an incorrect answer{{selected: You can specify optional feedback for none, all, or a subset of the answers.},{unselected: You can specify optional feedback for selected answers, cleared answers, or both.}}
[x] a correct answer
||You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.||
||If you add more than one hint, a different hint appears each time learners select the hint button.||
{{ (( A B D )) You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted. }}
{{ (( A B C D )) You can specify optional feedback for one, several, or all answer combinations. }}
`,
matlab_api_key: 'sample_matlab_api_key',
max_attempts: 5,
show_reset_button: true,
showanswer: 'after_attempts',
attempts_before_showanswer_button: 2,
submission_wait_seconds: 3,
weight: 2.5,
},
};
export const dropdownWithFeedbackHints = {
state: {
rawOLX: '<problem>\n <optionresponse>\n <p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>\n <label>Add the question text, or prompt, here. This text is required.</label>\n <description>You can add an optional tip or note related to the prompt like this. </description>\n <optioninput>\n <option correct="False">an incorrect answer <optionhint>You can specify optional feedback like this, which appears after this answer is submitted.</optionhint></option>\n <option correct="True">the correct answer</option>\n <option correct="False">an incorrect answer <optionhint>You can specify optional feedback for none, a subset, or all of the answers.</optionhint></option>\n </optioninput>\n </optionresponse>\n <demandhint>\n <hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>\n <hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>\n </demandhint>\n</problem>\n',
problemType: 'DROPDOWN',
question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.\n<p>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this. </p>\n',
answers: [
{
id: 'A',
title: 'an incorrect answer',
correct: false,
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
},
{
id: 'B',
title: 'the correct answer',
correct: true,
feedback: '',
},
{
id: 'C',
title: 'an incorrect answer',
correct: false,
feedback: 'You can specify optional feedback for none, a subset, or all of the answers.',
},
],
groupFeedbackList: [],
settings: {
hints: [
{
id: 8,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 9,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
scoring: {
weight: 2.5,
attempts: {
unlimited: false,
number: 5,
},
},
timeBetween: 3,
matLabApiKey: '',
showAnswer: {
on: 'after_attempts',
afterAttempts: 2,
},
showResetButton: true,
},
},
metadata: {
markdown: `You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown with hints and feedback problems. Edit this component to replace this template with your own assessment.
>>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this. <<
[[
an incorrect answer {{You can specify optional feedback like this, which appears after this answer is submitted.}}
(the correct answer)
an incorrect answer {{You can specify optional feedback for none, a subset, or all of the answers.}}
]]
||You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.||
||If you add more than one hint, a different hint appears each time learners select the hint button.||
`,
max_attempts: 5,
show_reset_button: true,
showanswer: 'after_attempts',
attempts_before_showanswer_button: 2,
submission_wait_seconds: 3,
weight: 2.5,
},
};
export const numericWithHints = {
state: {
rawOLX: '<problem>\n <numericalresponse answer="100">\n <p>You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>\n <label>Add the question text, or prompt, here. This text is required.</label>\n <description>You can add an optional tip or note related to the prompt like this.</description>\n <responseparam type="tolerance" default="5"/>\n <formulaequationinput/>\n <correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>\n </numericalresponse>\n <demandhint>\n <hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>\n <hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>\n </demandhint>\n</problem>\n',
problemType: 'TEXTINPUT',
question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.\n\n<p>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this. </p>\n\n',
answers: [
{
id: 'A',
title: '100 +-5',
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
correct: true,
},
{
id: 'B',
title: '90 +-5',
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
correct: true,
},
{
id: 'C',
title: '60 +-5',
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
correct: false,
},
],
groupFeedbackList: [],
settings: {
hints: [
{
id: 6,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 7,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
scoring: {
weight: 2.5,
attempts: {
unlimited: true,
number: 0,
},
},
timeBetween: 0,
matLabApiKey: '',
showAnswer: {
on: 'after_attempts',
afterAttempts: 1,
},
showResetButton: false,
},
},
metadata: {
markdown: `You can use this template as a guide to the simple editor markdown and OLX markup to use for numerical input with hints and feedback problems. Edit this component to replace this template with your own assessment.
>>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this. <<
=100 +-5 {{You can specify optional feedback like this, which appears after this answer is submitted.}}
or=90 +-5 {{You can specify optional feedback like this, which appears after this answer is submitted.}}
not=60 +-5 {{You can specify optional feedback like this, which appears after this answer is submitted.}}
||You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.||
||If you add more than one hint, a different hint appears each time learners select the hint button.||
`,
weight: 2.5,
max_attempts: 0,
rerandomize: 'invalid_input',
showanswer: 'invalid_input',
attempts_before_showanswer_button: 2,
},
};
export const textInputWithHints = {
state: {
rawOLX: '<problem>\n <stringresponse answer="the correct answer" type="ci">\n <p>You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>\n <label>Add the question text, or prompt, here. This text is required.</label>\n <description>You can add an optional tip or note related to the prompt like this.</description>\n <correcthint>You can specify optional feedback like this, which appears after this answer is submitted.</correcthint>\n <additional_answer answer="optional acceptable variant of the correct answer"/>\n <stringequalhint answer="optional incorrect answer such as a frequent misconception">You can specify optional feedback for none, a subset, or all of the answers.</stringequalhint>\n <textline size="20"/>\n </stringresponse>\n <demandhint>\n <hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>\n <hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>\n </demandhint>\n</problem>\n',
problemType: 'TEXTINPUT',
question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.\n\n<p>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this. </p>\n\n',
answers: [
{
id: 'A',
title: 'the correct answer',
feedback: 'You can specify optional feedback like this, which appears after this answer is submitted.',
correct: true,
},
{
id: 'B',
title: 'optional acceptable variant of the correct answer',
feedback: '',
correct: true,
},
{
id: 'C',
title: 'optional incorrect answer such as a frequent misconception',
feedback: 'You can specify optional feedback for none, a subset, or all of the answers.',
correct: false,
},
],
groupFeedbackList: [],
settings: {
hints: [
{
id: 9,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 10,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
scoring: {
weight: 2.5,
attempts: {
unlimited: true,
number: 0,
},
},
timeBetween: 0,
matLabApiKey: '',
showAnswer: {
on: '',
afterAttempts: 1,
},
showResetButton: false,
},
},
metadata: {
markdown: `You can use this template as a guide to the simple editor markdown and OLX markup to use for text input with hints and feedback problems. Edit this component to replace this template with your own assessment.
>>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this. <<
=the correct answer {{You can specify optional feedback like this, which appears after this answer is submitted.}}
or=optional acceptable variant of the correct answer
not=optional incorrect answer such as a frequent misconception {{You can specify optional feedback for none, a subset, or all of the answers.}}
||You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.||
||If you add more than one hint, a different hint appears each time learners select the hint button.||
`,
weight: 2.5,
},
};
export const sigleSelectWithHints = {
state: {
rawOLX: '<problem>\n<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.</p>\n\n<label>Add the question text, or prompt, here. This text is required.</label>\n<description>You can add an optional tip or note related to the prompt like this.</description>\n<multiplechoiceresponse>\n <choicegroup type="MultipleChoice">\n <choice correct="true">a correct answer <choicehint>selected: You can specify optional feedback that appears after the learner selects and submits this answer. }, { unselected: You can specify optional feedback that appears after the learner clears and submits this answer.</choicehint></choice>\n <choice correct="false">an incorrect answer</choice>\n <choice correct="false">an incorrect answer <choicehint>selected: You can specify optional feedback for none, all, or a subset of the answers. }, { unselected: You can specify optional feedback for selected answers, cleared answers, or both.</choicehint></choice>\n <choice correct="false">an incorrect answer again</choice>\n </choicegroup>\n</multiplechoiceresponse>\n<choiceresponse>\n <checkboxgroup>\n <compoundhint value="A B D">You can specify optional feedback for a combination of answers which appears after the specified set of answers is submitted.</compoundhint>\n <compoundhint value="A B C D">You can specify optional feedback for one, several, or all answer combinations.</compoundhint>\n </checkboxgroup>\n</choiceresponse>\n\n\n<demandhint>\n <hint>You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.</hint>\n <hint>If you add more than one hint, a different hint appears each time learners select the hint button.</hint>\n</demandhint>\n</problem>',
problemType: 'SINGLESELECT',
question: 'You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.\n\n<p>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this.</p>\n\n',
answers: [
{
id: 'A',
title: 'a correct answer',
correct: true,
feedback: 'Some new feedback',
},
{
id: 'B',
title: 'an incorrect answer',
correct: false,
feedback: '',
},
{
id: 'C',
title: 'an incorrect answer',
correct: false,
feedback: 'Wrong feedback',
},
{
id: 'D',
title: 'an incorrect answer again',
correct: false,
feedback: '',
},
],
groupFeedbackList: [],
settings: {
hints: [
{
id: 13,
value: 'You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.',
},
{
id: 14,
value: 'If you add more than one hint, a different hint appears each time learners select the hint button.',
},
],
scoring: {
weight: 0,
attempts: {
unlimited: true,
number: 0,
},
},
timeBetween: 0,
matLabApiKey: '',
showAnswer: {
on: '',
afterAttempts: 1,
},
showResetButton: false,
},
},
metadata: {
markdown: `You can use this template as a guide to the simple editor markdown and OLX markup to use for checkboxes with hints and feedback problems. Edit this component to replace this template with your own assessment.
>>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this.<<
(x) a correct answer {{Some new feedback}}
( ) an incorrect answer
( ) an incorrect answer {{Wrong feedback}}
( ) an incorrect answer again
||You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.||
||If you add more than one hint, a different hint appears each time learners select the hint button.||
`,
},
};

View File

@@ -0,0 +1,41 @@
import {
useRef, useCallback, useState, useEffect,
} from 'react';
import { ProblemTypeKeys } from '../../data/constants/problem';
import { StrictDict } from '../../utils';
import * as module from './hooks';
export const state = StrictDict({
refReady: (val) => useState(val),
});
export const problemEditorConfig = ({
setEditorRef,
editorRef,
question,
updateQuestion,
}) => ({
onInit: (evt, editor) => {
setEditorRef(editor);
},
initialValue: question || '',
onFocusOut: () => {
const content = editorRef.current.getContent();
updateQuestion(content);
},
});
export const prepareEditorRef = () => {
const editorRef = useRef(null);
const setEditorRef = useCallback((ref) => {
editorRef.current = ref;
}, []);
const [refReady, setRefReady] = module.state.refReady(false);
useEffect(() => setRefReady(true), [setRefReady]);
return { editorRef, refReady, setEditorRef };
};
export const initializeAnswerContainer = (problemType) => {
const hasSingleAnswer = problemType === ProblemTypeKeys.DROPDOWN || problemType === ProblemTypeKeys.SINGLESELECT;
return { hasSingleAnswer };
};

View File

@@ -0,0 +1,62 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Spinner } from '@edx/paragon';
import { injectIntl } from '@edx/frontend-platform/i18n';
import SelectTypeModal from './components/SelectTypeModal';
import EditProblemView from './components/EditProblemView';
import { selectors, thunkActions } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
export const ProblemEditor = ({
onClose,
// Redux
problemType,
blockFinished,
studioViewFinished,
blockValue,
initializeProblemEditor,
}) => {
React.useEffect(() => initializeProblemEditor(blockValue), [blockValue]);
// TODO: INTL MSG, Add LOAD FAILED ERROR using BLOCKFAILED
if (!blockFinished || !studioViewFinished) {
return (
<div className="text-center p-6">
<Spinner
animation="border"
className="m-3"
screenreadertext="Loading Problem Editor"
/>
</div>
);
}
// once data is loaded, init store
if (problemType === null) {
return (<SelectTypeModal onClose={onClose} />);
}
return (<EditProblemView onClose={onClose} />);
};
ProblemEditor.propTypes = {
onClose: PropTypes.func.isRequired,
// redux
blockFinished: PropTypes.bool.isRequired,
studioViewFinished: PropTypes.bool.isRequired,
problemType: PropTypes.string.isRequired,
initializeProblemEditor: PropTypes.func.isRequired,
blockValue: PropTypes.objectOf(PropTypes.object).isRequired,
};
export const mapStateToProps = (state) => ({
blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
studioViewFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchStudioView }),
problemType: selectors.problem.problemType(state),
blockValue: selectors.app.blockValue(state),
});
export const mapDispatchToProps = {
initializeProblemEditor: thunkActions.problem.initializeProblem,
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ProblemEditor));

View File

@@ -0,0 +1,108 @@
import { StrictDict } from '../../utils';
export const ProblemTypeKeys = StrictDict({
TEXTINPUT: 'stringresponse',
NUMERIC: 'numericalresponse',
DROPDOWN: 'optionresponse',
MULTISELECT: 'choiceresponse',
SINGLESELECT: 'multiplechoiceresponse',
});
export const ProblemTypes = StrictDict({
[ProblemTypeKeys.SINGLESELECT]: {
title: 'Single Select Problem',
preview: ('<div />'),
description: 'Specify one correct answer from a list of possible options',
helpLink: 'something.com',
},
[ProblemTypeKeys.MULTISELECT]: {
title: 'Multi Select Problem',
preview: ('<div />'),
description: 'Specify one or more correct answers from a list of possible options.',
helpLink: 'something.com',
},
[ProblemTypeKeys.DROPDOWN]: {
title: 'Dropdown Problem',
preview: ('<div />'),
description: 'Specify one correct answer from a list of possible options, selected in a dropdown menu.',
helpLink: 'something.com',
},
[ProblemTypeKeys.NUMERIC]: {
title: 'Numeric Response Problem',
preview: ('<div />'),
description: 'Specify one or more correct numeric answers, submitted in a response field.',
helpLink: 'something.com',
},
[ProblemTypeKeys.TEXTINPUT]: {
title: 'Text Input Problem',
preview: ('<div />'),
description: 'Specify one or more correct text answers, including numbers and special characters, submitted in a response field.',
helpLink: 'something.com',
},
});
export const ShowAnswerTypesKeys = StrictDict({
ALWAYS: 'always',
ANSWERED: 'answered',
ATTEMPTED: 'attempted',
CLOSED: 'closed',
FINISHED: 'finished',
CORRECT_OR_PAST_DUE: 'correct_or_past_due',
PAST_DUE: 'past_due',
NEVER: 'never',
AFTER_SOME_NUMBER_OF_ATTEMPTS: 'after_attempts',
AFTER_ALL_ATTEMPTS: 'after_all_attempts',
AFTER_ALL_ATTEMPTS_OR_CORRECT: 'after_all_attempts_or_correct',
ATTEMPTED_NO_PAST_DUE: 'attempted_no_past_due',
});
export const ShowAnswerTypes = StrictDict({
[ShowAnswerTypesKeys.ALWAYS]: {
id: 'authoring.problemeditor.settings.showanswertype.always',
defaultMessage: 'Always',
},
[ShowAnswerTypesKeys.ANSWERED]: {
id: 'authoring.problemeditor.settings.showanswertype.answered',
defaultMessage: 'Answered',
},
[ShowAnswerTypesKeys.ATTEMPTED]: {
id: 'authoring.problemeditor.settings.showanswertype.attempted',
defaultMessage: 'Attempted or Past Due',
},
[ShowAnswerTypesKeys.CLOSED]: {
id: 'authoring.problemeditor.settings.showanswertype.closed',
defaultMessage: 'Closed',
},
[ShowAnswerTypesKeys.FINISHED]: {
id: 'authoring.problemeditor.settings.showanswertype.finished',
defaultMessage: 'Finished',
},
[ShowAnswerTypesKeys.CORRECT_OR_PAST_DUE]: {
id: 'authoring.problemeditor.settings.showanswertype.correct_or_past_due',
defaultMessage: 'Correct or Past Due',
},
[ShowAnswerTypesKeys.PAST_DUE]: {
id: 'authoring.problemeditor.settings.showanswertype.past_due',
defaultMessage: 'Past Due',
},
[ShowAnswerTypesKeys.NEVER]: {
id: 'authoring.problemeditor.settings.showanswertype.never',
defaultMessage: 'Never',
},
[ShowAnswerTypesKeys.AFTER_SOME_NUMBER_OF_ATTEMPTS]: {
id: 'authoring.problemeditor.settings.showanswertype.after_attempts',
defaultMessage: 'After Some Number of Attempts',
},
[ShowAnswerTypesKeys.AFTER_ALL_ATTEMPTS]: {
id: 'authoring.problemeditor.settings.showanswertype.after_all_attempts',
defaultMessage: 'After All Attempts',
},
[ShowAnswerTypesKeys.AFTER_ALL_ATTEMPTS_OR_CORRECT]: {
id: 'authoring.problemeditor.settings.showanswertype.after_all_attempts_or_correct',
defaultMessage: 'After All Attempts or Correct',
},
[ShowAnswerTypesKeys.ATTEMPTED_NO_PAST_DUE]: {
id: 'authoring.problemeditor.settings.showanswertype.attempted_no_past_due',
defaultMessage: 'Attempted',
},
});

View File

@@ -11,8 +11,8 @@ export const RequestKeys = StrictDict({
fetchAssets: 'fetchAssets',
fetchBlock: 'fetchBlock',
fetchImages: 'fetchImages',
fetchStudioView: 'fetchStudioView',
fetchUnit: 'fetchUnit',
fetchStudioView: 'fetchStudioView',
saveBlock: 'saveBlock',
uploadAsset: 'uploadAsset',
allowThumbnailUpload: 'allowThumbnailUpload',

View File

@@ -38,6 +38,7 @@ const app = createSlice({
blockValue: payload,
blockTitle: payload.data.display_name,
}),
setStudioView: (state, { payload }) => ({ ...state, studioView: payload }),
setBlockContent: (state, { payload }) => ({ ...state, blockContent: payload }),
setBlockTitle: (state, { payload }) => ({ ...state, blockTitle: payload }),

View File

@@ -1,5 +1,4 @@
import { createSelector } from 'reselect';
import { blockTypes } from '../../constants/app';
import * as urls from '../../services/cms/urls';
import * as module from './selectors';

View File

@@ -5,6 +5,7 @@ import { StrictDict } from '../../utils';
import * as app from './app';
import * as requests from './requests';
import * as video from './video';
import * as problem from './problem';
/* eslint-disable import/no-cycle */
export { default as thunkActions } from './thunkActions';
@@ -13,6 +14,7 @@ const modules = {
app,
requests,
video,
problem,
};
const moduleProps = (propName) => Object.keys(modules).reduce(

View File

@@ -0,0 +1,2 @@
export { actions, reducer } from './reducers';
export { default as selectors } from './selectors';

View File

@@ -0,0 +1,142 @@
import _ from 'lodash-es';
import { createSlice } from '@reduxjs/toolkit';
import { indexToLetterMap } from '../../../containers/ProblemEditor/data/OLXParser';
import { StrictDict } from '../../../utils';
import { ProblemTypeKeys, ShowAnswerTypesKeys } from '../../constants/problem';
const nextAlphaId = (lastId) => String.fromCharCode(lastId.charCodeAt(0) + 1);
const initialState = {
rawOLX: '',
problemType: ProblemTypeKeys.SINGLESELECT,
question: '',
answers: [],
groupFeedbackList: [],
additionalAttributes: {},
settings: {
scoring: {
weight: 0,
attempts: {
unlimited: true,
number: 0,
},
},
hints: [],
timeBetween: 0,
matLabApiKey: '',
showAnswer: {
on: ShowAnswerTypesKeys.FINISHED,
afterAttempts: 0,
},
showResetButton: false,
},
};
// eslint-disable-next-line no-unused-vars
const problem = createSlice({
name: 'problem',
initialState,
reducers: {
updateField: (state, { payload }) => ({
...state,
...payload,
}),
updateQuestion: (state, { payload }) => ({
...state,
question: payload,
}),
updateAnswer: (state, { payload }) => {
const { id, hasSingleAnswer, ...answer } = payload;
const answers = state.answers.map(obj => {
if (obj.id === id) {
return { ...obj, ...answer };
}
// set other answers as incorrect if problem only has one answer correct
// and changes object include correct key change
if (hasSingleAnswer && _.has(answer, 'correct') && obj.correct) {
return { ...obj, correct: false };
}
return obj;
});
return {
...state,
answers,
};
},
deleteAnswer: (state, { payload }) => {
const { id } = payload;
if (state.answers.length <= 1) {
return state;
}
const answers = state.answers.filter(obj => obj.id !== id).map((answer, index) => {
const newId = indexToLetterMap[index];
if (answer.id === newId) {
return answer;
}
return { ...answer, id: newId };
});
return {
...state,
answers,
};
},
addAnswer: (state) => {
const currAnswers = state.answers;
if (currAnswers.length >= indexToLetterMap.length) {
return state;
}
const newOption = {
id: currAnswers.length ? nextAlphaId(currAnswers[currAnswers.length - 1].id) : 'A',
title: '',
selectedFeedback: undefined,
unselectedFeedback: undefined,
feedback: undefined,
correct: false,
};
if (state.problemType === ProblemTypeKeys.MULTISELECT) {
newOption.selectedFeedback = '';
newOption.unselectedFeedback = '';
} else {
newOption.feedback = '';
}
const answers = [
...currAnswers,
newOption,
];
return {
...state,
answers,
};
},
updateSettings: (state, { payload }) => ({
...state,
settings: {
...state.settings,
...payload,
},
}),
load: (state, { payload: { settings: { scoring, showAnswer, ...settings }, ...payload } }) => ({
...state,
settings: {
...state.settings,
scoring: { ...state.settings.scoring, ...scoring },
showAnswer: { ...state.settings.showAnswer, ...showAnswer },
...settings,
},
...payload,
}),
onSelect: (state, { payload }) => ({
...state,
...payload,
}),
},
});
const actions = StrictDict(problem.actions);
const { reducer } = problem;
export {
actions,
initialState,
reducer,
};

View File

@@ -0,0 +1,16 @@
import { createSelector } from 'reselect';
import * as module from './selectors';
export const problemState = (state) => state.problem;
const mkSimpleSelector = (cb) => createSelector([module.problemState], cb);
export const simpleSelectors = {
problemType: mkSimpleSelector(problemData => problemData.problemType),
answers: mkSimpleSelector(problemData => problemData.answers),
settings: mkSimpleSelector(problemData => problemData.settings),
question: mkSimpleSelector(problemData => problemData.question),
completeState: mkSimpleSelector(problemData => problemData),
};
export default {
...simpleSelectors,
};

View File

@@ -6,6 +6,7 @@ import * as module from './app';
export const fetchBlock = () => (dispatch) => {
dispatch(requests.fetchBlock({
onSuccess: (response) => dispatch(actions.app.setBlockValue(response)),
// eslint-disable-next-line
onFailure: (e) => console.log({ fetchFailure: e }),
}));
};

View File

@@ -2,8 +2,10 @@ import { StrictDict } from '../../../utils';
import app from './app';
import video from './video';
import problem from './problem';
export default StrictDict({
app,
video,
problem,
});

View File

@@ -0,0 +1,22 @@
import _ from 'lodash-es';
import { actions } from '..';
import { OLXParser } from '../../../containers/ProblemEditor/data/OLXParser';
import { parseSettings } from '../../../containers/ProblemEditor/data/SettingsParser';
export const initializeProblem = (blockValue) => (dispatch) => {
const rawOLX = _.get(blockValue, 'data.data', {});
try {
const olxParser = new OLXParser(rawOLX);
const { ...data } = olxParser.getParsedOLXData();
let { settings } = olxParser.getParsedOLXData();
settings = { ...settings, ...parseSettings(_.get(blockValue, 'data.metadata', {})) };
if (!_.isEmpty(rawOLX) && !_.isEmpty(data)) {
dispatch(actions.problem.load({ ...data, rawOLX, settings }));
}
} catch (error) {
// eslint-disable-next-line
console.error(error);
}
};
export default { initializeProblem };

View File

@@ -57,6 +57,7 @@ export const fetchBlock = ({ ...rest }) => (dispatch, getState) => {
};
/**
* Tracked fetchStudioView api method.
* Tracked to the `fetchBlock` request key.
* @param {[func]} onSuccess - onSuccess method ((response) => { ... })

View File

@@ -101,8 +101,9 @@ export const apiMethods = {
learningContextId,
title,
}) => {
let response = {};
if (blockType === 'html') {
return {
response = {
category: blockType,
courseKey: learningContextId,
data: content,
@@ -110,8 +111,16 @@ export const apiMethods = {
id: blockId,
metadata: { display_name: title },
};
}
if (blockType === 'video') {
} else if (blockType === 'problem') {
response = {
data: content.olx,
category: blockType,
couseKey: learningContextId,
has_changes: true,
id: blockId,
metadata: { display_name: title, ...content.settings },
};
} else if (blockType === 'video') {
const {
html5Sources,
edxVideoId,
@@ -121,7 +130,7 @@ export const apiMethods = {
videoSource: content.videoSource,
fallbackVideos: content.fallbackVideos,
});
return {
response = {
category: blockType,
courseKey: learningContextId,
display_name: title,
@@ -142,8 +151,10 @@ export const apiMethods = {
license: module.processLicense(content.licenseType, content.licenseDetails),
},
};
} else {
throw new TypeError(`No Block in V2 Editors named /"${blockType}/", Cannot Save Content.`);
}
throw new TypeError(`No Block in V2 Editors named /"${blockType}/", Cannot Save Content.`);
return { ...response };
},
saveBlock: ({
blockId,

View File

@@ -1,68 +1,72 @@
/* istanbul ignore file */
import * as urls from './urls';
const mockPromise = (returnValue) => new Promise(resolve => resolve(returnValue));
// TODO: update to return block data appropriate per block ID, which will equal block type
// eslint-disable-next-line
export const fetchBlockById = ({ blockId, studioEndpointUrl }) => mockPromise({
data: {
data: '<p>Test prompt content</p>',
display_name: 'My Text Prompt',
metadata: {
display_name: 'Welcome!',
download_track: true,
download_video: true,
edx_video_id: 'f36f06b5-92e5-47c7-bb26-bcf986799cb7',
html5_sources: [
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
],
show_captions: true,
sub: '',
track: '',
transcripts: {
en: { filename: 'my-transcript-url' },
export const fetchBlockById = ({ blockId, studioEndpointUrl }) => {
let data = {};
if (blockId === 'html-block-id') {
data = {
data: '<p>Test prompt content</p>',
display_name: 'My Text Prompt',
metadata: {
display_name: 'Welcome!',
download_track: true,
download_video: true,
edx_video_id: 'f36f06b5-92e5-47c7-bb26-bcf986799cb7',
html5_sources: [
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
],
show_captions: true,
sub: '',
track: '',
transcripts: {
en: { filename: 'my-transcript-url' },
},
xml_attributes: {
source: '',
},
youtube_id_1_0: 'dQw4w9WgXcQ',
},
xml_attributes: {
source: '',
};
} else if (blockId === 'problem-block-id') {
data = {
data: `<problem>
<optionresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<optioninput>
<option correct="false">an incorrect answer</option>
<option correct="true">the correct answer</option>
<option correct="false">an incorrect answer</option>
</optioninput>
</optionresponse>
</problem>`,
display_name: 'Dropdown',
metadata: {
markdown: `You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.
>>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this. <<
[[
an incorrect answer
(the correct answer)
an incorrect answer
]]`,
attempts_before_showanswer_button: 7,
matlab_api_key: 'sample_matlab_api_key',
max_attempts: 5,
show_reset_button: true,
showanswer: 'after_attempts',
submission_wait_seconds: 15,
weight: 29,
},
youtube_id_1_0: 'dQw4w9WgXcQ',
},
},
});
// TODO: update to return block data appropriate per block ID, which will equal block type
// eslint-disable-next-line
export const fetchStudioView = ({ blockId, studioEndpointUrl }) => mockPromise({
data: {
// The following is sent for 'raw' editors.
html: blockId.includes('mockRaw') ? 'data-editor="raw"' : '',
data: '<p>Test prompt content</p> <div data-metadata="license, "value": "all-rights-reserved", "type": " />',
display_name: 'My Text Prompt',
metadata: {
display_name: 'Welcome!',
download_track: true,
download_video: true,
edx_video_id: 'f36f06b5-92e5-47c7-bb26-bcf986799cb7',
html5_sources: [
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
],
show_captions: true,
sub: '',
track: '',
transcripts: {
en: { filename: 'my-transcript-url' },
},
xml_attributes: {
source: '',
},
youtube_id_1_0: 'dQw4w9WgXcQ',
},
},
});
};
}
return mockPromise({ data: { ...data } });
};
// TODO: update to return block data appropriate per block ID, which will equal block type
// eslint-disable-next-line
@@ -139,8 +143,9 @@ export const normalizeContent = ({
learningContextId,
title,
}) => {
let response = {};
if (blockType === 'html') {
return {
response = {
category: blockType,
couseKey: learningContextId,
data: content,
@@ -148,8 +153,19 @@ export const normalizeContent = ({
id: blockId,
metadata: { display_name: title },
};
} else if (blockType === 'problem') {
response = {
data: content.olx,
category: blockType,
couseKey: learningContextId,
has_changes: true,
id: blockId,
metadata: { display_name: title, ...content.settings },
};
} else {
throw new TypeError(`No Block in V2 Editors named /"${blockType}/", Cannot Save Content.`);
}
throw new TypeError(`No Block in V2 Editors named /"${blockType}/", Cannot Save Content.`);
return { ...response };
};
export const saveBlock = ({
@@ -191,3 +207,76 @@ export const uploadAsset = ({
msg: 'Upload completed',
},
});
// TODO: update to return block data appropriate per block ID, which will equal block type
// eslint-disable-next-line
export const fetchStudioView = ({ blockId, studioEndpointUrl }) => {
let data = {};
if (blockId === 'html-block-id') {
data = {
data: '<p>Test prompt content</p>',
display_name: 'My Text Prompt',
metadata: {
display_name: 'Welcome!',
download_track: true,
download_video: true,
edx_video_id: 'f36f06b5-92e5-47c7-bb26-bcf986799cb7',
html5_sources: [
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
],
show_captions: true,
sub: '',
track: '',
transcripts: {
en: { filename: 'my-transcript-url' },
},
xml_attributes: {
source: '',
},
youtube_id_1_0: 'dQw4w9WgXcQ',
},
};
} else if (blockId === 'problem-block-id') {
data = {
data: `<problem>
<optionresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<optioninput>
<option correct="False">an incorrect answer</option>
<option correct="True">the correct answer</option>
<option correct="False">an incorrect answer</option>
</optioninput>
</optionresponse>
</problem>`,
display_name: 'Dropdown',
metadata: {
markdown: `You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.
>>Add the question text, or prompt, here. This text is required.||You can add an optional tip or note related to the prompt like this. <<
[[
an incorrect answer
(the correct answer)
an incorrect answer
]]`,
attempts_before_showanswer_button: 7,
matlab_api_key: 'numerical_input_matlab_api_key',
max_attempts: 5,
rerandomize: 'per_student',
show_reset_button: true,
showanswer: 'after_attempts',
submission_wait_seconds: 15,
weight: 29,
},
};
}
return mockPromise({
data: {
// The following is sent for 'raw' editors.
html: blockId.includes('mockRaw') ? 'data-editor="raw"' : '',
...data,
},
});
};

View File

@@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
import { ProblemTypes, ShowAnswerTypes } from '../../constants/problem';
export const videoDataProps = {
videoSource: PropTypes.string,
@@ -24,6 +25,46 @@ export const videoDataProps = {
}),
};
export const answerOptionProps = PropTypes.shape({
id: PropTypes.string,
title: PropTypes.string,
correct: PropTypes.bool,
feedback: PropTypes.string,
selectedFeedback: PropTypes.string,
unselectedFeedback: PropTypes.string,
});
export const problemDataProps = {
rawOLX: PropTypes.string,
problemType: PropTypes.instanceOf(ProblemTypes),
question: PropTypes.string,
answers: PropTypes.arrayOf(
answerOptionProps,
),
settings: PropTypes.shape({
scoring: PropTypes.shape({
advanced: PropTypes.bool,
scoring: PropTypes.shape({
weight: PropTypes.number,
attempts: PropTypes.shape({
unlimited: PropTypes.bool,
number: PropTypes.number,
}),
}),
}),
hints: PropTypes.arrayOf(PropTypes.string),
timeBetween: PropTypes.number,
matLabApiKey: PropTypes.string,
showAnswer: PropTypes.shape({
on: PropTypes.instanceOf(ShowAnswerTypes),
afterAtempts: PropTypes.number,
}),
showResetButton: PropTypes.bool,
}),
};
export default {
videoDataProps,
problemDataProps,
answerOptionProps,
};

View File

@@ -1,6 +1,6 @@
import TextEditor from './containers/TextEditor';
import VideoEditor from './containers/VideoEditor';
import ProblemEditor from './containers/ProblemEditor/ProblemEditor';
import ProblemEditor from './containers/ProblemEditor';
// ADDED_EDITOR_IMPORTS GO HERE

View File

@@ -80,6 +80,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Footer: 'Card.Footer',
Body: 'Card.Body',
},
Container: 'Container',
Dropdown: {
Item: 'Dropdown.Item',
Menu: 'Dropdown.Menu',
@@ -93,6 +94,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
IconButton: 'IconButton',
IconButtonWithTooltip: 'IconButtonWithTooltip',
Image: 'Image',
MailtoLink: 'MailtoLink',
ModalDialog: {
Footer: 'ModalDialog.Footer',
Header: 'ModalDialog.Header',
@@ -111,6 +113,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Row: 'Form.Row',
},
FullscreenModal: 'FullscreenModal',
Row: 'Row',
Scrollable: 'Scrollable',
SelectableBox: {
Set: 'SelectableBox.Set',

48437
www/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,17 +11,18 @@
"license": "ISC",
"dependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "^9.1.1",
"@edx/browserslist-config": "1.0.0",
"@edx/frontend-build": "^11.0.0",
"@edx/frontend-lib-content-components": "file:..",
"@edx/frontend-platform": "^3.0.1",
"@edx/paragon": "^20.21.0",
"@edx/frontend-platform": "2.5.1",
"@edx/paragon": "20.13.0",
"core-js": "^3.21.1",
"dotenv": "^16.0.0",
"prop-types": "^15.5.10",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-intl": "^5.24.6",
"react-redux": "^7.1.1",
"regenerator-runtime": "^0.13.9"
}
}

View File

@@ -1,8 +1,13 @@
import { Provider } from 'react-redux';
// eslint-disable-next-line
import store from '@edx/frontend-lib-content-components/editors/data/store';
import Gallery from './Gallery';
export const App = () => (
<div className="editor-gallery">
<Gallery />
</div>
<Provider store={store}>
<div className="editor-gallery">
<Gallery />
</div>
</Provider>
);
export default App;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Form } from '@edx/paragon';
// eslint-disable-next-line
@@ -7,6 +7,9 @@ import { EditorPage } from '@edx/frontend-lib-content-components';
import { blockTypes } from '@edx/frontend-lib-content-components/editors/data/constants/app';
// eslint-disable-next-line
import { mockBlockIdByType } from '@edx/frontend-lib-content-components/editors/data/constants/mockData';
import { useDispatch } from 'react-redux';
// eslint-disable-next-line
import { thunkActions } from '@edx/frontend-lib-content-components/editors/data/redux';
export const EditorGallery = () => {
const [blockType, setBlockType] = React.useState('html');
@@ -23,6 +26,15 @@ export const EditorGallery = () => {
const handleBlockChange = (e) => setBlockType(e.target.value);
const handleRawChange = (e) => setMockRaw(e.target.value === 'true');
const dispatch = useDispatch();
useEffect(() => {
thunkActions.app.initialize({
blockId: blockIds[blockType],
blockType,
lmsEndpointUrl,
studioEndpointUrl,
})(dispatch);
}, [dispatch, blockType]);
return (
<div className="gallery">
<div style={{ display: 'flex' }}>

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line
import 'core-js/stable';
import 'core-js';
import 'regenerator-runtime/runtime';
import React from 'react';