feat add olx solution support (#225)

This adds support for the tag in OLX and maps it to the description in the settings options on the ShowAnswer card.
https://2u-internal.atlassian.net/browse/TNL-10397
This commit is contained in:
Jesper Hodge
2023-02-01 13:28:14 -05:00
committed by GitHub
parent 83d45a249a
commit 861b47b772
26 changed files with 493 additions and 160 deletions

3
.gitignore vendored
View File

@@ -110,3 +110,6 @@ dist
### local overrides ###
module.config.js
### Code editors ###
.vscode

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Collapsible, Card } from '@edx/paragon';
import {
bool, string, node,
} from 'prop-types';
const CardSection = ({
children, none, isCardCollapsibleOpen, summary,
}) => {
const show = isCardCollapsibleOpen || summary;
if (!show) { return null; }
return (
<Card.Section className="px-4 pb-4 pt-3">
<Collapsible.Advanced
open={!isCardCollapsibleOpen}
>
<Collapsible.Body className="collapsible-body">
<span className={`small ${none ? 'text-gray-500' : 'text-primary-500'}`}>{summary}</span>
</Collapsible.Body>
</Collapsible.Advanced>
<Collapsible.Advanced
open={isCardCollapsibleOpen}
>
<Collapsible.Body className="collapsible-body">
{children}
</Collapsible.Body>
</Collapsible.Advanced>
</Card.Section>
);
};
CardSection.propTypes = {
none: bool,
children: node.isRequired,
summary: string,
isCardCollapsibleOpen: bool.isRequired,
};
CardSection.defaultProps = {
none: false,
summary: null,
};
export default CardSection;

View File

@@ -0,0 +1,12 @@
import { shallow } from 'enzyme';
import CardSection from './CardSection';
describe('CardSection', () => {
test('open', () => {
expect(shallow(<CardSection summary="summary" isCardCollapsibleOpen><h1>Section Text</h1></CardSection>)).toMatchSnapshot();
});
test('closed', () => {
expect(shallow(<CardSection isCardCollapsibleOpen={false}><h1>Section Text</h1></CardSection>)).toMatchSnapshot();
});
});

View File

