Files
frontend-app-authoring/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js
2023-03-22 09:36:01 -04:00

395 lines
12 KiB
JavaScript

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,
};
const questionParserOptions = {
ignoreAttributes: false,
alwaysCreateTextNode: true,
preserveOrder: true,
};
const questionBuilderOptions = {
ignoreAttributes: false,
attributeNamePrefix: '@_',
suppressBooleanAttributes: false,
format: true,
preserveOrder: true,
};
const builderOptions = {
ignoreAttributes: false,
attributeNamePrefix: '@_',
suppressBooleanAttributes: false,
format: true,
};
this.questionParser = new XMLParser(questionParserOptions);
this.parser = new XMLParser(parserOptions);
this.builder = new XMLBuilder(builderOptions);
this.questionBuilder = new XMLBuilder(questionBuilderOptions);
this.editorObject = problemState.editorObject;
this.problemState = problemState.problem;
}
addHints() {
const hintsArray = [];
const { hints } = this.editorObject;
hints.forEach(hint => {
if (hint.length > 0) {
const parsedHint = this.parser.parse(hint);
hintsArray.push({
...parsedHint,
});
}
});
const demandhint = {
demandhint: {
hint: hintsArray,
},
};
return demandhint;
}
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,
},
},
};
return solutionObject;
}
addMultiSelectAnswers(option) {
const choice = [];
let compoundhint = [];
let widget = {};
// eslint-disable-next-line prefer-const
let { answers, problemType } = this.problemState;
const answerTitles = this.editorObject?.answers;
const { selectedFeedback, unselectedFeedback } = this.editorObject;
/* todo */
/*
* the logic for general feedback is ot current being used.
* when component is updated will need to return to this code.
* general feedback replaces selected feedback if all incorrect selected feedback is the same.
* ******************************************
if (generalFeedback !== ''
&& answers.every(
answer => (
answer.correct
? true
: answer?.selectedFeedback === answers.find(a => a.correct === false).selectedFeedback
),
)) {
answers = answers.map(answer => (!answer?.correct
? { ...answer, selectedFeedback: generalFeedback }
: answer));
}
*/
answers.forEach((answer) => {
const feedback = [];
let singleAnswer = {};
const title = answerTitles ? this.parser.parse(answerTitles[answer.id]) : { '#text': answer.title };
const currentSelectedFeedback = selectedFeedback?.[answer.id] || null;
const currentUnselectedFeedback = unselectedFeedback?.[answer.id] || null;
let isEmpty;
if (answerTitles) {
isEmpty = Object.keys(title)?.length <= 0;
} else {
isEmpty = title['#text']?.length <= 0;
}
if (title && !isEmpty) {
if (currentSelectedFeedback && problemType === ProblemTypeKeys.MULTISELECT) {
const parsedSelectedFeedback = this.parser.parse(currentSelectedFeedback);
feedback.push({
...parsedSelectedFeedback,
'@_selected': true,
});
}
if (currentSelectedFeedback && problemType !== ProblemTypeKeys.MULTISELECT) {
const parsedSelectedFeedback = this.parser.parse(currentSelectedFeedback);
feedback.push({
...parsedSelectedFeedback,
});
}
if (currentUnselectedFeedback && problemType === ProblemTypeKeys.MULTISELECT) {
const parsedUnselectedFeedback = this.parser.parse(currentUnselectedFeedback);
feedback.push({
...parsedUnselectedFeedback,
'@_selected': false,
});
}
if (feedback.length) {
singleAnswer[`${option}hint`] = feedback;
}
singleAnswer = {
'@_correct': answer.correct,
...title,
...singleAnswer,
};
choice.push(singleAnswer);
}
});
widget = { [option]: choice };
if (_.has(this.problemState, 'groupFeedbackList') && problemType === ProblemTypeKeys.MULTISELECT) {
compoundhint = this.addGroupFeedbackList();
widget = {
...widget,
compoundhint,
};
}
return widget;
}
addGroupFeedbackList() {
const compoundhint = [];
const { groupFeedbackList } = this.problemState;
const { groupFeedback } = this.editorObject;
groupFeedbackList.forEach((element) => {
const feedbackString = groupFeedback?.[element.id];
const parsedFeedback = this.parser.parse(feedbackString);
compoundhint.push({
...parsedFeedback,
'@_value': element.answers.join(' '),
});
});
return compoundhint;
}
addQuestion() {
const { question } = this.editorObject;
const questionObject = this.questionParser.parse(question);
return questionObject;
}
buildMultiSelectProblem(problemType, widget, option) {
const question = this.addQuestion();
const widgetObject = this.addMultiSelectAnswers(option);
const demandhint = this.addHints();
const solution = this.addSolution();
const problemObject = {
problem: {
[problemType]: {
[widget]: widgetObject,
...solution,
},
...demandhint,
},
};
const problem = this.builder.build(problemObject);
const questionString = this.questionBuilder.build(question);
let problemTypeTag;
switch (problemType) {
case ProblemTypeKeys.MULTISELECT:
[problemTypeTag] = problem.match(/<choiceresponse>|<choiceresponse.[^>]+>/);
break;
case ProblemTypeKeys.DROPDOWN:
[problemTypeTag] = problem.match(/<optionresponse>|<optionresponse.[^>]+>/);
break;
case ProblemTypeKeys.SINGLESELECT:
[problemTypeTag] = problem.match(/<multiplechoiceresponse>|<multiplechoiceresponse.[^>]+>/);
break;
default:
break;
}
const updatedString = `${problemTypeTag}\n${questionString}`;
const problemString = problem.replace(problemTypeTag, updatedString);
return problemString;
}
buildTextInput() {
const question = this.addQuestion();
const demandhint = this.addHints();
const answerObject = this.buildTextInputAnswersFeedback();
const solution = this.addSolution();
const problemObject = {
problem: {
[ProblemTypeKeys.TEXTINPUT]: {
...answerObject,
...solution,
},
...demandhint,
},
};
const problem = this.builder.build(problemObject);
const questionString = this.questionBuilder.build(question);
const [problemTypeTag] = problem.match(/<stringresponse>|<stringresponse.[^>]+>/);
const updatedString = `${problemTypeTag}\n${questionString}`;
const problemString = problem.replace(problemTypeTag, updatedString);
return problemString;
}
buildTextInputAnswersFeedback() {
const { answers } = this.problemState;
const { selectedFeedback } = this.editorObject;
let answerObject = {};
const additionAnswers = [];
const wrongAnswers = [];
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,
});
} else if (answer.correct && !firstCorrectAnswerParsed) {
firstCorrectAnswerParsed = true;
answerObject = {
'@_answer': answer.title,
...correcthint,
};
} else if (!answer.correct) {
const wronghint = correcthint.correcthint;
wrongAnswers.push({
'@_answer': answer.title,
...wronghint,
});
}
}
});
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 solution = this.addSolution();
const problemObject = {
problem: {
[ProblemTypeKeys.NUMERIC]: {
...answerObject,
...solution,
},
...demandhint,
},
};
const problem = this.builder.build(problemObject);
const questionString = this.questionBuilder.build(question);
const [problemTypeTag] = problem.match(/<numericalresponse>|<numericalresponse.[^>]+>/);
const updatedString = `${questionString}\n${problemTypeTag}`;
const problemString = problem.replace(problemTypeTag, updatedString);
return problemString;
}
buildNumericalResponse() {
const { answers } = this.problemState;
const { selectedFeedback } = this.editorObject;
let answerObject = {};
const additionalAnswers = [];
let firstCorrectAnswerParsed = false;
answers.forEach((answer) => {
const correcthint = this.getAnswerHints(selectedFeedback?.[answer.id]);
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(feedback) {
let correcthint = {};
if (feedback !== undefined && feedback !== '') {
const parsedFeedback = this.parser.parse(feedback);
correcthint = {
correcthint: {
...parsedFeedback,
},
};
}
return correcthint;
}
hasAttributeWithValue(obj, attr) {
return _.has(obj, attr) && _.get(obj, attr, '').toString().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;