sellquiz
Version:
An open source domain-specific language for online assessment
511 lines • 24.6 kB
JavaScript
/******************************************************************************
* SELL - SIMPLE E-LEARNING LANGUAGE *
* *
* Copyright (c) 2019-2021 TH Köln *
* Author: Andreas Schwenk, contact@compiler-construction.com *
* *
* Partly funded by: Digitale Hochschule NRW *
* https://www.dh.nrw/kooperationen/hm4mint.nrw-31 *
* *
* GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 *
* *
* This library is licensed as described in LICENSE, which you should have *
* received as part of this distribution. *
* *
* This software is distributed on "AS IS" basis, WITHOUT WARRENTY OF ANY *
* KIND, either impressed or implied. *
******************************************************************************/
import * as math from 'mathjs';
import * as $ from 'jquery';
import { SellInputElementType } from './quiz.js';
import { sellassert } from './sellassert.js';
import { symtype } from './symbol.js';
import { sellLevenShteinDistance, getHtmlChildElementRecursive } from './help.js';
import { GET_STR, checkmark, crossmark } from './lang.js';
import { SellSymTerm } from './symbolic.js';
import { SellLinAlg } from './linalg.js';
export class Evaluate {
constructor(parent) {
this.selectedAnySingleChoiceOption = false;
this.questionContainsSingleChoice = false;
this.allMultipleChoiceAnswersAreCorrect = true;
this.p = parent;
}
// TODO: replace asserts by error log!!
setStudentAnswerManually(qidx, solutionVariableId, answerStr) {
let q = this.p.getQuestionByIdx(qidx);
if (q == null)
return false;
let input = null;
for (let i = 0; i < q.inputs.length; i++) {
if (q.inputs[i].solutionVariableId == solutionVariableId) {
input = q.inputs[i];
break;
}
}
if (input == null) {
sellassert(false, "setStudentAnswerManually(): could not find input element for given solution variable '" + solutionVariableId + "'");
}
switch (input.htmlElementInputType) {
case SellInputElementType.TEXTFIELD:
input.studentAnswer = [answerStr];
break;
default:
sellassert(false, "setStudentAnswerManually(): UNIMPLEMENTED!");
break;
}
return true;
}
getStudentAnswers(qidx) {
let q = this.p.getQuestionByIdx(qidx);
if (q == null)
return false;
if (q.bodyHtmlElement == null)
sellassert(false, "getStudentAnswers(): bodyHtmlElement was not set");
for (let i = 0; i < q.inputs.length; i++) {
let input = q.inputs[i];
let htmlElement = null;
switch (input.htmlElementInputType) {
case SellInputElementType.TEXTFIELD:
htmlElement = getHtmlChildElementRecursive(q.bodyHtmlElement, input.htmlElementId);
sellassert(htmlElement != null, "getStudentAnswers(): failed to get HTML child element: "
+ input.htmlElementId);
input.studentAnswer = [htmlElement.value];
break;
case SellInputElementType.COMPLEX_NUMBER:
// real part
htmlElement = getHtmlChildElementRecursive(q.bodyHtmlElement, input.htmlElementId + '_real');
sellassert(htmlElement != null, "getStudentAnswers(): failed to get HTML child element: "
+ input.htmlElementId);
if (htmlElement.value.length == 0)
htmlElement.value = "0";
input.studentAnswer = [htmlElement.value];
// imaginay parg
htmlElement = getHtmlChildElementRecursive(q.bodyHtmlElement, input.htmlElementId + '_imag');
sellassert(htmlElement != null, "getStudentAnswers(): failed to get HTML child element: "
+ input.htmlElementId);
if (htmlElement.value.length == 0)
htmlElement.value = "0";
input.studentAnswer.push(htmlElement.value);
break;
case SellInputElementType.CHECKBOX:
htmlElement = getHtmlChildElementRecursive(q.bodyHtmlElement, input.htmlElementId);
sellassert(htmlElement != null, "getStudentAnswers(): failed to get HTML child element: "
+ input.htmlElementId);
input.studentAnswer = [htmlElement.checked ? "true" : "false"];
break;
case SellInputElementType.VECTOR:
input.studentAnswer = [];
for (let i = 0; i < input.vectorLength; i++) {
htmlElement = getHtmlChildElementRecursive(q.bodyHtmlElement, input.htmlElementId + '_' + i);
sellassert(htmlElement != null, "getStudentAnswers(): failed to get HTML child element: "
+ input.htmlElementId);
if (htmlElement.value.length == 0) {
htmlElement.value = "0";
}
input.studentAnswer.push(htmlElement.value);
}
break;
case SellInputElementType.MATRIX:
input.matrixInput.setUnsetElementsToZero();
input.studentAnswer = input.matrixInput.getStudentAnswer();
break;
case SellInputElementType.PROGRAMMING:
htmlElement = getHtmlChildElementRecursive(q.bodyHtmlElement, input.htmlElementId);
sellassert(htmlElement != null, "getStudentAnswers(): failed to get HTML child element: "
+ input.htmlElementId);
let src = input.codeMirror.getValue().split("\n");
// restore given source code:
let givenSrc = input.solutionVariableRef.value["given"].split("\n");
let restored_src = '';
for (let i = 0; i < src.length; i++) {
if (i < givenSrc.length - 1)
restored_src += givenSrc[i] + "\n";
else
restored_src += src[i] + "\n";
}
restored_src = restored_src.replaceAll("§", "");
// set answer to input element
input.studentAnswer = [restored_src];
break;
default:
sellassert(false, "getStudentAnswers(..): UNIMPLEMENTED HTML element type '" + input.htmlElementInputType + "'");
}
}
return true;
}
displayFeedback(qidx) {
let q = this.p.getQuestionByIdx(qidx);
if (q == null)
return false;
if (q.bodyHtmlElement == null)
sellassert(false, "displayFeedback(): bodyHtmlElement was not set");
for (let i = 0; i < q.inputs.length; i++) {
let input = q.inputs[i];
let htmlElement = getHtmlChildElementRecursive(q.bodyHtmlElement, input.htmlElementId_feedback);
sellassert(htmlElement != null, "displayFeedback(): failed to get HTML child element: " + input.htmlElementId);
htmlElement.innerHTML = input.evaluationFeedbackStr;
}
return true;
}
getScore(qidx) {
// TODO: scoring is not yet weighted correctly etc...
let q = this.p.questions[qidx];
if (q == null)
return -1;
let score = 0.0;
if (q.inputs.length == 0)
return score;
for (let i = 0; i < q.inputs.length; i++) {
let input = q.inputs[i];
if (input.correct)
score += 1.0;
}
score /= q.inputs.length;
return score;
}
evaluate(qidx) {
this.selectedAnySingleChoiceOption = false;
this.questionContainsSingleChoice = false;
let q = this.p.questions[qidx];
if (q == null)
return false;
this.checkIfAllMultipleChoiceAnswersAreCorrect(q);
q.generalFeedbackStr = "";
q.allAnswersCorrect = true;
for (let i = 0; i < q.inputs.length; i++) {
let input = q.inputs[i];
let v = q.solutionSymbols[input.solutionVariableId];
sellassert(v != null, "evaluate(): unknown solution symbol " + input.solutionVariableId + " known solution symbols: " + JSON.stringify(q.solutionSymbols));
input.evaluationInProgress = false;
switch (v.type) {
case symtype.T_BOOL:
this.evaluateBool(q, input, v);
break;
case symtype.T_REAL:
this.evaluateReal(q, input, v);
break;
case symtype.T_COMPLEX:
this.evaluateComplex(q, input, v);
break;
case symtype.T_FUNCTION:
this.evaluateFunction(q, input, v);
break;
case symtype.T_STRING_LIST:
this.evaluateStringList(q, input, v);
break;
case symtype.T_SET:
case symtype.T_COMPLEX_SET:
this.evaluateSet(q, input, v);
break;
case symtype.T_MATRIX:
case symtype.T_MATRIX_OF_FUNCTIONS:
this.evaluateMatrix(v.type == symtype.T_MATRIX_OF_FUNCTIONS, q, input, v);
break;
case symtype.T_PROGRAMMING:
input.evaluationInProgress = true;
this.evaluateProgramming(q, input, v);
break;
default:
sellassert(false, "evaluate(): unimplemented math type: " + v.type.toString());
}
}
if (this.questionContainsSingleChoice && this.selectedAnySingleChoiceOption == false) {
q.generalFeedbackStr += GET_STR("no_answer_selected", this.p.language);
}
if (!this.allMultipleChoiceAnswersAreCorrect) {
q.generalFeedbackStr += GET_STR("not_yet_correct", this.p.language);
}
return true;
}
isEvaluationReady(qidx) {
let q = this.p.questions[qidx];
if (q == null)
return true;
for (let i = 0; i < q.inputs.length; i++) {
let input = q.inputs[i];
if (input.evaluationInProgress)
return false;
}
return true;
}
checkIfAllMultipleChoiceAnswersAreCorrect(question) {
this.allMultipleChoiceAnswersAreCorrect = true;
for (let i = 0; i < question.inputs.length; i++) {
let input = question.inputs[i];
if (input.solutionVariableId.includes('_mc_')) {
let studentAnswer = input.studentAnswer[0] === "true";
let solution = question.solutionSymbols[input.solutionVariableId].value;
if (studentAnswer != solution) {
this.allMultipleChoiceAnswersAreCorrect = false;
break;
}
}
}
}
evaluateBool(question, input, solutionVariable) {
let studentAnswer = input.studentAnswer[0] === "true";
input.correct = solutionVariable.value == studentAnswer;
input.evaluationFeedbackStr = input.correct ? checkmark : crossmark;
if (input.solutionVariableId.includes('_sc_')) {
this.questionContainsSingleChoice = true;
// if the input is a single-choice option, then only show feedback, if student
// seleted this option:
if (studentAnswer == false)
input.evaluationFeedbackStr = "";
if (studentAnswer == true)
this.selectedAnySingleChoiceOption = true;
}
else if (input.solutionVariableId.includes('_mc_')) {
// only give feedback, if all multiple-choice answers are correct
if (this.allMultipleChoiceAnswersAreCorrect == false)
input.evaluationFeedbackStr = "";
}
if (input.correct == false)
question.allAnswersCorrect = false;
}
evaluateReal(question, input, solutionVariable) {
let studentAnswerStr = input.studentAnswer[0].replaceAll(',', '.');
let studentAnwser = 0;
let feedback = '';
try {
studentAnwser = math.evaluate(studentAnswerStr);
input.correct = math.abs(solutionVariable.value - studentAnwser) < solutionVariable.precision;
}
catch (e) {
feedback += GET_STR("feedback_syntaxerror", this.p.language).replace("$", studentAnswerStr) + " ";
input.correct = false;
question.allAnswersCorrect = false;
}
input.evaluationFeedbackStr = input.correct ? checkmark : crossmark;
input.evaluationFeedbackStr += feedback;
if (input.correct == false)
question.allAnswersCorrect = false;
}
evaluateComplex(question, input, solutionVariable) {
// TODO: need property in SELL language, if trigonometric functions are allowed -> check here
let studentAnswerStrReal = input.studentAnswer[0].replaceAll(',', '.');
let studentAnswerStrImag = input.studentAnswer[1].replaceAll(',', '.');
let studentAnwserReal = 0, studentAnswerImag = 0;
let feedback = '';
input.correct = true;
try {
studentAnwserReal = math.evaluate(studentAnswerStrReal);
}
catch (e) {
input.correct = false;
question.allAnswersCorrect = false;
feedback += GET_STR("feedback_syntaxerror", this.p.language).replace("$", studentAnswerStrReal) + " ";
}
try {
studentAnswerImag = math.evaluate(studentAnswerStrImag);
}
catch (e) {
input.correct = false;
question.allAnswersCorrect = false;
feedback += GET_STR("feedback_syntaxerror", this.p.language).replace("$", studentAnswerStrImag) + " ";
}
if (input.correct) {
let studentAnswer = math.complex(studentAnwserReal, studentAnswerImag);
input.correct = math.abs(math.subtract(solutionVariable.value, studentAnswer)) < solutionVariable.precision;
}
input.evaluationFeedbackStr = input.correct ? checkmark : crossmark;
input.evaluationFeedbackStr += feedback;
if (input.correct == false)
question.allAnswersCorrect = false;
}
evaluateStringList(question, input, solutionVariable) {
let studentAnswerStr = input.studentAnswer[0];
let feedback = '';
input.correct = true;
for (let i = 0; i < solutionVariable.value.length; i++) {
let sol_i = solutionVariable.value[i];
let levDist = sellLevenShteinDistance(studentAnswerStr, sol_i);
levDist = math.abs(levDist);
let correct = sol_i.length <= 3 ? levDist == 0 : levDist <= 2;
if (sol_i.length > 3 && levDist > 0 && levDist <= 2)
feedback = '<span class="text-warning">' + sol_i + '</span>';
else
feedback = '';
if (correct == false) {
input.correct = false;
question.allAnswersCorrect = false;
break;
}
}
input.evaluationFeedbackStr = input.correct ? checkmark : crossmark;
input.evaluationFeedbackStr += feedback;
}
evaluateFunction(question, input, solutionVariable) {
let studentAnswerStr = input.studentAnswer[0].replaceAll(',', '.');
let feedback = '';
if (studentAnswerStr.length == 0)
studentAnswerStr = "123456789123456789"; // if no answer is given, do not assume "0", since zero is often a valid answer
if (input.solutionVariableId in question.solutionSymbolsMustDiffFirst) {
let studentAnswer = new SellSymTerm();
if (studentAnswer.importTerm(studentAnswerStr) == false) {
question.allAnswersCorrect = false;
input.correct = false;
feedback += GET_STR("feedback_syntaxerror", this.p.language).
replace("$", studentAnswerStr) + " ";
input.evaluationFeedbackStr = crossmark + feedback;
return;
}
let diffVar = question.solutionSymbolsMustDiffFirst[input.solutionVariableId];
studentAnswer = studentAnswer.derivate(diffVar);
studentAnswerStr = studentAnswer.toString();
}
input.correct = solutionVariable.value.compareWithStringTerm(studentAnswerStr);
if (input.correct == false) {
question.allAnswersCorrect = false;
if (solutionVariable.value.state === "syntaxerror") {
feedback += GET_STR("feedback_syntaxerror_or_invalid_variables", this.p.language).
replace("$", studentAnswerStr) + " ";
}
}
input.evaluationFeedbackStr = input.correct ? checkmark : crossmark;
input.evaluationFeedbackStr += feedback;
}
evaluateSet(question, input, solutionVariable) {
let studentAnswer = [];
let n = solutionVariable.value.length;
let feedback = '';
for (let i = 0; i < n; i++) {
let studentAnswerStr_i = input.studentAnswer[i].replaceAll(',', '.').replaceAll('j', 'i');
let studentAnswer_i = 0;
try {
studentAnswer_i = math.evaluate(studentAnswerStr_i);
}
catch (e) {
feedback += GET_STR("feedback_syntaxerror", this.p.language).
replace("$", studentAnswerStr_i) + " ";
}
studentAnswer.push(studentAnswer_i);
}
let num_ok = 0;
for (let i = 0; i < n; i++) {
let sol = solutionVariable.value[i].value;
for (let j = 0; j < n; j++) {
let user_sol = studentAnswer[j];
let diff = math.abs(math.subtract(sol, user_sol));
if (diff < solutionVariable.precision) {
num_ok++;
break;
}
}
}
input.correct = num_ok == n;
if (num_ok < n) {
question.allAnswersCorrect = false;
let f = GET_STR("i_out_of_n_correct", this.p.language) + " ";
f = f.replace('$i', '' + num_ok);
f = f.replace('$n', '' + n);
feedback += f;
}
input.evaluationFeedbackStr = input.correct ? checkmark : crossmark;
input.evaluationFeedbackStr += feedback;
}
evaluateMatrix(functionalElements, question, input, solutionVariable) {
let m = 0, n = 0;
if (functionalElements) {
m = solutionVariable.value.m;
n = solutionVariable.value.n;
}
else {
m = SellLinAlg.mat_get_row_count(solutionVariable.value);
n = SellLinAlg.mat_get_col_count(solutionVariable.value);
}
input.correct = true;
let feedback = '';
if (input.matrixInput.m != m || input.matrixInput.n != n) {
input.correct = false;
question.allAnswersCorrect = false;
feedback += GET_STR("dimensions_incorrect", this.p.language) + " ";
}
if (input.correct) {
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
let k = i * n + j;
let studentAnswerStr = input.studentAnswer[k].replaceAll(',', '.');
let correct = true;
if (functionalElements) {
correct = solutionVariable.value.elements[k].compareWithStringTerm(studentAnswerStr);
if (solutionVariable.value.state === "syntaxerror") {
input.correct = false;
question.allAnswersCorrect = false;
feedback += GET_STR("feedback_syntaxerror_or_invalid_variables", this.p.language).replace("$", studentAnswerStr) + " ";
break;
}
}
else {
let studentAnswer = 0.0;
try {
studentAnswer = math.evaluate(studentAnswerStr);
}
catch (e) {
input.correct = false;
question.allAnswersCorrect = false;
feedback += GET_STR("feedback_syntaxerror", this.p.language).replace("$", studentAnswerStr) + " ";
break;
}
if (math.abs(studentAnswer - SellLinAlg.mat_get_element_value(solutionVariable.value, i, j)) > solutionVariable.precision) {
input.correct = false;
question.allAnswersCorrect = false;
break;
}
}
if (!correct) {
input.correct = false;
question.allAnswersCorrect = false;
let hint = GET_STR("hint_matrix_element", this.p.language);
hint = hint.replaceAll('$i', '' + (i + 1));
hint = hint.replaceAll('$j', '' + (j + 1));
feedback += hint + " ";
break;
}
}
if (input.correct == false)
break;
}
}
input.evaluationFeedbackStr = input.correct ? checkmark : crossmark;
input.evaluationFeedbackStr += feedback;
}
evaluateProgramming(question, input, solutionVariable) {
let task = {
"type": solutionVariable.value["type"],
"source": input.studentAnswer[0],
"asserts": solutionVariable.value["asserts"],
"hiddenMethods": solutionVariable.value["hiddenMethods"],
"language": this.p.language
};
let service_url = this.p.servicePath + "service-prog.php";
// TODO: should forbid running twice at the same time!!!!!
let feedback_htmlElement = getHtmlChildElementRecursive(question.bodyHtmlElement, input.htmlElementId_feedback);
let wait_text = GET_STR("please_wait", this.p.language, false);
feedback_htmlElement.innerHTML = "<span class=\"text-danger\">" + wait_text + "</span>";
let correct_str = GET_STR("correct", this.p.language, false);
$.ajax({
type: "POST",
url: service_url,
data: {
input: JSON.stringify(task)
},
success: function (data) {
data = JSON.parse(data);
let status = data["status"];
let message = data["msg"];
input.correct = status === "ok";
input.evaluationFeedbackStr = input.correct ? checkmark + correct_str : crossmark;
input.evaluationFeedbackStr += ' <code>' + message.replaceAll("\n", "<br/>").replaceAll(" ", " ") + '</code>';
if (input.correct == false)
question.allAnswersCorrect = false;
input.evaluationInProgress = false;
},
error: function (xhr, status, error) {
console.error(xhr); // TODO: error handling!
}
});
}
}
//# sourceMappingURL=evaluate.js.map