Latest build deployed.

This commit is contained in:
2026-08-01 15:00:14 +00:00
parent e43fb0d0ff
commit 331f133dfe
1278 changed files with 19041 additions and 18362 deletions
+440
View File
@@ -0,0 +1,440 @@
const timeOutHandler = new Object();
const inputPrefix = "stackapi_input_";
const feedbackPrefix = "stackapi_fb_";
const validationPrefix = "stackapi_val_";
const stackstring = {
"teacheranswershow_mcq": "A correct answer is: {$a->display}",
"api_which_typed": "which can be typed as follows",
"api_valid_all_parts": "Please enter valid answers for all parts of the question.",
"api_out_of": "out of",
"api_marks_sub": "Marks for this submission",
"api_submit": "Submit Answers",
"generalfeedback": "General feedback",
"score": "Score",
"api_response": "Response summary",
"api_correct": "Correct answers"
};
function wrap_math(content) {
content = content.replace(/(?<!\\)(\\\(.*?(?<!\\)\\\))/gs, '<span class="process-math">$1</span>');
return content.replace(/(?<!\\)(\\\[.*?(?<!\\)\\\])/gs, '<span class="process-math">$1</span>');
}
async function collectData(qfile, qname, qprefix) {
let res = "";
await getQuestionFile(qfile, qname).then((response) => {
if (response.questionxml != "<quiz>\nnull\n</quiz>") {
res = {
questionDefinition: response.questionxml,
answers: collectAnswer(qprefix),
seed: response.seed,
renderInputs: qprefix + inputPrefix,
readOnly: false
};
}
;
});
return res;
}
function collectAnswer(qprefix) {
const inputs = document.getElementsByTagName("input");
const textareas = document.getElementsByTagName("textarea");
const selects = document.getElementsByTagName("select");
let res = {};
res = processNodes(res, inputs, qprefix);
res = processNodes(res, textareas, qprefix);
res = processNodes(res, selects, qprefix);
return res;
}
function processNodes(res, nodes, qprefix) {
for (let i = 0; i < nodes.length; i++) {
const element = nodes[i];
if (element.name.indexOf(qprefix + inputPrefix) === 0 && element.name.indexOf("_val") === -1) {
if (element.type === "checkbox" || element.type === "radio") {
if (element.checked) {
res[element.name.slice((qprefix + inputPrefix).length)] = element.value;
}
} else {
res[element.name.slice((qprefix + inputPrefix).length)] = element.value;
}
}
}
return res;
}
function send(qfile, qname, qprefix) {
const http = new XMLHttpRequest();
const url = stack_api_url + "/render";
http.open("POST", url, true);
http.setRequestHeader("Content-Type", "application/json");
http.onreadystatechange = function() {
if (http.readyState == 4) {
try {
const json = JSON.parse(http.responseText);
if (json.message) {
console.log(json);
document.getElementById(`${qprefix + "errors"}`).innerText = json.message;
return;
} else {
document.getElementById(`${qprefix + "errors"}`).innerText = "";
}
renameIframeHolders();
let question = json.questionrender;
const inputs = json.questioninputs;
const seed = json.questionseed;
let correctAnswers = "";
for (const [name, input] of Object.entries(inputs)) {
question = question.replace(`[[input:${name}]]`, input.render);
question = question.replace(`[[validation:${name}]]`, `<span name='${qprefix + validationPrefix + name}'></span>`);
question = question.replace(/javascript:download\(([^,]+?),([^,]+?)\)/, `javascript:download($1,$2, '${qfile}', '${qname}', '${qprefix}', ${seed})`);
question = wrap_math(question);
if (input.samplesolutionrender && name !== "remember") {
correctAnswers += `<p>
${stackstring["teacheranswershow_mcq"]} \\[{${input.samplesolutionrender}}\\],
${stackstring["api_which_typed"]}: `;
for (const [name2, solution] of Object.entries(input.samplesolution)) {
if (name2.indexOf("_val") === -1) {
correctAnswers += `<span class='correct-answer'>${wrap_math(solution)}</span>`;
}
}
correctAnswers += ".</p>";
} else if (name !== "remember") {
for (const solution of Object.values(input.samplesolution)) {
if (input.configuration.options) {
correctAnswers += `<p class='correct-answer'>${input.configuration.options[solution]}</p>`;
}
}
}
}
for (const [name, file] of Object.entries(json.questionassets)) {
const plotUrl = getPlotUrl(file);
question = question.replace(name, plotUrl);
json.questionsamplesolutiontext = json.questionsamplesolutiontext.replace(name, plotUrl);
correctAnswers = correctAnswers.replace(name, plotUrl);
}
question = replaceFeedbackTags(question, qprefix);
qoutput = document.getElementById(`${qprefix + "output"}`);
qoutput.innerHTML = question;
document.getElementById(`${qprefix + "stackapi_qtext"}`).style.display = "block";
document.getElementById(`${qprefix + "stackapi_correct"}`).style.display = "block";
for (const inputName of Object.keys(inputs)) {
const inputElements = document.querySelectorAll(`[name^=${qprefix + inputPrefix + inputName}]`);
for (const inputElement of Object.values(inputElements)) {
inputElement.oninput = (event) => {
const currentTimeout = timeOutHandler[event.target.id];
if (currentTimeout) {
window.clearTimeout(currentTimeout);
}
timeOutHandler[event.target.id] = window.setTimeout(validate.bind(null, event.target, qfile, qname, qprefix), 1e3);
};
}
}
let sampleText = json.questionsamplesolutiontext;
if (sampleText) {
sampleText = replaceFeedbackTags(sampleText, qprefix);
document.getElementById(`${qprefix + "generalfeedback"}`).innerHTML = wrap_math(sampleText);
document.getElementById(`${qprefix + "stackapi_generalfeedback"}`).style.display = "block";
} else {
document.getElementById(`${qprefix + "stackapi_generalfeedback"}`).style.display = "none";
}
document.getElementById(`${qprefix + "stackapi_score"}`).style.display = "none";
document.getElementById(`${qprefix + "stackapi_validity"}`).innerText = "";
const innerFeedback = document.getElementById(`${qprefix + "specificfeedback"}`);
innerFeedback.innerHTML = "";
innerFeedback.classList.remove("feedback");
document.getElementById(`${qprefix + "formatcorrectresponse"}`).innerHTML = correctAnswers;
document.getElementById(`${qprefix + "stackapi_generalfeedback"}`).style.display = "none";
document.getElementById(`${qprefix + "stackapi_correct"}`).style.display = "none";
createIframes(json.iframes);
MathJax.typeset();
} catch (e) {
console.log(e);
document.getElementById(`${qprefix + "errors"}`).innerText = http.responseText;
return;
}
}
};
collectData(qfile, qname, qprefix).then((data) => {
let submitbutton = document.getElementById(`${qprefix + "stackapi_qtext"}`).querySelector('input[type="button"]');
submitbutton.addEventListener("click", function() {
answer(qfile, qname, qprefix, data.seed);
});
http.send(JSON.stringify(data));
let questioncontainer = document.getElementById(`${qprefix + "stack"}`).parentElement;
if (questioncontainer.getBoundingClientRect().top < 0) {
questioncontainer.scrollIntoView({ behavior: "smooth", block: "start" });
}
;
});
}
function validate(element, qfile, qname, qprefix) {
const http = new XMLHttpRequest();
const url = stack_api_url + "/validate";
http.open("POST", url, true);
const answerNamePrefixTrim = (qprefix + inputPrefix).length;
const answerName = element.name.slice(answerNamePrefixTrim).split("_", 1)[0];
http.setRequestHeader("Content-Type", "application/json");
http.onreadystatechange = function() {
if (http.readyState == 4) {
try {
const json = JSON.parse(http.responseText);
if (json.message) {
document.getElementById(`${qprefix + "errors"}`).innerText = json.message;
return;
} else {
document.getElementById(`${qprefix + "errors"}`).innerText = "";
}
renameIframeHolders();
const validationHTML = json.validation;
const element2 = document.getElementsByName(`${qprefix + validationPrefix + answerName}`)[0];
element2.innerHTML = wrap_math(validationHTML);
if (validationHTML) {
element2.classList.add("validation");
} else {
element2.classList.remove("validation");
}
createIframes(json.iframes);
MathJax.typeset();
} catch (e) {
document.getElementById(`${qprefix + "errors"}`).innerText = http.responseText;
return;
}
}
};
collectData(qfile, qname, qprefix).then((data) => {
data.inputName = answerName;
http.send(JSON.stringify(data));
});
}
function answer(qfile, qname, qprefix, seed) {
const http = new XMLHttpRequest();
const url = stack_api_url + "/grade";
http.open("POST", url, true);
if (!document.getElementById(`${qprefix + "output"}`).innerText) {
return;
}
http.setRequestHeader("Content-Type", "application/json");
http.onreadystatechange = function() {
if (http.readyState == 4) {
try {
const json = JSON.parse(http.responseText);
if (json.message) {
document.getElementById(`${qprefix + "errors"}`).innerText = json.message;
return;
} else {
document.getElementById(`${qprefix + "errors"}`).innerText = "";
}
if (!json.isgradable) {
document.getElementById(`${qprefix + "stackapi_validity"}`).innerText = " " + stackstring["api_valid_all_parts"];
return;
}
renameIframeHolders();
document.getElementById(`${qprefix + "score"}`).innerText = (json.score * json.scoreweights.total).toFixed(2) + " " + stackstring["api_out_of"] + " " + json.scoreweights.total;
document.getElementById(`${qprefix + "stackapi_score"}`).style.display = "block";
document.getElementById(`${qprefix + "response_summary"}`).innerText = json.responsesummary;
document.getElementById(`${qprefix + "stackapi_generalfeedback"}`).style.display = "block";
document.getElementById(`${qprefix + "stackapi_summary"}`).style.display = "none";
const feedback = json.prts;
const specificFeedbackElement2 = document.getElementById(`${qprefix + "specificfeedback"}`);
if (json.specificfeedback) {
for (const [name, file] of Object.entries(json.gradingassets)) {
json.specificfeedback = json.specificfeedback.replace(name, getPlotUrl(file));
}
json.specificfeedback = replaceFeedbackTags(json.specificfeedback, qprefix);
specificFeedbackElement2.innerHTML = wrap_math(json.specificfeedback);
specificFeedbackElement2.classList.add("feedback");
} else {
specificFeedbackElement2.classList.remove("feedback");
}
for (let [name, fb] of Object.entries(feedback)) {
for (const [name2, file] of Object.entries(json.gradingassets)) {
fb = fb.replace(name2, getPlotUrl(file));
}
const elements = document.getElementsByName(`${qprefix + feedbackPrefix + name}`);
if (elements.length > 0) {
const element = elements[0];
if (json.scores[name] !== void 0) {
fb = fb + `<div>${stackstring["api_marks_sub"]}:
${(json.scores[name] * json.scoreweights[name] * json.scoreweights.total).toFixed(2)}
/ ${(json.scoreweights[name] * json.scoreweights.total).toFixed(2)}.</div>`;
}
element.innerHTML = wrap_math(fb);
}
}
createIframes(json.iframes);
MathJax.typeset();
} catch (e) {
console.log(e);
document.getElementById(`${qprefix + "errors"}`).innerText = http.responseText;
return;
}
}
};
const specificFeedbackElement = document.getElementById(`${qprefix + "specificfeedback"}`);
specificFeedbackElement.innerHTML = "";
specificFeedbackElement.classList.remove("feedback");
document.getElementById(`${qprefix + "response_summary"}`).innerText = "";
document.getElementById(`${qprefix + "stackapi_summary"}`).style.display = "none";
const inputElements = document.querySelectorAll(`[name^=${qprefix + feedbackPrefix}]`);
for (const inputElement of Object.values(inputElements)) {
inputElement.innerHTML = "";
inputElement.classList.remove("feedback");
}
document.getElementById(`${qprefix + "stackapi_score"}`).style.display = "none";
document.getElementById(`${qprefix + "stackapi_validity"}`).innerText = "";
collectData(qfile, qname, qprefix).then((data) => {
data.seed = seed;
http.send(JSON.stringify(data));
});
}
function download(filename, fileid, qfile, qname, qprefix, seed) {
const http = new XMLHttpRequest();
const url = stack_api_url + "/download";
http.open("POST", url, true);
http.setRequestHeader("Content-Type", "application/json");
http.filename = filename;
http.fileid = fileid;
http.onreadystatechange = function() {
if (http.readyState == 4) {
try {
const blob = new Blob([http.responseText], { type: "application/octet-binary", endings: "native" });
const selector = CSS.escape(`javascript:download('${http.filename}', ${http.fileid}, '${qfile}', '${qname}', '${qprefix}', ${seed})`);
const linkElements = document.querySelectorAll(`a[href^=${selector}]`);
const link = linkElements[0];
link.setAttribute("href", URL.createObjectURL(blob));
link.setAttribute("download", filename);
link.click();
} catch (e) {
document.getElementById("errors").innerText = http.responseText;
return;
}
}
};
collectData(qfile, qname, qprefix).then((data) => {
data.filename = filename;
data.fileid = fileid;
data.seed = seed;
http.send(JSON.stringify(data));
});
}
function saveState(key, value) {
if (typeof Storage !== "undefined") {
localStorage.setItem(key, value);
}
}
function loadState(key) {
if (typeof Storage !== "undefined") {
return localStorage.getItem(key) || "";
}
return "";
}
function renameIframeHolders() {
for (const iframe of document.querySelectorAll(`[id^=stack-iframe-holder]:not([id$=old]`)) {
iframe.id = iframe.id + "_old";
}
}
function createIframes(iframes) {
const corsFragment = "/cors.php?name=";
for (const iframe of iframes) {
create_iframe(
iframe[0],
iframe[1].replaceAll(corsFragment, stack_api_url + corsFragment),
...iframe.slice(2)
);
}
}
function replaceFeedbackTags(text, qprefix) {
let result = text;
const feedbackTags = text.match(/\[\[feedback:.*?\]\]/g);
if (feedbackTags) {
for (const tag of feedbackTags) {
result = result.replace(tag, `<div name='${qprefix + feedbackPrefix + tag.slice(11, -2)}'></div>`);
}
}
return result;
}
async function getQuestionFile(questionURL, questionName) {
let res = "";
if (questionURL) {
await fetch(questionURL).then((result) => result.text()).then((result) => {
res = loadQuestionFromFile(result, questionName);
});
}
return res;
}
function loadQuestionFromFile(fileContents, questionName) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(fileContents, "text/xml");
let thequestion = null;
let randSeed = "";
for (const question of xmlDoc.getElementsByTagName("question")) {
if (question.getAttribute("type").toLowerCase() === "stack" && (!questionName || question.querySelectorAll("name text")[0].textContent === questionName)) {
thequestion = question.outerHTML;
let seeds = question.querySelectorAll("deployedseed");
if (seeds.length) {
randSeed = parseInt(seeds[Math.floor(Math.random() * seeds.length)].textContent);
}
break;
}
}
return { questionxml: setQuestion(thequestion), seed: randSeed };
}
function setQuestion(question) {
return "<quiz>\n" + question + "\n</quiz>";
}
function createQuestionBlocks() {
questionBlocks = document.getElementsByClassName("que stack");
let i = 0;
for (questionblock of questionBlocks) {
i++;
let questionPrefix = "q" + i.toString() + "_";
var qfile = questionblock.dataset.qfile;
var qname = questionblock.dataset.qname || "";
questionblock.innerHTML = `
<div class="collapsiblecontent" id=${questionPrefix + "stack"}>
<div class="vstack gap-3 ms-3 col-lg-8">
<div id=${questionPrefix + "errors"}></div>
<div id=${questionPrefix + "stackapi_qtext"} class="col-lg-8" style="display: none">
<!--<h2>${stackstring["questiontext"]}:</h2>-->
<div id=${questionPrefix + "output"} class="formulation"></div>
<div id=${questionPrefix + "specificfeedback"}></div>
<br>
<!-- <input type="button" onclick="answer('${qfile}', '${qname}', '${questionPrefix}')" class="btn btn-primary" value=${stackstring["api_submit"]}/>-->
<input type="button" class="btn btn-primary" value=${stackstring["api_submit"]}/>
<span id=${questionPrefix + "stackapi_validity"} style="color:darkred"></span>
</div>
<div id=${questionPrefix + "stackapi_generalfeedback"} class="col-lg-8" style="display: none">
<h2>${stackstring["generalfeedback"]}:</h2>
<div id=${questionPrefix + "generalfeedback"} class="feedback"></div>
</div>
<h2 id=${questionPrefix + "stackapi_score"} style="display: none">${stackstring["score"]}: <span id=${questionPrefix + "score"}></span></h2>
<div id=${questionPrefix + "stackapi_summary"} class="col-lg-10" style="display: none">
<h2>${stackstring["api_response"]}:</h2>
<div id=${questionPrefix + "response_summary"} class="feedback"></div>
</div>
<div id=${questionPrefix + "stackapi_correct"} class="col-lg-10" style="display: none">
<h2>${stackstring["api_correct"]}:</h2>
<div id=${questionPrefix + "formatcorrectresponse"} class="feedback"></div>
</div>
</div>
<div id=${questionPrefix + "newquestionbutton"}>
<input type="button" onclick="send('${qfile}', '${qname}', '${questionPrefix}')" class="btn btn-primary" value="Show new example question"/>
</div>
</div>
`;
}
}
function addCollapsibles() {
var collapsibles = document.querySelectorAll(".level2>h2, .stack>h2");
for (let i = 0; i < collapsibles.length; i++) {
collapsibles[i].addEventListener("click", () => collapseFunc(this));
}
}
function collapseFunc(e) {
e.classList.toggle("collapsed");
}
function stackSetup() {
createQuestionBlocks();
addCollapsibles();
}
function getPlotUrl(file) {
return `${stack_api_url}/plots/${file}`;
}
//# sourceMappingURL=stackapicalls.js.map