fix: olx parsers with formatted text (#336)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
||||
import _ from 'lodash-es';
|
||||
import { ProblemTypeKeys } from '../../../data/constants/problem';
|
||||
import { ProblemTypeKeys, RichTextProblems } from '../../../data/constants/problem';
|
||||
|
||||
export const indexToLetterMap = [...Array(26)].map((val, i) => String.fromCharCode(i + 65));
|
||||
|
||||
@@ -64,7 +64,8 @@ export class OLXParser {
|
||||
constructor(olxString) {
|
||||
this.problem = {};
|
||||
this.questionData = {};
|
||||
const questionOptions = {
|
||||
this.richTextProblem = {};
|
||||
const richTextOptions = {
|
||||
ignoreAttributes: false,
|
||||
alwaysCreateTextNode: true,
|
||||
numberParseOptions: {
|
||||
@@ -91,20 +92,74 @@ export class OLXParser {
|
||||
},
|
||||
processEntities: false,
|
||||
};
|
||||
// There are two versions of the parsed XLM because the question requires the order of the
|
||||
// parsed data to be preserved. However, all the other widgets need the data grouped by
|
||||
const richTextBuilderOptions = {
|
||||
ignoreAttributes: false,
|
||||
numberParseOptions: {
|
||||
leadingZeros: false,
|
||||
hex: false,
|
||||
},
|
||||
preserveOrder: true,
|
||||
processEntities: false,
|
||||
};
|
||||
// There are two versions of the parsed XLM because the fields using tinymce require the order
|
||||
// of the parsed data to be preserved. However, all the other widgets need the data grouped by
|
||||
// the wrapping tag.
|
||||
const questionParser = new XMLParser(questionOptions);
|
||||
const richTextParser = new XMLParser(richTextOptions);
|
||||
const parser = new XMLParser(parserOptions);
|
||||
this.builder = new XMLBuilder(builderOptions);
|
||||
this.richTextBuilder = new XMLBuilder(richTextBuilderOptions);
|
||||
this.parsedOLX = parser.parse(olxString);
|
||||
this.parsedQuestionOLX = questionParser.parse(olxString);
|
||||
this.richTextOLX = richTextParser.parse(olxString);
|
||||
if (_.has(this.parsedOLX, 'problem')) {
|
||||
this.problem = this.parsedOLX.problem;
|
||||
this.questionData = this.parsedQuestionOLX[0].problem;
|
||||
this.questionData = this.richTextOLX[0].problem;
|
||||
this.richTextProblem = this.richTextOLX[0].problem;
|
||||
}
|
||||
}
|
||||
|
||||
/** getPreservedAnswersAndFeedback(problemType, widgetName, option)
|
||||
* getPreservedAnswersAndFeedback takes a problemType, widgetName, and a valid option. The
|
||||
* olx for the given problem type and widget is parsed. Do to the structure of xml that is
|
||||
* parsed with the prsereved attribute, the function has to loop through arrays of objects.
|
||||
* The first for-loop checks for feedback tags and answer choices and appended to the
|
||||
* preservedAnswers. The nested for loop checks for feedback and answer values inside the
|
||||
* option (answer) tags.
|
||||
* @param {string} problemType - string of the olx problem type
|
||||
* @param {string} widgetName - string of the wrapping tag name
|
||||
* (optioninput, choicegroup, checkboxgroup, addition_answer)
|
||||
* @param {string} option - string of the type of answers (choice, option, correcthint, stringequalhint)
|
||||
* @return {array} array containing answer objects and possibly an array of grouped feedback
|
||||
*/
|
||||
getPreservedAnswersAndFeedback(problemType, widgetName, option) {
|
||||
const [problemBody] = this.richTextProblem.filter(section => Object.keys(section).includes(problemType));
|
||||
const isChoiceProblem = !([ProblemTypeKeys.NUMERIC, ProblemTypeKeys.TEXTINPUT].includes(problemType));
|
||||
const preservedAnswers = [];
|
||||
let correctAnswerFeedbackTag = option;
|
||||
let incorrectAnswerFeedbackTag;
|
||||
if (problemType === ProblemTypeKeys.TEXTINPUT) {
|
||||
[correctAnswerFeedbackTag, incorrectAnswerFeedbackTag] = option;
|
||||
}
|
||||
const problemBodyArr = problemBody[problemType];
|
||||
problemBodyArr.forEach(subtag => {
|
||||
const tagNames = Object.keys(subtag);
|
||||
if (!isChoiceProblem && tagNames.includes(correctAnswerFeedbackTag)) {
|
||||
preservedAnswers.unshift(subtag[correctAnswerFeedbackTag]);
|
||||
}
|
||||
if (problemType === ProblemTypeKeys.TEXTINPUT && tagNames.includes(incorrectAnswerFeedbackTag)) {
|
||||
preservedAnswers.push(subtag);
|
||||
}
|
||||
if (tagNames.includes(widgetName)) {
|
||||
const currentAnswerArr = subtag[widgetName];
|
||||
currentAnswerArr.forEach(answer => {
|
||||
if (Object.keys(answer).includes(correctAnswerFeedbackTag)) {
|
||||
preservedAnswers.push(answer[correctAnswerFeedbackTag]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return preservedAnswers;
|
||||
}
|
||||
|
||||
/** parseMultipleChoiceAnswers(problemType, widgetName, option)
|
||||
* parseMultipleChoiceAnswers takes a problemType, widgetName, and a valid option. The
|
||||
* olx for the given problem type and widget is parsed. Depending on the problem
|
||||
@@ -119,6 +174,11 @@ export class OLXParser {
|
||||
* @return {object} object containing an array of answer objects and possibly an array of grouped feedback
|
||||
*/
|
||||
parseMultipleChoiceAnswers(problemType, widgetName, option) {
|
||||
const preservedAnswers = this.getPreservedAnswersAndFeedback(
|
||||
problemType,
|
||||
widgetName,
|
||||
option,
|
||||
);
|
||||
const answers = [];
|
||||
let data = {};
|
||||
const widget = _.get(this.problem, `${problemType}.${widgetName}`);
|
||||
@@ -127,7 +187,7 @@ export class OLXParser {
|
||||
throw new Error('Misc Tags, reverting to Advanced Editor');
|
||||
}
|
||||
const choice = _.get(widget, option);
|
||||
const isComplexAnswer = [ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType);
|
||||
const isComplexAnswer = RichTextProblems.includes(problemType);
|
||||
if (_.isEmpty(choice)) {
|
||||
answers.push(
|
||||
{
|
||||
@@ -138,14 +198,14 @@ export class OLXParser {
|
||||
);
|
||||
} else if (_.isArray(choice)) {
|
||||
choice.forEach((element, index) => {
|
||||
const [preservedAnswer, ...preservedFeedback] = preservedAnswers[index];
|
||||
let title = element['#text'];
|
||||
if (isComplexAnswer) {
|
||||
const answerTitle = stripNonTextTags({ input: element, tag: `${option}hint` });
|
||||
title = this.builder.build(answerTitle);
|
||||
if (isComplexAnswer && preservedAnswer) {
|
||||
title = this.richTextBuilder.build([preservedAnswer]);
|
||||
}
|
||||
const correct = eval(element['@_correct'].toLowerCase());
|
||||
const id = indexToLetterMap[index];
|
||||
const feedback = this.getAnswerFeedback(element, `${option}hint`);
|
||||
const feedback = this.getAnswerFeedback(preservedFeedback, `${option}hint`);
|
||||
answers.push(
|
||||
{
|
||||
id,
|
||||
@@ -156,12 +216,12 @@ export class OLXParser {
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const [preservedAnswer, ...preservedFeedback] = preservedAnswers[0];
|
||||
let title = choice['#text'];
|
||||
if (isComplexAnswer) {
|
||||
const answerTitle = stripNonTextTags({ input: choice, tag: `${option}hint` });
|
||||
title = this.builder.build(answerTitle);
|
||||
if (isComplexAnswer && preservedAnswer) {
|
||||
title = this.richTextBuilder.build([preservedAnswer]);
|
||||
}
|
||||
const feedback = this.getAnswerFeedback(choice, `${option}hint`);
|
||||
const feedback = this.getAnswerFeedback(preservedFeedback, `${option}hint`);
|
||||
answers.push({
|
||||
correct: eval(choice['@_correct'].toLowerCase()),
|
||||
id: indexToLetterMap[answers.length],
|
||||
@@ -180,38 +240,28 @@ export class OLXParser {
|
||||
return data;
|
||||
}
|
||||
|
||||
/** getAnswerFeedback(choice, hintKey)
|
||||
* getAnswerFeedback takes a choice and a valid option. The choice object is checked for
|
||||
* selected and unselected feedback. The respective values are added to the feedback object.
|
||||
* The feedback object is returned.
|
||||
/** getAnswerFeedback(preservedFeedback, hintKey)
|
||||
* getAnswerFeedback takes preservedFeedback and a valid option. The preservedFeedback object
|
||||
* is checked for selected and unselected feedback. The respective values are added to the
|
||||
* feedback object. The feedback object is returned.
|
||||
* @param {object} choice - object of an answer choice
|
||||
* @param {string} hintKey - string of the wrapping tag name (optionhint or choicehint)
|
||||
* @return {object} object containing selected and unselected feedback
|
||||
*/
|
||||
getAnswerFeedback(choice, hintKey) {
|
||||
let feedback = {};
|
||||
getAnswerFeedback(preservedFeedback, hintKey) {
|
||||
const feedback = {};
|
||||
let feedbackKeys = 'selectedFeedback';
|
||||
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]: this.builder.build(element),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
if (_.has(answerFeedback, '@_selected')) {
|
||||
feedbackKeys = eval(answerFeedback['@_selected'].toLowerCase()) ? 'selectedFeedback' : 'unselectedFeedback';
|
||||
if (_.isEmpty(preservedFeedback)) { return feedback; }
|
||||
|
||||
preservedFeedback.forEach((feedbackArr) => {
|
||||
if (_.has(feedbackArr, hintKey)) {
|
||||
if (_.has(feedbackArr, ':@') && _.has(feedbackArr[':@'], '@_selected')) {
|
||||
const isSelectedFeedback = feedbackArr[':@']['@_selected'] === 'true';
|
||||
feedbackKeys = isSelectedFeedback ? 'selectedFeedback' : 'unselectedFeedback';
|
||||
}
|
||||
feedback = {
|
||||
[feedbackKeys]: this.builder.build(answerFeedback),
|
||||
};
|
||||
feedback[feedbackKeys] = this.richTextBuilder.build(feedbackArr[hintKey]);
|
||||
}
|
||||
}
|
||||
});
|
||||
return feedback;
|
||||
}
|
||||
|
||||
@@ -256,24 +306,32 @@ export class OLXParser {
|
||||
* @return {object} object containing an array of answer objects and object of additionalStringAttributes
|
||||
*/
|
||||
parseStringResponse() {
|
||||
const [firstCorrectFeedback, ...preservedFeedback] = this.getPreservedAnswersAndFeedback(
|
||||
ProblemTypeKeys.TEXTINPUT,
|
||||
'additional_answer',
|
||||
['correcthint', 'stringequalhint'],
|
||||
);
|
||||
const { stringresponse } = this.problem;
|
||||
const answers = [];
|
||||
let answerFeedback = '';
|
||||
let additionalStringAttributes = {};
|
||||
let data = {};
|
||||
const feedback = this.getFeedback(stringresponse);
|
||||
const firstFeedback = this.getFeedback(firstCorrectFeedback);
|
||||
answers.push({
|
||||
id: indexToLetterMap[answers.length],
|
||||
title: stringresponse['@_answer'],
|
||||
correct: true,
|
||||
selectedFeedback: feedback,
|
||||
selectedFeedback: firstFeedback,
|
||||
});
|
||||
|
||||
const additionalAnswerFeedback = preservedFeedback.filter(feedback => _.isArray(feedback));
|
||||
const stringEqualHintFeedback = preservedFeedback.filter(feedback => !_.isArray(feedback));
|
||||
|
||||
// Parsing additional_answer for string response.
|
||||
const additionalAnswer = _.get(stringresponse, 'additional_answer', []);
|
||||
if (_.isArray(additionalAnswer)) {
|
||||
additionalAnswer.forEach((newAnswer) => {
|
||||
answerFeedback = this.getFeedback(newAnswer);
|
||||
additionalAnswer.forEach((newAnswer, indx) => {
|
||||
answerFeedback = this.getFeedback(additionalAnswerFeedback[indx]);
|
||||
answers.push({
|
||||
id: indexToLetterMap[answers.length],
|
||||
title: newAnswer['@_answer'],
|
||||
@@ -282,7 +340,7 @@ export class OLXParser {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
answerFeedback = this.getFeedback(additionalAnswer);
|
||||
answerFeedback = this.getFeedback(additionalAnswerFeedback[0]);
|
||||
answers.push({
|
||||
id: indexToLetterMap[answers.length],
|
||||
title: additionalAnswer['@_answer'],
|
||||
@@ -294,9 +352,8 @@ export class OLXParser {
|
||||
// Parsing stringequalhint for string response.
|
||||
const stringEqualHint = _.get(stringresponse, 'stringequalhint', []);
|
||||
if (_.isArray(stringEqualHint)) {
|
||||
stringEqualHint.forEach((newAnswer) => {
|
||||
const parsedFeedback = stripNonTextTags({ input: newAnswer, tag: '@_answer' });
|
||||
answerFeedback = this.builder.build(parsedFeedback);
|
||||
stringEqualHint.forEach((newAnswer, indx) => {
|
||||
answerFeedback = this.richTextBuilder.build(stringEqualHintFeedback[indx].stringequalhint);
|
||||
answers.push({
|
||||
id: indexToLetterMap[answers.length],
|
||||
title: newAnswer['@_answer'],
|
||||
@@ -305,8 +362,7 @@ export class OLXParser {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const parsedFeedback = stripNonTextTags({ input: stringEqualHint, tag: '@_answer' });
|
||||
answerFeedback = this.builder.build(parsedFeedback);
|
||||
answerFeedback = this.richTextBuilder.build(stringEqualHintFeedback[0].stringequalhint);
|
||||
answers.push({
|
||||
id: indexToLetterMap[answers.length],
|
||||
title: stringEqualHint['@_answer'],
|
||||
@@ -340,11 +396,16 @@ export class OLXParser {
|
||||
* @return {object} object containing an array of answer objects
|
||||
*/
|
||||
parseNumericResponse() {
|
||||
const [firstCorrectFeedback, ...preservedFeedback] = this.getPreservedAnswersAndFeedback(
|
||||
ProblemTypeKeys.NUMERIC,
|
||||
'additional_answer',
|
||||
'correcthint',
|
||||
);
|
||||
const { numericalresponse } = this.problem;
|
||||
let answerFeedback = '';
|
||||
const answers = [];
|
||||
let responseParam = {};
|
||||
const feedback = this.getFeedback(numericalresponse);
|
||||
const feedback = this.getFeedback(firstCorrectFeedback);
|
||||
if (_.has(numericalresponse, 'responseparam')) {
|
||||
const type = _.get(numericalresponse, 'responseparam.@_type');
|
||||
const defaultValue = _.get(numericalresponse, 'responseparam.@_default');
|
||||
@@ -365,8 +426,8 @@ export class OLXParser {
|
||||
// Parsing additional_answer for numerical response.
|
||||
const additionalAnswer = _.get(numericalresponse, 'additional_answer', []);
|
||||
if (_.isArray(additionalAnswer)) {
|
||||
additionalAnswer.forEach((newAnswer) => {
|
||||
answerFeedback = this.getFeedback(newAnswer);
|
||||
additionalAnswer.forEach((newAnswer, indx) => {
|
||||
answerFeedback = this.getFeedback(preservedFeedback[indx]);
|
||||
answers.push({
|
||||
id: indexToLetterMap[answers.length],
|
||||
title: newAnswer['@_answer'],
|
||||
@@ -375,7 +436,7 @@ export class OLXParser {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
answerFeedback = this.getFeedback(additionalAnswer);
|
||||
answerFeedback = this.getFeedback(preservedFeedback[0]);
|
||||
answers.push({
|
||||
id: indexToLetterMap[answers.length],
|
||||
title: additionalAnswer['@_answer'],
|
||||
@@ -397,16 +458,6 @@ export class OLXParser {
|
||||
* @return {string} string of OLX
|
||||
*/
|
||||
parseQuestions(problemType) {
|
||||
const options = {
|
||||
ignoreAttributes: false,
|
||||
numberParseOptions: {
|
||||
leadingZeros: false,
|
||||
hex: false,
|
||||
},
|
||||
preserveOrder: true,
|
||||
processEntities: false,
|
||||
};
|
||||
const builder = new XMLBuilder(options);
|
||||
const problemArray = _.get(this.questionData[0], problemType) || this.questionData;
|
||||
|
||||
const questionArray = [];
|
||||
@@ -429,7 +480,7 @@ export class OLXParser {
|
||||
});
|
||||
}
|
||||
});
|
||||
const questionString = builder.build(questionArray);
|
||||
const questionString = this.richTextBuilder.build(questionArray);
|
||||
return questionString.replace(/<description>/gm, '<em>').replace(/<\/description>/gm, '</em>');
|
||||
}
|
||||
|
||||
@@ -442,28 +493,26 @@ export class OLXParser {
|
||||
getHints() {
|
||||
const hintsObject = [];
|
||||
if (_.has(this.problem, 'demandhint.hint')) {
|
||||
const hint = _.get(this.problem, 'demandhint.hint');
|
||||
if (_.isArray(hint)) {
|
||||
hint.forEach(element => {
|
||||
const hintValue = this.builder.build(element);
|
||||
hintsObject.push({
|
||||
id: hintsObject.length,
|
||||
value: hintValue,
|
||||
const preservedProblem = this.richTextProblem;
|
||||
preservedProblem.forEach(obj => {
|
||||
const objKeys = Object.keys(obj);
|
||||
if (objKeys.includes('demandhint')) {
|
||||
const currentDemandHint = obj.demandhint;
|
||||
currentDemandHint.forEach(hint => {
|
||||
const hintValue = this.richTextBuilder.build(hint.hint);
|
||||
hintsObject.push({
|
||||
id: hintsObject.length,
|
||||
value: hintValue,
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const hintValue = this.builder.build(hint);
|
||||
hintsObject.push({
|
||||
id: hintsObject.length,
|
||||
value: hintValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return hintsObject;
|
||||
}
|
||||
|
||||
/** parseQuestions(problemType)
|
||||
* parseQuestions takes a problemType. The problem type is used to determine where the
|
||||
/** getSolutionExplanation(problemType)
|
||||
* getSolutionExplanation takes a problemType. The problem type is used to determine where the
|
||||
* text for the solution lies (sibling or child to warpping problem type tags).
|
||||
* Using the XMLBuilder, the solution is built removing the redundant "explanation" that is
|
||||
* appended for Studio styling purposes. The string version of the OLX is return.
|
||||
@@ -472,31 +521,22 @@ export class OLXParser {
|
||||
*/
|
||||
getSolutionExplanation(problemType) {
|
||||
if (!_.has(this.problem, `${problemType}.solution`) && !_.has(this.problem, 'solution')) { return null; }
|
||||
let solution = _.get(this.problem, `${problemType}.solution`, null) || _.get(this.problem, 'solution', null);
|
||||
const wrapper = Object.keys(solution)[0];
|
||||
if (Object.keys(solution).length === 1 && wrapper === 'div') {
|
||||
const parsedSolution = {};
|
||||
Object.entries(solution.div).forEach(([key, value]) => {
|
||||
if (key.indexOf('@_' === -1)) {
|
||||
// The redundant "explanation" title should be removed.
|
||||
// If the key is a paragraph or h2, and the text of either the first or only item is "Explanation."
|
||||
if (
|
||||
(key === 'p' || key === 'h2')
|
||||
&& (_.get(value, '#text', null) === 'Explanation'
|
||||
|| (_.isArray(value) && _.get(value[0], '#text', null) === 'Explanation'))
|
||||
) {
|
||||
if (_.isArray(value)) {
|
||||
value.shift();
|
||||
parsedSolution[key] = value;
|
||||
}
|
||||
} else {
|
||||
parsedSolution[key] = value;
|
||||
}
|
||||
const [problemBody] = this.richTextProblem.filter(section => Object.keys(section).includes(problemType));
|
||||
let { solution } = problemBody[problemType].pop();
|
||||
const { div } = solution[0];
|
||||
if (solution.length === 1 && div) {
|
||||
div.forEach((block) => {
|
||||
const [key] = Object.keys(block);
|
||||
const [value] = block[key];
|
||||
if ((key === 'p' || key === 'h2')
|
||||
&& (_.get(value, '#text', null) === 'Explanation')
|
||||
) {
|
||||
div.shift();
|
||||
}
|
||||
});
|
||||
solution = parsedSolution;
|
||||
solution = div;
|
||||
}
|
||||
const solutionString = this.builder.build(solution);
|
||||
const solutionString = this.richTextBuilder.build(solution);
|
||||
return solutionString;
|
||||
}
|
||||
|
||||
@@ -508,9 +548,8 @@ export class OLXParser {
|
||||
* @return {string} string of feedback
|
||||
*/
|
||||
getFeedback(xmlElement) {
|
||||
if (!_.has(xmlElement, 'correcthint')) { return ''; }
|
||||
const feedback = _.get(xmlElement, 'correcthint');
|
||||
const feedbackString = this.builder.build(feedback);
|
||||
if (_.isEmpty(xmlElement)) { return ''; }
|
||||
const feedbackString = this.richTextBuilder.build(xmlElement);
|
||||
return feedbackString;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,7 @@ import { ToleranceTypes } from '../components/EditProblemView/SettingsWidget/set
|
||||
|
||||
class ReactStateOLXParser {
|
||||
constructor(problemState) {
|
||||
const parserOptions = {
|
||||
ignoreAttributes: false,
|
||||
alwaysCreateTextNode: true,
|
||||
numberParseOptions: {
|
||||
leadingZeros: false,
|
||||
hex: false,
|
||||
},
|
||||
};
|
||||
const questionParserOptions = {
|
||||
const richTextParserOptions = {
|
||||
ignoreAttributes: false,
|
||||
alwaysCreateTextNode: true,
|
||||
numberParseOptions: {
|
||||
@@ -22,7 +14,7 @@ class ReactStateOLXParser {
|
||||
},
|
||||
preserveOrder: true,
|
||||
};
|
||||
const questionBuilderOptions = {
|
||||
const richTextBuilderOptions = {
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: '@_',
|
||||
suppressBooleanAttributes: false,
|
||||
@@ -33,20 +25,9 @@ class ReactStateOLXParser {
|
||||
},
|
||||
preserveOrder: true,
|
||||
};
|
||||
const builderOptions = {
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: '@_',
|
||||
suppressBooleanAttributes: false,
|
||||
format: true,
|
||||
numberParseOptions: {
|
||||
leadingZeros: false,
|
||||
hex: false,
|
||||
},
|
||||
};
|
||||
this.questionParser = new XMLParser(questionParserOptions);
|
||||
this.parser = new XMLParser(parserOptions);
|
||||
this.builder = new XMLBuilder(builderOptions);
|
||||
this.questionBuilder = new XMLBuilder(questionBuilderOptions);
|
||||
|
||||
this.richTextParser = new XMLParser(richTextParserOptions);
|
||||
this.richTextBuilder = new XMLBuilder(richTextBuilderOptions);
|
||||
this.editorObject = problemState.editorObject;
|
||||
this.problemState = problemState.problem;
|
||||
}
|
||||
@@ -61,21 +42,17 @@ class ReactStateOLXParser {
|
||||
const hintsArray = [];
|
||||
const { hints } = this.editorObject;
|
||||
if (hints.length < 1) {
|
||||
return {};
|
||||
return hintsArray;
|
||||
}
|
||||
hints.forEach(hint => {
|
||||
if (hint.length > 0) {
|
||||
const parsedHint = this.parser.parse(hint);
|
||||
const parsedHint = this.richTextParser.parse(hint);
|
||||
hintsArray.push({
|
||||
...parsedHint,
|
||||
hint: [...parsedHint],
|
||||
});
|
||||
}
|
||||
});
|
||||
const demandhint = {
|
||||
demandhint: {
|
||||
hint: hintsArray,
|
||||
},
|
||||
};
|
||||
const demandhint = [{ demandhint: hintsArray }];
|
||||
return demandhint;
|
||||
}
|
||||
|
||||
@@ -88,19 +65,16 @@ class ReactStateOLXParser {
|
||||
*/
|
||||
addSolution() {
|
||||
const { solution } = this.editorObject;
|
||||
if (!solution || solution.length <= 0) { return {}; }
|
||||
const solutionTitle = { '#text': 'Explanation' };
|
||||
const parsedSolution = this.parser.parse(solution);
|
||||
const paragraphs = parsedSolution.p;
|
||||
const withWrapper = _.isArray(paragraphs) ? [solutionTitle, ...paragraphs] : [solutionTitle, paragraphs];
|
||||
const solutionObject = {
|
||||
solution: {
|
||||
div: {
|
||||
'@_class': 'detailed-solution',
|
||||
p: withWrapper,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (!solution || solution.length <= 0) { return []; }
|
||||
const solutionTitle = { p: [{ '#text': 'Explanation' }] };
|
||||
const parsedSolution = this.richTextParser.parse(solution);
|
||||
const withWrapper = [solutionTitle, ...parsedSolution];
|
||||
const solutionObject = [{
|
||||
solution: [{
|
||||
':@': { '@_class': 'detailed-solution' },
|
||||
div: [...withWrapper],
|
||||
}],
|
||||
}];
|
||||
return solutionObject;
|
||||
}
|
||||
|
||||
@@ -120,7 +94,6 @@ class ReactStateOLXParser {
|
||||
addMultiSelectAnswers(option) {
|
||||
const choice = [];
|
||||
let compoundhint = [];
|
||||
let widget = {};
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { answers, problemType } = this.problemState;
|
||||
const answerTitles = this.editorObject?.answers;
|
||||
@@ -146,8 +119,8 @@ class ReactStateOLXParser {
|
||||
*/
|
||||
answers.forEach((answer) => {
|
||||
const feedback = [];
|
||||
let singleAnswer = {};
|
||||
const title = answerTitles ? this.parser.parse(answerTitles[answer.id]) : { '#text': answer.title };
|
||||
let singleAnswer = [];
|
||||
const title = answerTitles ? this.richTextParser.parse(answerTitles[answer.id]) : [{ '#text': answer.title }];
|
||||
const currentSelectedFeedback = selectedFeedback?.[answer.id] || null;
|
||||
const currentUnselectedFeedback = unselectedFeedback?.[answer.id] || null;
|
||||
let isEmpty;
|
||||
@@ -158,45 +131,37 @@ class ReactStateOLXParser {
|
||||
}
|
||||
if (title && !isEmpty) {
|
||||
if (currentSelectedFeedback && problemType === ProblemTypeKeys.MULTISELECT) {
|
||||
const parsedSelectedFeedback = this.parser.parse(currentSelectedFeedback);
|
||||
const parsedSelectedFeedback = this.richTextParser.parse(currentSelectedFeedback);
|
||||
feedback.push({
|
||||
...parsedSelectedFeedback,
|
||||
'@_selected': true,
|
||||
':@': { '@_selected': true },
|
||||
[`${option}hint`]: parsedSelectedFeedback,
|
||||
});
|
||||
}
|
||||
if (currentSelectedFeedback && problemType !== ProblemTypeKeys.MULTISELECT) {
|
||||
const parsedSelectedFeedback = this.parser.parse(currentSelectedFeedback);
|
||||
const parsedSelectedFeedback = this.richTextParser.parse(currentSelectedFeedback);
|
||||
feedback.push({
|
||||
...parsedSelectedFeedback,
|
||||
[`${option}hint`]: parsedSelectedFeedback,
|
||||
});
|
||||
}
|
||||
if (currentUnselectedFeedback && problemType === ProblemTypeKeys.MULTISELECT) {
|
||||
const parsedUnselectedFeedback = this.parser.parse(currentUnselectedFeedback);
|
||||
const parsedUnselectedFeedback = this.richTextParser.parse(currentUnselectedFeedback);
|
||||
feedback.push({
|
||||
...parsedUnselectedFeedback,
|
||||
'@_selected': false,
|
||||
':@': { '@_selected': false },
|
||||
[`${option}hint`]: parsedUnselectedFeedback,
|
||||
});
|
||||
}
|
||||
if (feedback.length) {
|
||||
singleAnswer[`${option}hint`] = feedback;
|
||||
}
|
||||
singleAnswer = {
|
||||
'@_correct': answer.correct,
|
||||
...title,
|
||||
...singleAnswer,
|
||||
':@': { '@_correct': answer.correct },
|
||||
[option]: [...title, ...feedback],
|
||||
};
|
||||
choice.push(singleAnswer);
|
||||
}
|
||||
});
|
||||
widget = { [option]: choice };
|
||||
if (_.has(this.problemState, 'groupFeedbackList') && problemType === ProblemTypeKeys.MULTISELECT) {
|
||||
compoundhint = this.addGroupFeedbackList();
|
||||
widget = {
|
||||
...widget,
|
||||
compoundhint,
|
||||
};
|
||||
choice.push(...compoundhint);
|
||||
}
|
||||
return widget;
|
||||
return choice;
|
||||
}
|
||||
|
||||
/** addGroupFeedbackList()
|
||||
@@ -210,8 +175,8 @@ class ReactStateOLXParser {
|
||||
const { groupFeedbackList } = this.problemState;
|
||||
groupFeedbackList.forEach((element) => {
|
||||
compoundhint.push({
|
||||
'#text': element.feedback,
|
||||
'@_value': element.answers.join(' '),
|
||||
compoundhint: [{ '#text': element.feedback }],
|
||||
':@': { '@_value': element.answers.join(' ') },
|
||||
});
|
||||
});
|
||||
return compoundhint;
|
||||
@@ -224,7 +189,7 @@ class ReactStateOLXParser {
|
||||
*/
|
||||
addQuestion() {
|
||||
const { question } = this.editorObject;
|
||||
const questionObject = this.questionParser.parse(question);
|
||||
const questionObject = this.richTextParser.parse(question);
|
||||
/* Removes block tags like <p> or <h1> that surround the <label> format.
|
||||
Block tags are required by tinyMCE but have adverse effect on css in studio.
|
||||
*/
|
||||
@@ -260,36 +225,36 @@ class ReactStateOLXParser {
|
||||
const demandhint = this.addHints();
|
||||
const solution = this.addSolution();
|
||||
|
||||
const problemObject = {
|
||||
problem: {
|
||||
[problemType]: {
|
||||
[widget]: widgetObject,
|
||||
...solution,
|
||||
},
|
||||
...demandhint,
|
||||
},
|
||||
};
|
||||
const problemBodyArr = [{
|
||||
[problemType]: [
|
||||
{ [widget]: widgetObject },
|
||||
...solution,
|
||||
],
|
||||
}];
|
||||
|
||||
const problem = this.builder.build(problemObject);
|
||||
const questionString = this.questionBuilder.build(question);
|
||||
const questionString = this.richTextBuilder.build(question);
|
||||
const hintString = this.richTextBuilder.build(demandhint);
|
||||
const problemBody = this.richTextBuilder.build(problemBodyArr);
|
||||
let problemTypeTag;
|
||||
|
||||
switch (problemType) {
|
||||
case ProblemTypeKeys.MULTISELECT:
|
||||
[problemTypeTag] = problem.match(/<choiceresponse>|<choiceresponse.[^>]+>/);
|
||||
[problemTypeTag] = problemBody.match(/<choiceresponse>|<choiceresponse.[^>]+>/);
|
||||
break;
|
||||
case ProblemTypeKeys.DROPDOWN:
|
||||
[problemTypeTag] = problem.match(/<optionresponse>|<optionresponse.[^>]+>/);
|
||||
[problemTypeTag] = problemBody.match(/<optionresponse>|<optionresponse.[^>]+>/);
|
||||
break;
|
||||
case ProblemTypeKeys.SINGLESELECT:
|
||||
[problemTypeTag] = problem.match(/<multiplechoiceresponse>|<multiplechoiceresponse.[^>]+>/);
|
||||
[problemTypeTag] = problemBody.match(/<multiplechoiceresponse>|<multiplechoiceresponse.[^>]+>/);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const updatedString = `${problemTypeTag}\n${questionString}`;
|
||||
const problemString = problem.replace(problemTypeTag, updatedString);
|
||||
const problemBodyString = problemBody.replace(problemTypeTag, updatedString);
|
||||
const fullProblemString = `<problem>${problemBodyString}${hintString}\n</problem>`;
|
||||
|
||||
return problemString;
|
||||
return fullProblemString;
|
||||
}
|
||||
|
||||
/** buildTextInput()
|
||||
@@ -304,23 +269,17 @@ class ReactStateOLXParser {
|
||||
const answerObject = this.buildTextInputAnswersFeedback();
|
||||
const solution = this.addSolution();
|
||||
|
||||
const problemObject = {
|
||||
problem: {
|
||||
[ProblemTypeKeys.TEXTINPUT]: {
|
||||
...answerObject,
|
||||
...solution,
|
||||
},
|
||||
...demandhint,
|
||||
},
|
||||
};
|
||||
answerObject[ProblemTypeKeys.TEXTINPUT].push(...solution);
|
||||
|
||||
const problem = this.builder.build(problemObject);
|
||||
const questionString = this.questionBuilder.build(question);
|
||||
const [problemTypeTag] = problem.match(/<stringresponse>|<stringresponse.[^>]+>/);
|
||||
const problemBody = this.richTextBuilder.build([answerObject]);
|
||||
const questionString = this.richTextBuilder.build(question);
|
||||
const hintString = this.richTextBuilder.build(demandhint);
|
||||
const [problemTypeTag] = problemBody.match(/<stringresponse>|<stringresponse.[^>]+>/);
|
||||
const updatedString = `${problemTypeTag}\n${questionString}`;
|
||||
const problemString = problem.replace(problemTypeTag, updatedString);
|
||||
const problemBodyString = problemBody.replace(problemTypeTag, updatedString);
|
||||
const fullProblemString = `<problem>${problemBodyString}${hintString}\n</problem>`;
|
||||
|
||||
return problemString;
|
||||
return fullProblemString;
|
||||
}
|
||||
|
||||
/** buildTextInputAnswersFeedback()
|
||||
@@ -338,44 +297,40 @@ class ReactStateOLXParser {
|
||||
* @return {object} object representation of answers
|
||||
*/
|
||||
buildTextInputAnswersFeedback() {
|
||||
const { answers } = this.problemState;
|
||||
const { answers, problemType } = this.problemState;
|
||||
const { selectedFeedback } = this.editorObject;
|
||||
let answerObject = {};
|
||||
const additionAnswers = [];
|
||||
const wrongAnswers = [];
|
||||
let answerObject = { [problemType]: [] };
|
||||
let firstCorrectAnswerParsed = false;
|
||||
answers.forEach((answer) => {
|
||||
const correcthint = this.getAnswerHints(selectedFeedback?.[answer.id]);
|
||||
if (this.hasAttributeWithValue(answer, 'title')) {
|
||||
if (answer.correct && firstCorrectAnswerParsed) {
|
||||
additionAnswers.push({
|
||||
'@_answer': answer.title,
|
||||
...correcthint,
|
||||
answerObject[problemType].push({
|
||||
':@': { '@_answer': answer.title },
|
||||
additional_answer: [...correcthint],
|
||||
});
|
||||
} else if (answer.correct && !firstCorrectAnswerParsed) {
|
||||
firstCorrectAnswerParsed = true;
|
||||
answerObject = {
|
||||
'@_answer': answer.title,
|
||||
...correcthint,
|
||||
':@': {
|
||||
'@_answer': answer.title,
|
||||
'@_type': _.get(this.problemState, 'additionalAttributes.type', 'ci'),
|
||||
},
|
||||
[problemType]: [...correcthint],
|
||||
};
|
||||
} else if (!answer.correct) {
|
||||
const wronghint = correcthint.correcthint;
|
||||
wrongAnswers.push({
|
||||
'@_answer': answer.title,
|
||||
...wronghint,
|
||||
const wronghint = correcthint[0]?.correcthint;
|
||||
answerObject[problemType].push({
|
||||
':@': { '@_answer': answer.title },
|
||||
stringequalhint: wronghint ? [...wronghint] : [],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
answerObject = {
|
||||
...answerObject,
|
||||
additional_answer: additionAnswers,
|
||||
stringequalhint: wrongAnswers,
|
||||
'@_type': _.get(this.problemState, 'additionalAttributes.type', 'ci'),
|
||||
textline: {
|
||||
'@_size': _.get(this.problemState, 'additionalAttributes.textline.size', 20),
|
||||
},
|
||||
};
|
||||
answerObject[problemType].push({
|
||||
textline: { '#text': '' },
|
||||
':@': { '@_size': _.get(this.problemState, 'additionalAttributes.textline.size', 20) },
|
||||
});
|
||||
return answerObject;
|
||||
}
|
||||
|
||||
@@ -391,23 +346,17 @@ class ReactStateOLXParser {
|
||||
const answerObject = this.buildNumericalResponse();
|
||||
const solution = this.addSolution();
|
||||
|
||||
const problemObject = {
|
||||
problem: {
|
||||
[ProblemTypeKeys.NUMERIC]: {
|
||||
...answerObject,
|
||||
...solution,
|
||||
},
|
||||
...demandhint,
|
||||
},
|
||||
};
|
||||
answerObject[ProblemTypeKeys.NUMERIC].push(...solution);
|
||||
|
||||
const problem = this.builder.build(problemObject);
|
||||
const questionString = this.questionBuilder.build(question);
|
||||
const [problemTypeTag] = problem.match(/<numericalresponse>|<numericalresponse.[^>]+>/);
|
||||
const problemBody = this.richTextBuilder.build([answerObject]);
|
||||
const questionString = this.richTextBuilder.build(question);
|
||||
const hintString = this.richTextBuilder.build(demandhint);
|
||||
const [problemTypeTag] = problemBody.match(/<numericalresponse>|<numericalresponse.[^>]+>/);
|
||||
const updatedString = `${questionString}\n${problemTypeTag}`;
|
||||
const problemString = problem.replace(problemTypeTag, updatedString);
|
||||
const problemBodyString = problemBody.replace(problemTypeTag, updatedString);
|
||||
const fullProblemString = `<problem>${problemBodyString}${hintString}\n</problem>`;
|
||||
|
||||
return problemString;
|
||||
return fullProblemString;
|
||||
}
|
||||
|
||||
/** buildNumericalResponse()
|
||||
@@ -423,11 +372,10 @@ class ReactStateOLXParser {
|
||||
* @return {object} object representation of answers
|
||||
*/
|
||||
buildNumericalResponse() {
|
||||
const { answers } = this.problemState;
|
||||
const { answers, problemType } = this.problemState;
|
||||
const { tolerance } = this.problemState.settings;
|
||||
const { selectedFeedback } = this.editorObject;
|
||||
let answerObject = {};
|
||||
const additionalAnswers = [];
|
||||
let answerObject = { [problemType]: [] };
|
||||
let firstCorrectAnswerParsed = false;
|
||||
answers.forEach((answer) => {
|
||||
const correcthint = this.getAnswerHints(selectedFeedback?.[answer.id]);
|
||||
@@ -472,35 +420,29 @@ class ReactStateOLXParser {
|
||||
}
|
||||
if (answer.correct && !firstCorrectAnswerParsed) {
|
||||
firstCorrectAnswerParsed = true;
|
||||
let responseParam = {};
|
||||
const responseParam = [];
|
||||
if (tolerance?.value) {
|
||||
responseParam = {
|
||||
responseparam: {
|
||||
responseParam.push({
|
||||
responseparam: [],
|
||||
':@': {
|
||||
'@_type': 'tolerance',
|
||||
'@_default': `${tolerance.value}${tolerance.type === ToleranceTypes.number.type ? '' : '%'}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
answerObject = {
|
||||
'@_answer': title,
|
||||
...responseParam,
|
||||
...correcthint,
|
||||
':@': { '@_answer': title },
|
||||
[problemType]: [...responseParam, ...correcthint],
|
||||
};
|
||||
} else if (answer.correct && firstCorrectAnswerParsed) {
|
||||
additionalAnswers.push({
|
||||
'@_answer': title,
|
||||
...correcthint,
|
||||
answerObject[problemType].push({
|
||||
':@': { '@_answer': title },
|
||||
additional_answer: [...correcthint],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
answerObject = {
|
||||
...answerObject,
|
||||
additional_answer: additionalAnswers,
|
||||
formulaequationinput: {
|
||||
'#text': '',
|
||||
},
|
||||
};
|
||||
answerObject[problemType].push({ formulaequationinput: { '#text': '' } });
|
||||
return answerObject;
|
||||
}
|
||||
|
||||
@@ -513,14 +455,10 @@ class ReactStateOLXParser {
|
||||
* @return {object} object representaion of feedback
|
||||
*/
|
||||
getAnswerHints(feedback) {
|
||||
let correcthint = {};
|
||||
const correcthint = [];
|
||||
if (feedback !== undefined && feedback !== '') {
|
||||
const parsedFeedback = this.parser.parse(feedback);
|
||||
correcthint = {
|
||||
correcthint: {
|
||||
...parsedFeedback,
|
||||
},
|
||||
};
|
||||
const parsedFeedback = this.richTextParser.parse(feedback);
|
||||
correcthint.push({ correcthint: parsedFeedback });
|
||||
}
|
||||
return correcthint;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import ReactStateOLXParser from './ReactStateOLXParser';
|
||||
|
||||
describe('Check React State OLXParser problem', () => {
|
||||
test('Test checkbox with feedback and hints problem type', () => {
|
||||
test('for checkbox with feedback and hints problem type', () => {
|
||||
const olxparser = new OLXParser(checkboxesOLXWithFeedbackAndHintsOLX.rawOLX);
|
||||
const problem = olxparser.getParsedOLXData();
|
||||
const stateParser = new ReactStateOLXParser({
|
||||
|
||||
Reference in New Issue
Block a user