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:
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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
73727
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ProblemEditor() {
|
||||
return (
|
||||
<div className="problem-editor">
|
||||
<span>Problem</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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)));
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -0,0 +1,11 @@
|
||||
.problem-answer {
|
||||
padding: 12px;
|
||||
color: #00262B;
|
||||
.problem-answer-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.problem-answer-description {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
@@ -0,0 +1,9 @@
|
||||
export const messages = {
|
||||
questionWidgetTitle: {
|
||||
id: 'authoring.questionwidget.question.questionWidgetTitle',
|
||||
defaultMessage: 'Question',
|
||||
description: 'Question Title',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
@@ -0,0 +1,11 @@
|
||||
.settingsCardTitleSection {
|
||||
padding-bottom: 0rem;
|
||||
}
|
||||
|
||||
.halfSpacedMessage {
|
||||
padding-bottom: .5rem;
|
||||
}
|
||||
|
||||
.spacedMessage {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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} />
|
||||
<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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,10 @@
|
||||
import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { StrictDict } from '../../../../utils';
|
||||
|
||||
export const state = StrictDict({
|
||||
selected: (val) => useState(val),
|
||||
});
|
||||
|
||||
export default { state };
|
||||
@@ -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;
|
||||
397
src/editors/containers/ProblemEditor/data/OLXParser.js
Normal file
397
src/editors/containers/ProblemEditor/data/OLXParser.js
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
138
src/editors/containers/ProblemEditor/data/OLXParser.test.js
Normal file
138
src/editors/containers/ProblemEditor/data/OLXParser.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
281
src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js
Normal file
281
src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js
Normal 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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
67
src/editors/containers/ProblemEditor/data/SettingsParser.js
Normal file
67
src/editors/containers/ProblemEditor/data/SettingsParser.js
Normal 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;
|
||||
};
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`,
|
||||
};
|
||||
@@ -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.||
|
||||
`,
|
||||
},
|
||||
};
|
||||
41
src/editors/containers/ProblemEditor/hooks.js
Normal file
41
src/editors/containers/ProblemEditor/hooks.js
Normal 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 };
|
||||
};
|
||||
62
src/editors/containers/ProblemEditor/index.jsx
Normal file
62
src/editors/containers/ProblemEditor/index.jsx
Normal 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));
|
||||
108
src/editors/data/constants/problem.js
Normal file
108
src/editors/data/constants/problem.js
Normal 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',
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
2
src/editors/data/redux/problem/index.js
Normal file
2
src/editors/data/redux/problem/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { actions, reducer } from './reducers';
|
||||
export { default as selectors } from './selectors';
|
||||
142
src/editors/data/redux/problem/reducers.js
Normal file
142
src/editors/data/redux/problem/reducers.js
Normal 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,
|
||||
};
|
||||
16
src/editors/data/redux/problem/selectors.js
Normal file
16
src/editors/data/redux/problem/selectors.js
Normal 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,
|
||||
};
|
||||
@@ -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 }),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
22
src/editors/data/redux/thunkActions/problem.js
Normal file
22
src/editors/data/redux/thunkActions/problem.js
Normal 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 };
|
||||
@@ -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) => { ... })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
48437
www/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// eslint-disable-next-line
|
||||
import 'core-js/stable';
|
||||
import 'core-js';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
Reference in New Issue
Block a user