@@ -1,23 +1,22 @@
import React from 'react';
import { Collapsible, Icon, Card } from '@edx/paragon';
import { KeyboardArrowUp, KeyboardArrowDown } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import {
arrayOf, shape, string, node,
} from 'prop-types';
import { showFullCard } from './hooks';
import CardSection from './CardSection';
export const SettingsOption = ({
title,
summary,
none,
children,
className,
title, className, extraSections, children, summary, ...passThroughProps
}) => {
const { isCardCollapsed, toggleCardCollapse } = showFullCard();
const { isCardCollapsibleOpen, toggleCardCollapse } = showFullCard();
return (
<Card className={`${className} settingsOption border border-light-700 shadow-none`}>
<Card.Section className="settingsCardTitleSection">
<Card.Section className="settingsCardTitleSection" key={`settingsOption-${title}-header`}>
<Collapsible.Advanced
open={isCardCollapsed}
open={isCardCollapsibleOpen}
onToggle={toggleCardCollapse}
>
<Collapsible.Trigger className="collapsible-trigger d-flex">
@@ -31,36 +30,33 @@ export const SettingsOption = ({
</Collapsible.Trigger>
</Collapsible.Advanced>
</Card.Section>
<Card.Section className="px-4 pb-4 pt-3">
<Collapsible.Advanced
open={!isCardCollapsed}
>
<Collapsible.Body className="collapsible-body">
<span className={`small ${none ? 'text-gray-500' : 'text-primary-500'}`}>{summary}</span>
</Collapsible.Body>
</Collapsible.Advanced>
<Collapsible.Advanced
open={isCardCollapsed}
>
<Collapsible.Body className="collapsible-body">
{children}
</Collapsible.Body>
</Collapsible.Advanced>
</Card.Section>
<CardSection {...passThroughProps} isCardCollapsibleOpen={isCardCollapsibleOpen} summary={summary} key={`settingsOption-${title}-children`}>
{children}
</CardSection>
{extraSections.map((section, index) => (
<>
{isCardCollapsibleOpen && <hr />}
{/* eslint-disable-next-line react/no-array-index-key */}
<CardSection {...passThroughProps} isCardCollapsibleOpen={isCardCollapsibleOpen} key={`settingsOption-${title}-${index}`}>
{section.children}
</CardSection>
</>
))}
</Card>
);
};
SettingsOption.propTypes = {
title: PropTypes.string.isRequired,
summary: PropTypes.string.isRequired,
none: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.node.isRequired,
title: string.isRequired,
children: node.isRequired,
className: string,
summary: string.isRequired,
extraSections: arrayOf(shape({
children: node,
})),
};
SettingsOption.defaultProps = {
none: false,
className: '',
extraSections: [],
};
export default SettingsOption;

View File

@@ -3,10 +3,21 @@ import { shallow } from 'enzyme';
import SettingsOption from './SettingsOption';
describe('SettingsOption', () => {
describe('render', () => {
const testContent = (<h1>My test content</h1>);
describe('default with children', () => {
const children = (<h1>My test content</h1>);
test('snapshot: renders correct', () => {
expect(shallow(<SettingsOption title="Settings Option Title" summary="Settings Option Summary">{testContent}</SettingsOption>)).toMatchSnapshot();
expect(shallow(<SettingsOption title="Settings Option Title" summary="Settings Option Summary">{children}</SettingsOption>)).toMatchSnapshot();
});
});
describe('with additional sections', () => {
const children = (<h1>First Section</h1>);
const sections = [<h1>Second Section</h1>, <h1>Third Section</h1>];
test('snapshot: renders correct', () => {
expect(shallow(
<SettingsOption title="Settings Option Title" summary="Settings Option Summary" extraSections={sections}>
{children}
</SettingsOption>,
)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CardSection closed 1`] = `""`;
exports[`CardSection open 1`] = `
<Card.Section
className="px-4 pb-4 pt-3"
>
<Advanced
open={false}
>
<Body
className="collapsible-body"
>
<span
className="small text-primary-500"
>
summary
</span>
</Body>
</Advanced>
<Advanced
open={true}
>
<Body
className="collapsible-body"
>
<h1>
Section Text
</h1>
</Body>
</Advanced>
</Card.Section>
`;

View File

@@ -1,11 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SettingsOption render snapshot: renders correct 1`] = `
exports[`SettingsOption default with children snapshot: renders correct 1`] = `
<Card
className=" settingsOption border border-light-700 shadow-none"
>
<Card.Section
className="settingsCardTitleSection"
key="settingsOption-Settings Option Title-header"
>
<Advanced
onToggle={[Function]}
@@ -32,33 +33,73 @@ exports[`SettingsOption render snapshot: renders correct 1`] = `
</Trigger>
</Advanced>
</Card.Section>
<Card.Section
className="px-4 pb-4 pt-3"
<CardSection
isCardCollapsibleOpen={false}
key="settingsOption-Settings Option Title-children"
none={false}
summary="Settings Option Summary"
>
<Advanced
open={true}
>
<Body
className="collapsible-body"
>
<span
className="small text-primary-500"
>
Settings Option Summary
</span>
</Body>
</Advanced>
<Advanced
open={false}
>
<Body
className="collapsible-body"
>
<h1>
My test content
</h1>
</Body>
</Advanced>
</Card.Section>
<h1>
My test content
</h1>
</CardSection>
</Card>
`;
exports[`SettingsOption with additional sections snapshot: renders correct 1`] = `
<Card
className=" settingsOption border border-light-700 shadow-none"
>
<Card.Section
className="settingsCardTitleSection"
key="settingsOption-Settings Option Title-header"
>
<Advanced
onToggle={[Function]}
open={false}
>
<Trigger
className="collapsible-trigger d-flex"
>
<span
className="flex-grow-1 text-primary-500 x-small"
>
Settings Option Title
</span>
<Visible
whenClosed={true}
>
<Icon />
</Visible>
<Visible
whenOpen={true}
>
<Icon />
</Visible>
</Trigger>
</Advanced>
</Card.Section>
<CardSection
isCardCollapsibleOpen={false}
key="settingsOption-Settings Option Title-children"
none={false}
summary="Settings Option Summary"
>
<h1>
First Section
</h1>
</CardSection>
<CardSection
isCardCollapsibleOpen={false}
key="settingsOption-Settings Option Title-0"
none={false}
summary={null}
/>
<CardSection
isCardCollapsibleOpen={false}
key="settingsOption-Settings Option Title-1"
none={false}
summary={null}
/>
</Card>
`;

View File

@@ -21,10 +21,10 @@ export const showAdvancedSettingsCards = () => {
};
export const showFullCard = () => {
const [isCardCollapsed, setIsCardCollapsed] = module.state.cardCollapsed(false);
const [isCardCollapsibleOpen, setIsCardCollapsibleOpen] = module.state.cardCollapsed(false);
return {
isCardCollapsed,
toggleCardCollapse: () => setIsCardCollapsed(!isCardCollapsed),
isCardCollapsibleOpen,
toggleCardCollapse: () => setIsCardCollapsibleOpen(!isCardCollapsibleOpen),
};
};
@@ -147,8 +147,9 @@ export const scoringCardHooks = (scoring, updateSettings) => {
};
};
export const showAnswerCardHooks = (showAnswer, updateSettings) => {
export const useAnswerSettings = (showAnswer, updateSettings) => {
const [showAttempts, setShowAttempts] = module.state.showAttempts(false);
const numberOfAttemptsChoice = [
ShowAnswerTypesKeys.AFTER_SOME_NUMBER_OF_ATTEMPTS,
ShowAnswerTypesKeys.AFTER_ALL_ATTEMPTS,
@@ -173,9 +174,14 @@ export const showAnswerCardHooks = (showAnswer, updateSettings) => {
updateSettings({ showAnswer: { ...showAnswer, afterAttempts: attempts } });
};
const handleExplanationChange = (event) => {
updateSettings({ solutionExplanation: event.target.value });
};
return {
handleShowAnswerChange,
handleAttemptsChange,
handleExplanationChange,
showAttempts,
};
};

View File

@@ -53,7 +53,7 @@ describe('Problem settings hooks', () => {
output = hooks.showFullCard();
});
test('test default state is false', () => {
expect(output.isCardCollapsed).toBeFalsy();
expect(output.isCardCollapsibleOpen).toBeFalsy();
});
test('test toggleCardCollapse to true', () => {
output.toggleCardCollapse();
@@ -220,7 +220,7 @@ describe('Problem settings hooks', () => {
afterAttempts: 5,
};
beforeEach(() => {
output = hooks.showAnswerCardHooks(showAnswer, updateSettings);
output = hooks.useAnswerSettings(showAnswer, updateSettings);
});
test('test handleShowAnswerChange', () => {
const value = 'always';
@@ -232,6 +232,11 @@ describe('Problem settings hooks', () => {
output.handleAttemptsChange({ target: { value } });
expect(updateSettings).toHaveBeenCalledWith({ showAnswer: { ...showAnswer, afterAttempts: parseInt(value) } });
});
test('handleExplanationChange should update settings', () => {
const value = 'explanation';
output.handleExplanationChange({ target: { value } });
expect(updateSettings).toHaveBeenCalledWith({ solutionExplanation: value });
});
});
describe('Timer card hooks', () => {

View File

@@ -67,7 +67,11 @@ export const SettingsWidget = ({
<Collapsible.Advanced open={isAdvancedCardsVisible}>
<Collapsible.Body className="collapsible-body">
<div className="my-3">
<ShowAnswerCard showAnswer={settings.showAnswer} updateSettings={updateSettings} />
<ShowAnswerCard
showAnswer={settings.showAnswer}
updateSettings={updateSettings}
solutionExplanation={settings.solutionExplanation}
/>
</div>
<div className="my-3">
<ResetCard showResetButton={settings.showResetButton} updateSettings={updateSettings} />

View File

@@ -194,6 +194,16 @@ export const messages = {
defaultMessage: 'Switch To Advanced Editor',
description: 'message to confirm that a user wants to use the advanced editor',
},
explanationInputLabel: {
id: 'authoring.problemeditor.settings.showAnswer.explanation.inputLabel',
defaultMessage: 'Explanation',
description: 'answer explanation input label',
},
explanationSettingText: {
id: 'authoring.problemeditor.settings.showAnswer.explanation.text',
defaultMessage: 'Provide an explanation for the correct answer.',
description: 'Solution Explanation text',
},
};
export default messages;

View File

@@ -7,10 +7,11 @@ import SettingsOption from '../SettingsOption';
import { ShowAnswerTypes, ShowAnswerTypesKeys } from '../../../../../../data/constants/problem';
import { selectors } from '../../../../../../data/redux';
import messages from '../messages';
import { showAnswerCardHooks } from '../hooks';
import { useAnswerSettings } from '../hooks';
export const ShowAnswerCard = ({
showAnswer,
solutionExplanation,
updateSettings,
// inject
intl,
@@ -21,24 +22,23 @@ export const ShowAnswerCard = ({
const {
handleShowAnswerChange,
handleAttemptsChange,
handleExplanationChange,
showAttempts,
} = showAnswerCardHooks(showAnswer, updateSettings);
return (
<SettingsOption
title={intl.formatMessage(messages.showAnswerSettingsTitle)}
summary={intl.formatMessage(ShowAnswerTypes[showAnswer.on])}
>
<div className="halfSpacedMessage">
} = useAnswerSettings(showAnswer, updateSettings);
const showAnswerSection = (
<>
<div className="pb-2">
<span>
<FormattedMessage {...messages.showAnswerSettingText} />
</span>
</div>
<div className="spacedMessage">
<div className="pb-4">
<Hyperlink destination={`${studioEndpointUrl}/settings/advanced/${learningContextId}`} target="_blank">
<FormattedMessage {...messages.advancedSettingsLinkText} />
</Hyperlink>
</div>
<Form.Group>
<Form.Group className="pb-0 mb-0">
<Form.Control
as="select"
value={showAnswer.on}
@@ -56,7 +56,7 @@ export const ShowAnswerCard = ({
</Form.Group>
{showAttempts
&& (
<Form.Group>
<Form.Group className="pb-0 mb-0">
<Form.Control
type="number"
value={showAnswer.afterAttempts}
@@ -65,6 +65,33 @@ export const ShowAnswerCard = ({
/>
</Form.Group>
)}
</>
);
const explanationSection = (
<>
<div className="pb-3">
<span>
<FormattedMessage {...messages.explanationSettingText} />
</span>
</div>
<Form.Group className="pb-0">
<Form.Control
value={solutionExplanation}
onChange={handleExplanationChange}
floatingLabel={intl.formatMessage(messages.explanationInputLabel)}
/>
</Form.Group>
</>
);
return (
<SettingsOption
title={intl.formatMessage(messages.showAnswerSettingsTitle)}
summary={intl.formatMessage(ShowAnswerTypes[showAnswer.on])}
extraSections={[{ children: explanationSection }]}
>
{showAnswerSection}
</SettingsOption>
);
};
@@ -73,10 +100,14 @@ ShowAnswerCard.propTypes = {
intl: intlShape.isRequired,
// eslint-disable-next-line
showAnswer: PropTypes.any.isRequired,
solutionExplanation: PropTypes.string,
updateSettings: PropTypes.func.isRequired,
studioEndpointUrl: PropTypes.string.isRequired,
learningContextId: PropTypes.string.isRequired,
};
ShowAnswerCard.defaultProps = {
solutionExplanation: '',
};
export const mapStateToProps = (state) => ({
studioEndpointUrl: selectors.app.studioEndpointUrl(state),

View File

@@ -3,10 +3,10 @@ import { shallow } from 'enzyme';
import { formatMessage } from '../../../../../../../testUtils';
import { selectors } from '../../../../../../data/redux';
import { ShowAnswerCard, mapStateToProps, mapDispatchToProps } from './ShowAnswerCard';
import { showAnswerCardHooks } from '../hooks';
import { useAnswerSettings } from '../hooks';
jest.mock('../hooks', () => ({
showAnswerCardHooks: jest.fn(),
useAnswerSettings: jest.fn(),
}));
jest.mock('../../../../../../data/redux', () => ({
@@ -34,17 +34,17 @@ describe('ShowAnswerCard', () => {
learningContextId: 'sOMEcouRseId',
};
const showAnswerCardHooksProps = {
handleShowAnswerChange: jest.fn().mockName('showAnswerCardHooks.handleShowAnswerChange'),
handleAttemptsChange: jest.fn().mockName('showAnswerCardHooks.handleAttemptsChange'),
const useAnswerSettingsProps = {
handleShowAnswerChange: jest.fn().mockName('useAnswerSettings.handleShowAnswerChange'),
handleAttemptsChange: jest.fn().mockName('useAnswerSettings.handleAttemptsChange'),
};
showAnswerCardHooks.mockReturnValue(showAnswerCardHooksProps);
useAnswerSettings.mockReturnValue(useAnswerSettingsProps);
describe('behavior', () => {
it(' calls showAnswerCardHooks when initialized', () => {
it(' calls useAnswerSettings when initialized', () => {
shallow(<ShowAnswerCard {...props} />);
expect(showAnswerCardHooks).toHaveBeenCalledWith(showAnswer, props.updateSettings);
expect(useAnswerSettings).toHaveBeenCalledWith(showAnswer, props.updateSettings);
});
});

View File

@@ -3,6 +3,7 @@
exports[`HintsCard snapshot snapshot: renders hints setting card multiple hints 1`] = `
<SettingsOption
className=""
extraSections={Array []}
none={false}
summary=" {count, plural, =0 {} other {(+# more)}}"
title="Hints"
@@ -40,6 +41,7 @@ exports[`HintsCard snapshot snapshot: renders hints setting card multiple hints
exports[`HintsCard snapshot snapshot: renders hints setting card no hints 1`] = `
<SettingsOption
className=""
extraSections={Array []}
none={true}
summary="None"
title="Hints"
@@ -63,6 +65,7 @@ exports[`HintsCard snapshot snapshot: renders hints setting card no hints 1`] =
exports[`HintsCard snapshot snapshot: renders hints setting card one hint 1`] = `
<SettingsOption
className=""
extraSections={Array []}
none={false}
summary="hint1 {count, plural, =0 {} other {(+# more)}}"
title="Hints"

View File

@@ -3,6 +3,7 @@
exports[`MatlabCard snapshot snapshot: renders matlab setting card 1`] = `
<SettingsOption
className=""
extraSections={Array []}
none={false}
summary="matlab_api_key"
title="MATLAB API Key"
@@ -48,6 +49,7 @@ exports[`MatlabCard snapshot snapshot: renders matlab setting card 1`] = `
exports[`MatlabCard snapshot snapshot: renders matlab setting card no key 1`] = `
<SettingsOption
className=""
extraSections={Array []}
none={true}
summary="None"
title="MATLAB API Key"

View File

@@ -3,7 +3,7 @@
exports[`ResetCard snapshot snapshot: renders reset true setting card 1`] = `
<SettingsOption
className="resetCard"
none={false}
extraSections={Array []}
summary="False"
title="Show reset option"
>
@@ -65,7 +65,7 @@ exports[`ResetCard snapshot snapshot: renders reset true setting card 1`] = `
exports[`ResetCard snapshot snapshot: renders reset true setting card 2`] = `
<SettingsOption
className="resetCard"
none={false}
extraSections={Array []}
summary="True"
title="Show reset option"
>

View File

@@ -3,7 +3,7 @@
exports[`ScoringCard snapshot snapshot: scoring setting card 1`] = `
<SettingsOption
className=""
none={false}
extraSections={Array []}
summary="{attempts, plural, =1 {# attempt} other {# attempts}} · {weight, plural, =0 {Ungraded} other {# points}}"
title="Scoring"
>
@@ -52,7 +52,7 @@ exports[`ScoringCard snapshot snapshot: scoring setting card 1`] = `
exports[`ScoringCard snapshot snapshot: scoring setting card max attempts 1`] = `
<SettingsOption
className=""
none={false}
extraSections={Array []}
summary="Unlimited attempts · {weight, plural, =0 {Ungraded} other {# points}}"
title="Scoring"
>
@@ -101,7 +101,7 @@ exports[`ScoringCard snapshot snapshot: scoring setting card max attempts 1`] =
exports[`ScoringCard snapshot snapshot: scoring setting card zero zero weight 1`] = `
<SettingsOption
className=""
none={false}
extraSections={Array []}
summary="{attempts, plural, =1 {# attempt} other {# attempts}} · {weight, plural, =0 {Ungraded} other {# points}}"
title="Scoring"
>

View File

@@ -3,12 +3,38 @@
exports[`ShowAnswerCard snapshot snapshot: show answer setting card 1`] = `
<SettingsOption
className=""
none={false}
extraSections={
Array [
Object {
"children": <React.Fragment>
<div
className="pb-3"
>
<span>
<FormattedMessage
defaultMessage="Provide an explanation for the correct answer."
description="Solution Explanation text"
id="authoring.problemeditor.settings.showAnswer.explanation.text"
/>
</span>
</div>
<Form.Group
className="pb-0"
>
<Form.Control
floatingLabel="Explanation"
value=""
/>
</Form.Group>
</React.Fragment>,
},
]
}
summary="After Some Number of Attempts"
title="Show answer"
>
<div
className="halfSpacedMessage"
className="pb-2"
>
<span>
<FormattedMessage
@@ -19,7 +45,7 @@ exports[`ShowAnswerCard snapshot snapshot: show answer setting card 1`] = `
</span>
</div>
<div
className="spacedMessage"
className="pb-4"
>
<Hyperlink
destination="SoMEeNDpOinT/settings/advanced/sOMEcouRseId"
@@ -32,10 +58,12 @@ exports[`ShowAnswerCard snapshot snapshot: show answer setting card 1`] = `
/>
</Hyperlink>
</div>
<Form.Group>
<Form.Group
className="pb-0 mb-0"
>
<Form.Control
as="select"
onChange={[MockFunction showAnswerCardHooks.handleShowAnswerChange]}
onChange={[MockFunction useAnswerSettings.handleShowAnswerChange]}
value="after_attempts"
>
<option

View File

@@ -3,7 +3,7 @@
exports[`TimerCard snapshot snapshot: renders reset true setting card 1`] = `
<SettingsOption
className=""
none={false}
extraSections={Array []}
summary="5 seconds"
title="Time between attempts"
>

View File

@@ -3,7 +3,7 @@
exports[`TypeCard snapshot snapshot: renders type setting card 1`] = `
<SettingsOption
className=""
none={false}
extraSections={Array []}
summary="Text input"
title="Type"
>

View File

@@ -8,24 +8,24 @@ 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',
'@_type',
'additional_answer',
'checkboxgroup',
'choicegroup',
'additional_answer',
'stringequalhint',
'textline',
'@_type',
'formulaequationinput',
'numericalresponse',
'stringresponse',
'multiplechoiceresponse',
'choiceresponse',
'optionresponse',
'correcthint',
'demandhint',
'formulaequationinput',
'multiplechoiceresponse',
'numericalresponse',
'optioninput',
'optionresponse',
'responseparam',
'solution',
'stringequalhint',
'stringresponse',
'textline',
];
export class OLXParser {
@@ -327,6 +327,38 @@ export class OLXParser {
return hintsObject;
}
#extractTextAndChildren(node) {
const children = [];
let text = null;
if (_.isArray(node)) {
children.push(...node);
} else if (_.isPlainObject(node)) {
text = _.get(node, '#text');
const nodeWithoutText = _.omit(node, '#text');
children.push(...Object.values(nodeWithoutText));
}
return { text, children };
}
getSolutionExplanation() {
if (!_.has(this.problem, 'solution')) { return null; }
const stack = [this.problem.solution];
const texts = [];
let currentNode;
while (stack.length) {
currentNode = stack.pop();
const { text, children } = this.#extractTextAndChildren(currentNode);
if (text) { texts.push(text); }
stack.push(...children);
}
return texts.reverse().join('\n ');
}
getFeedback(xmlElement) {
return _.has(xmlElement, 'correcthint') ? _.get(xmlElement, 'correcthint.#text') : '';
}
@@ -365,6 +397,8 @@ export class OLXParser {
const problemType = this.getProblemType();
const hints = this.getHints();
const question = this.parseQuestions(problemType);
const solutionExplanation = this.getSolutionExplanation();
switch (problemType) {
case ProblemTypeKeys.DROPDOWN:
answersObject = this.parseMultipleChoiceAnswers(ProblemTypeKeys.DROPDOWN, 'optioninput', 'option');
@@ -400,6 +434,8 @@ export class OLXParser {
}
const { answers } = answersObject;
const settings = { hints };
if (solutionExplanation) { settings.solutionExplanation = solutionExplanation; }
return {
question,
settings,

View File

@@ -1,11 +1,12 @@
import { OLXParser } from './OLXParser';
import {
checkboxesOLXWithFeedbackAndHintsOLX,
getCheckboxesOLXWithFeedbackAndHintsOLX,
dropdownOLXWithFeedbackAndHintsOLX,
numericInputWithFeedbackAndHintsOLX,
numericInputWithFeedbackAndHintsOLXException,
textInputWithFeedbackAndHintsOLX,
mutlipleChoiceWithFeedbackAndHintsOLX,
multipleChoiceWithFeedbackAndHintsOLX,
textInputWithFeedbackAndHintsOLXWithMultipleAnswers,
advancedProblemOlX,
multipleProblemOlX,
@@ -31,7 +32,7 @@ describe('Check OLXParser problem type', () => {
expect(problemType).toBe(ProblemTypeKeys.DROPDOWN);
});
test('Test multiple choice with feedback and hints problem type', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const olxparser = new OLXParser(multipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const problemType = olxparser.getProblemType();
expect(problemType).toBe(ProblemTypeKeys.SINGLESELECT);
});
@@ -74,9 +75,9 @@ describe('Check OLXParser hints', () => {
expect(hints).toEqual(dropdownOLXWithFeedbackAndHintsOLX.hints);
});
test('Test multiple choice with feedback and hints problem type', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const olxparser = new OLXParser(multipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const hints = olxparser.getHints();
expect(hints).toEqual(mutlipleChoiceWithFeedbackAndHintsOLX.hints);
expect(hints).toEqual(multipleChoiceWithFeedbackAndHintsOLX.hints);
});
test('Test textual problem type', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLX.rawOLX);
@@ -97,9 +98,9 @@ describe('Check OLXParser for answer parsing', () => {
expect(answer).toEqual(dropdownOLXWithFeedbackAndHintsOLX.data);
});
test('Test multiple choice single select', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const olxparser = new OLXParser(multipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const answer = olxparser.parseMultipleChoiceAnswers('multiplechoiceresponse', 'choicegroup', 'choice');
expect(answer).toEqual(mutlipleChoiceWithFeedbackAndHintsOLX.data);
expect(answer).toEqual(multipleChoiceWithFeedbackAndHintsOLX.data);
});
test('Test string response answers', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLX.rawOLX);
@@ -135,9 +136,9 @@ describe('Check OLXParser for question parsing', () => {
expect(question).toEqual(dropdownOLXWithFeedbackAndHintsOLX.question);
});
test('Test multiple choice single select question', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const olxparser = new OLXParser(multipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const question = olxparser.parseQuestions('multiplechoiceresponse');
expect(question).toEqual(mutlipleChoiceWithFeedbackAndHintsOLX.question);
expect(question).toEqual(multipleChoiceWithFeedbackAndHintsOLX.question);
});
test('Test string response question', () => {
const olxparser = new OLXParser(textInputWithFeedbackAndHintsOLX.rawOLX);
@@ -161,3 +162,20 @@ describe('Check OLXParser for question parsing', () => {
expect(question).toBe(blankQuestionOLX.question);
});
});
describe('OLXParser for problem with solution tag', () => {
describe('for checkbox questions', () => {
test('should parse simple text', () => {
const olxparser = new OLXParser(checkboxesOLXWithFeedbackAndHintsOLX.rawOLX);
const explanation = olxparser.getSolutionExplanation();
expect(explanation).toEqual(checkboxesOLXWithFeedbackAndHintsOLX.solutionExplanation);
});
test('should parse text in p tags', () => {
const { rawOLX } = getCheckboxesOLXWithFeedbackAndHintsOLX({ solution: 'html' });
const olxparser = new OLXParser(rawOLX);
const explanation = olxparser.getSolutionExplanation();
const expected = getCheckboxesOLXWithFeedbackAndHintsOLX({ solution: 'html' }).solutionExplanation;
expect(explanation.replace(/\s/g, '')).toBe(expected.replace(/\s/g, ''));
});
});
});

View File

@@ -36,6 +36,18 @@ class ReactStateOLXParser {
return demandhint;
}
addSolution() {
if (!_.has(this.problemState, 'settings.solutionExplanation')) { return {}; }
const solutionText = _.get(this.problemState, 'settings.solutionExplanation');
const solutionObject = {
solution: {
'#text': solutionText,
},
};
return solutionObject;
}
addMultiSelectAnswers(option) {
const choice = [];
let compoundhint = [];
@@ -106,6 +118,8 @@ class ReactStateOLXParser {
const question = this.addQuestion();
const widgetObject = this.addMultiSelectAnswers(option);
const demandhint = this.addHints();
const solution = this.addSolution();
const problemObject = {
problem: {
[problemType]: {
@@ -113,6 +127,7 @@ class ReactStateOLXParser {
[widget]: widgetObject,
},
...demandhint,
...solution,
},
};
return this.builder.build(problemObject);
@@ -122,6 +137,8 @@ class ReactStateOLXParser {
const question = this.addQuestion();
const demandhint = this.addHints();
const answerObject = this.buildTextInputAnswersFeedback();
const solution = this.addSolution();
const problemObject = {
problem: {
[ProblemTypeKeys.TEXTINPUT]: {
@@ -129,6 +146,7 @@ class ReactStateOLXParser {
...answerObject,
},
...demandhint,
...solution,
},
};
return this.builder.build(problemObject);
@@ -178,11 +196,14 @@ class ReactStateOLXParser {
const question = this.addQuestion();
const demandhint = this.addHints();
const answerObject = this.buildNumericalResponse();
const solution = this.addSolution();
const problemObject = {
problem: {
...question,
[ProblemTypeKeys.NUMERIC]: answerObject,
...demandhint,
...solution,
},
};
return this.builder.build(problemObject);
@@ -255,6 +276,7 @@ class ReactStateOLXParser {
buildOLX() {
const { problemType } = this.problemState;
let problemString = '';
switch (problemType) {
case ProblemTypeKeys.MULTISELECT:
problemString = this.buildMultiSelectProblem(ProblemTypeKeys.MULTISELECT, 'checkboxgroup', 'choice');

View File

@@ -5,7 +5,7 @@ import {
numericInputWithFeedbackAndHintsOLX,
numericInputWithFeedbackAndHintsOLXException,
textInputWithFeedbackAndHintsOLX,
mutlipleChoiceWithFeedbackAndHintsOLX,
multipleChoiceWithFeedbackAndHintsOLX,
textInputWithFeedbackAndHintsOLXWithMultipleAnswers,
} from './mockData/olxTestData';
import ReactStateOLXParser from './ReactStateOLXParser';
@@ -33,11 +33,11 @@ describe('Check React Sate OLXParser problem', () => {
expect(buildOLX).toEqual(textInputWithFeedbackAndHintsOLX.buildOLX);
});
test('Test multiple choice with feedback and hints problem type', () => {
const olxparser = new OLXParser(mutlipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const olxparser = new OLXParser(multipleChoiceWithFeedbackAndHintsOLX.rawOLX);
const problem = olxparser.getParsedOLXData();
const stateParser = new ReactStateOLXParser({ problem });
const buildOLX = stateParser.buildOLX();
expect(buildOLX).toEqual(mutlipleChoiceWithFeedbackAndHintsOLX.buildOLX);
expect(buildOLX).toEqual(multipleChoiceWithFeedbackAndHintsOLX.buildOLX);
});
test('Test numerical response with feedback and hints problem type', () => {
const olxparser = new OLXParser(numericInputWithFeedbackAndHintsOLX.rawOLX);

View File

@@ -1,38 +1,55 @@
export const checkboxesOLXWithFeedbackAndHintsOLX = {
export const getCheckboxesOLXWithFeedbackAndHintsOLX = ({ solution = 'simple' }) => ({
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.',
},
<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>
${solution === 'simple' ? '<solution>This is a detailed explanation of the solution.</solution>' : (
`<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>
You can form a voltage divider that evenly divides the input
voltage with two identically valued resistors, with the sampled
voltage taken in between the two.
</p>
<p><img src="/static/images/voltage_divider.png" alt=""/></p>
</div>
</solution>`
)}
</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.',
},
],
solutionExplanation: solution === 'simple' ? 'This is a detailed explanation of the solution.' : (
'Explanation\n You can form a voltage divider that evenly divides the input voltage with two identically valued resistors, with the sampled voltage taken in between the two.'
),
data: {
answers: [
{
@@ -107,9 +124,19 @@ an incorrect answer <choicehint selected="true">You can specify optional
<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>
${solution === 'simple' ? '<solution>This is a detailed explanation of the solution.</solution>' : (
`<solution>
Explanation\n
You can form a voltage divider that evenly divides the input
voltage with two identically valued resistors, with the sampled
voltage taken in between the two.
</solution>`
)}
</problem>
`,
};
});
export const checkboxesOLXWithFeedbackAndHintsOLX = getCheckboxesOLXWithFeedbackAndHintsOLX({});
export const dropdownOLXWithFeedbackAndHintsOLX = {
rawOLX: `<problem>
@@ -184,7 +211,7 @@ an incorrect answer <optionhint>You can specify optional feedback for non
`,
};
export const mutlipleChoiceWithFeedbackAndHintsOLX = {
export const multipleChoiceWithFeedbackAndHintsOLX = {
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>

View File

@@ -29,6 +29,7 @@ const initialState = {
afterAttempts: 0,
},
showResetButton: false,
solutionExplanation: '',
},
};