{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Test and demonstration notebook for Javascript quiz tool\n", "\n", "This notebook tests and demonstrates the jupyterquiz library and its use. It draws from a set of example questions that show the supported capabilities of the jupyterquiz library.\n", "\n", "In particular, this notebook also serves to test different ways for the library to load questions:\n", "1. **From a URL:** This is the preferred approach. It will embed a copy of the questions in your Jupyter notebook (and Jupyter book), but it will also try to load the latest version of the questions from the URL via Javascript at page load time. If the questions cannot be loaded from the URL, the stored versions will be used as a fallback.\n", "2. **From a JSON File:** This may be more convenient for local testing.\n", "3. **From a Python Dict:** This is probably the best path for testing new questions because they can be added and tested within a Jupyter notebook.\n", "\n", "jupyterquiz supports drawing a random subset of questions, and that is also tested.\n", "\n", "$$ \\begin{bmatrix} 7 \\end{bmatrix}$$" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "tags": [ "remove-input" ] }, "outputs": [], "source": [ "from jupyterquiz import display_quiz\n", "\n", "git_path=\"https://raw.githubusercontent.com/jmshea/jupyterquiz/main/examples/\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Test all question types, standard URL loading method:\n", "\n", "$~$" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionsqSESggBHRLRa=[\n", " {\n", " \"question\": \"Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Testing parameter to change number of colums for answers: Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answer_cols\": 4,\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these are used to create formatted text in Jupyter notebooks?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Wiki markup\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"SVG\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"Markdown\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Rich Text\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"precision\": 3,\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Determine the output of the following Python code:\",\n", " \"code\": \"a=\\\"1\\\"\\nb=\\\"2\\\"\\nprint(a+b)\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"1\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"2\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"12\",\n", " \"correct\": true,\n", " \"feedback\": \"Yes. The + operator will concatenate the strings \\\"1\\\" and \\\"2\\\".\"\n", " },\n", " {\n", " \"answer\": \"error\",\n", " \"correct\": false,\n", " \"feedback\": \"No. The + operator for strings performs string concatenation.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"The variable mylist is a Python list. Choose which code snippet will append the item 3 to mylist.\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"code\": \"mylist+=3\",\n", " \"correct\": false\n", " },\n", " {\n", " \"code\": \"mylist+=[3]\",\n", " \"correct\": true\n", " },\n", " {\n", " \"code\": \"mylist+={3}\",\n", " \"correct\": false\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these is the ratio of a circle's circumference to its diameter?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"$\\\\pi$\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\frac{22}{7}$\",\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\frac{22}{7}$ is only an approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"This is a crude approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\tau$\",\n", " \"correct\": false,\n", " \"feedback\": \"True for the ratio of the circle's circumference to its radius, not diameter.\"\n", " }\n", " ]\n", " }\n", "]\n", ";\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
  1. Copy the text in this cell below \"Answer String\"
  2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
  3. Select the whole \"Replace Me\" text
  4. Paste in your answer string and press shift-Enter.
  5. Save the notebook using the save icon or File->Save Notebook menu item



  6. Answer String:
    ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
    \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Attempt to fetch questions JSON, with timeout and fallback to embedded data.\n", " */\n", "{\n", " const controller = new AbortController();\n", " const signal = controller.signal;\n", " // Abort fetch after 5 seconds\n", " setTimeout(() => controller.abort(), 5000);\n", " fetch(\"https://raw.githubusercontent.com/jmshea/jupyterquiz/main/examples/questions.json\", { signal })\n", " .then(response => response.json())\n", " .then(json => show_questions(json, qSESggBHRLRa))\n", " .catch(err => {\n", " console.log(\"Fetch error or timeout\", err);\n", " show_questions(questionsqSESggBHRLRa, qSESggBHRLRa);\n", " });\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display_quiz(git_path+\"questions.json\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using the 'fdsp' color scheme:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
    " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionsBzwAnYrJjhAg=[\n", " {\n", " \"question\": \"Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Testing parameter to change number of colums for answers: Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answer_cols\": 4,\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these are used to create formatted text in Jupyter notebooks?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Wiki markup\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"SVG\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"Markdown\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Rich Text\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"precision\": 3,\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Determine the output of the following Python code:\",\n", " \"code\": \"a=\\\"1\\\"\\nb=\\\"2\\\"\\nprint(a+b)\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"1\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"2\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"12\",\n", " \"correct\": true,\n", " \"feedback\": \"Yes. The + operator will concatenate the strings \\\"1\\\" and \\\"2\\\".\"\n", " },\n", " {\n", " \"answer\": \"error\",\n", " \"correct\": false,\n", " \"feedback\": \"No. The + operator for strings performs string concatenation.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"The variable mylist is a Python list. Choose which code snippet will append the item 3 to mylist.\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"code\": \"mylist+=3\",\n", " \"correct\": false\n", " },\n", " {\n", " \"code\": \"mylist+=[3]\",\n", " \"correct\": true\n", " },\n", " {\n", " \"code\": \"mylist+={3}\",\n", " \"correct\": false\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these is the ratio of a circle's circumference to its diameter?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"$\\\\pi$\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\frac{22}{7}$\",\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\frac{22}{7}$ is only an approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"This is a crude approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\tau$\",\n", " \"correct\": false,\n", " \"feedback\": \"True for the ratio of the circle's circumference to its radius, not diameter.\"\n", " }\n", " ]\n", " }\n", "]\n", ";\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
    1. Copy the text in this cell below \"Answer String\"
    2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
    3. Select the whole \"Replace Me\" text
    4. Paste in your answer string and press shift-Enter.
    5. Save the notebook using the save icon or File->Save Notebook menu item



    6. Answer String:
      ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
      \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Attempt to fetch questions JSON, with timeout and fallback to embedded data.\n", " */\n", "{\n", " const controller = new AbortController();\n", " const signal = controller.signal;\n", " // Abort fetch after 5 seconds\n", " setTimeout(() => controller.abort(), 5000);\n", " fetch(\"https://raw.githubusercontent.com/jmshea/jupyterquiz/main/examples/questions.json\", { signal })\n", " .then(response => response.json())\n", " .then(json => show_questions(json, BzwAnYrJjhAg))\n", " .catch(err => {\n", " console.log(\"Fetch error or timeout\", err);\n", " show_questions(questionsBzwAnYrJjhAg, BzwAnYrJjhAg);\n", " });\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display_quiz(git_path+\"questions.json\", colors='fdsp')" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "# Test loading all questions from file and customizing individual colors:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
      " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionsiVJYtPUAkybb=[\n", " {\n", " \"question\": \"Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Testing parameter to change number of colums for answers: Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answer_cols\": 4,\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these are used to create formatted text in Jupyter notebooks?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Wiki markup\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"SVG\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"Markdown\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Rich Text\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"precision\": 3,\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Determine the output of the following Python code:\",\n", " \"code\": \"a=\\\"1\\\"\\nb=\\\"2\\\"\\nprint(a+b)\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"1\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"2\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"12\",\n", " \"correct\": true,\n", " \"feedback\": \"Yes. The + operator will concatenate the strings \\\"1\\\" and \\\"2\\\".\"\n", " },\n", " {\n", " \"answer\": \"error\",\n", " \"correct\": false,\n", " \"feedback\": \"No. The + operator for strings performs string concatenation.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"The variable mylist is a Python list. Choose which code snippet will append the item 3 to mylist.\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"code\": \"mylist+=3\",\n", " \"correct\": false\n", " },\n", " {\n", " \"code\": \"mylist+=[3]\",\n", " \"correct\": true\n", " },\n", " {\n", " \"code\": \"mylist+={3}\",\n", " \"correct\": false\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these is the ratio of a circle's circumference to its diameter?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"$\\\\pi$\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\frac{22}{7}$\",\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\frac{22}{7}$ is only an approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"This is a crude approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\tau$\",\n", " \"correct\": false,\n", " \"feedback\": \"True for the ratio of the circle's circumference to its radius, not diameter.\"\n", " }\n", " ]\n", " }\n", "]\n", ";\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
      1. Copy the text in this cell below \"Answer String\"
      2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
      3. Select the whole \"Replace Me\" text
      4. Paste in your answer string and press shift-Enter.
      5. Save the notebook using the save icon or File->Save Notebook menu item



      6. Answer String:
        ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
        \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Handle asynchrony issues when re-running quizzes in Jupyter notebooks.\n", " * Ensures show_questions is called after the container div is in the DOM.\n", " */\n", "function try_show() {\n", " if (document.getElementById(\"iVJYtPUAkybb\")) {\n", " show_questions(questionsiVJYtPUAkybb, iVJYtPUAkybb);\n", " } else {\n", " setTimeout(try_show, 200);\n", " }\n", "};\n", "// Invoke immediately\n", "{\n", " try_show();\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display_quiz(\"examples/questions.json\", \n", " )" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
        " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionsdxWZHAiMWRyt=[\n", " {\n", " \"question\": \"Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Testing parameter to change number of colums for answers: Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answer_cols\": 4,\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these are used to create formatted text in Jupyter notebooks?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Wiki markup\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"SVG\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"Markdown\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Rich Text\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"precision\": 3,\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Determine the output of the following Python code:\",\n", " \"code\": \"a=\\\"1\\\"\\nb=\\\"2\\\"\\nprint(a+b)\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"1\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"2\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"12\",\n", " \"correct\": true,\n", " \"feedback\": \"Yes. The + operator will concatenate the strings \\\"1\\\" and \\\"2\\\".\"\n", " },\n", " {\n", " \"answer\": \"error\",\n", " \"correct\": false,\n", " \"feedback\": \"No. The + operator for strings performs string concatenation.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"The variable mylist is a Python list. Choose which code snippet will append the item 3 to mylist.\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"code\": \"mylist+=3\",\n", " \"correct\": false\n", " },\n", " {\n", " \"code\": \"mylist+=[3]\",\n", " \"correct\": true\n", " },\n", " {\n", " \"code\": \"mylist+={3}\",\n", " \"correct\": false\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these is the ratio of a circle's circumference to its diameter?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"$\\\\pi$\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\frac{22}{7}$\",\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\frac{22}{7}$ is only an approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"This is a crude approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\tau$\",\n", " \"correct\": false,\n", " \"feedback\": \"True for the ratio of the circle's circumference to its radius, not diameter.\"\n", " }\n", " ]\n", " }\n", "]\n", ";\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
        1. Copy the text in this cell below \"Answer String\"
        2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
        3. Select the whole \"Replace Me\" text
        4. Paste in your answer string and press shift-Enter.
        5. Save the notebook using the save icon or File->Save Notebook menu item



        6. Answer String:
          ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
          \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Handle asynchrony issues when re-running quizzes in Jupyter notebooks.\n", " * Ensures show_questions is called after the container div is in the DOM.\n", " */\n", "function try_show() {\n", " if (document.getElementById(\"dxWZHAiMWRyt\")) {\n", " show_questions(questionsdxWZHAiMWRyt, dxWZHAiMWRyt);\n", " } else {\n", " setTimeout(try_show, 200);\n", " }\n", "};\n", "// Invoke immediately\n", "{\n", " try_show();\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display_quiz(\"examples/questions.json\", \n", " colors={\n", " '--jq-many-choice-bg': '#224dea',\n", " '--jq-multiple-choice-bg': '#a45995'\n", " }\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Test loading questions from dict (this is for more rapid question generation and testing) and changing border radius for \"flat\" look:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
          " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionsBgAoOlFVKPoB=[{\"question\": \"Choose all of the following that can be included in Jupyter notebooks?\", \"type\": \"many_choice\", \"answers\": [{\"answer\": \"Text and graphics output from Python\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Typeset mathematics\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Python executable code\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Formatted text\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Live snakes via Python\", \"correct\": false, \"feedback\": \"I hope not.\"}]}, {\"question\": \"Testing parameter to change number of colums for answers: Choose all of the following that can be included in Jupyter notebooks?\", \"type\": \"many_choice\", \"answer_cols\": 4, \"answers\": [{\"answer\": \"Text and graphics output from Python\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Typeset mathematics\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Python executable code\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Formatted text\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Live snakes via Python\", \"correct\": false, \"feedback\": \"I hope not.\"}]}, {\"question\": \"Which of these are used to create formatted text in Jupyter notebooks?\", \"type\": \"multiple_choice\", \"answers\": [{\"answer\": \"Wiki markup\", \"correct\": false, \"feedback\": \"False.\"}, {\"answer\": \"SVG\", \"correct\": false, \"feedback\": \"False.\"}, {\"answer\": \"Markdown\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Rich Text\", \"correct\": false, \"feedback\": \"False.\"}]}, {\"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\", \"type\": \"numeric\", \"answers\": [{\"type\": \"value\", \"value\": 3.14, \"correct\": true, \"feedback\": \"Correct.\"}, {\"type\": \"range\", \"range\": [3.142857, 3.142858], \"correct\": true, \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"}, {\"type\": \"range\", \"range\": [-100000000, 0], \"correct\": false, \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"}, {\"type\": \"default\", \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"}]}, {\"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\", \"type\": \"numeric\", \"precision\": 3, \"answers\": [{\"type\": \"value\", \"value\": 3.14, \"correct\": true, \"feedback\": \"Correct.\"}, {\"type\": \"range\", \"range\": [3.142857, 3.142858], \"correct\": true, \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"}, {\"type\": \"range\", \"range\": [-100000000, 0], \"correct\": false, \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"}, {\"type\": \"default\", \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"}]}, {\"question\": \"Determine the output of the following Python code:\", \"code\": \"a=\\\"1\\\"\\nb=\\\"2\\\"\\nprint(a+b)\", \"type\": \"multiple_choice\", \"answers\": [{\"answer\": \"1\", \"correct\": false, \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"}, {\"answer\": \"2\", \"correct\": false, \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"}, {\"answer\": \"3\", \"correct\": false, \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"}, {\"answer\": \"12\", \"correct\": true, \"feedback\": \"Yes. The + operator will concatenate the strings \\\"1\\\" and \\\"2\\\".\"}, {\"answer\": \"error\", \"correct\": false, \"feedback\": \"No. The + operator for strings performs string concatenation.\"}]}, {\"question\": \"The variable mylist is a Python list. Choose which code snippet will append the item 3 to mylist.\", \"type\": \"multiple_choice\", \"answers\": [{\"code\": \"mylist+=3\", \"correct\": false}, {\"code\": \"mylist+=[3]\", \"correct\": true}, {\"code\": \"mylist+={3}\", \"correct\": false}]}, {\"question\": \"Which of these is the ratio of a circle's circumference to its diameter?\", \"type\": \"multiple_choice\", \"answers\": [{\"answer\": \"$\\\\pi$\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"$\\\\frac{22}{7}$\", \"correct\": false, \"feedback\": \"$\\\\frac{22}{7}$ is only an approximation to the true value.\"}, {\"answer\": \"3\", \"correct\": false, \"feedback\": \"This is a crude approximation to the true value.\"}, {\"answer\": \"$\\\\tau$\", \"correct\": false, \"feedback\": \"True for the ratio of the circle's circumference to its radius, not diameter.\"}]}];\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
          1. Copy the text in this cell below \"Answer String\"
          2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
          3. Select the whole \"Replace Me\" text
          4. Paste in your answer string and press shift-Enter.
          5. Save the notebook using the save icon or File->Save Notebook menu item



          6. Answer String:
            ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
            \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Handle asynchrony issues when re-running quizzes in Jupyter notebooks.\n", " * Ensures show_questions is called after the container div is in the DOM.\n", " */\n", "function try_show() {\n", " if (document.getElementById(\"BgAoOlFVKPoB\")) {\n", " show_questions(questionsBgAoOlFVKPoB, BgAoOlFVKPoB);\n", " } else {\n", " setTimeout(try_show, 200);\n", " }\n", "};\n", "// Invoke immediately\n", "{\n", " try_show();\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import json\n", "with open(\"examples/questions.json\", \"r\") as file:\n", " questions=json.load(file)\n", " \n", "display_quiz(questions, border_radius=0)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "# #Leave this here for when doing question development\n", "\n", "# import json\n", "# with open(\"examples/questions.json\", \"w\") as file:\n", "# json.dump(questions, file, indent=4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Test sampling from questions:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
            " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionswULiNGaQSdiU=[\n", " {\n", " \"question\": \"Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Testing parameter to change number of colums for answers: Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answer_cols\": 4,\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these are used to create formatted text in Jupyter notebooks?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Wiki markup\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"SVG\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"Markdown\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Rich Text\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"precision\": 3,\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Determine the output of the following Python code:\",\n", " \"code\": \"a=\\\"1\\\"\\nb=\\\"2\\\"\\nprint(a+b)\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"1\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"2\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"12\",\n", " \"correct\": true,\n", " \"feedback\": \"Yes. The + operator will concatenate the strings \\\"1\\\" and \\\"2\\\".\"\n", " },\n", " {\n", " \"answer\": \"error\",\n", " \"correct\": false,\n", " \"feedback\": \"No. The + operator for strings performs string concatenation.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"The variable mylist is a Python list. Choose which code snippet will append the item 3 to mylist.\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"code\": \"mylist+=3\",\n", " \"correct\": false\n", " },\n", " {\n", " \"code\": \"mylist+=[3]\",\n", " \"correct\": true\n", " },\n", " {\n", " \"code\": \"mylist+={3}\",\n", " \"correct\": false\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these is the ratio of a circle's circumference to its diameter?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"$\\\\pi$\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\frac{22}{7}$\",\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\frac{22}{7}$ is only an approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"This is a crude approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\tau$\",\n", " \"correct\": false,\n", " \"feedback\": \"True for the ratio of the circle's circumference to its radius, not diameter.\"\n", " }\n", " ]\n", " }\n", "]\n", ";\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
            1. Copy the text in this cell below \"Answer String\"
            2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
            3. Select the whole \"Replace Me\" text
            4. Paste in your answer string and press shift-Enter.
            5. Save the notebook using the save icon or File->Save Notebook menu item



            6. Answer String:
              ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
              \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Attempt to fetch questions JSON, with timeout and fallback to embedded data.\n", " */\n", "{\n", " const controller = new AbortController();\n", " const signal = controller.signal;\n", " // Abort fetch after 5 seconds\n", " setTimeout(() => controller.abort(), 5000);\n", " fetch(\"https://raw.githubusercontent.com/jmshea/jupyterquiz/main/examples/questions.json\", { signal })\n", " .then(response => response.json())\n", " .then(json => show_questions(json, wULiNGaQSdiU))\n", " .catch(err => {\n", " console.log(\"Fetch error or timeout\", err);\n", " show_questions(questionswULiNGaQSdiU, wULiNGaQSdiU);\n", " });\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display_quiz(git_path+\"questions.json\",2)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "github_preview=[questions[0]]+[questions[2]]" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
              " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionsJMqfrjPPyYuj=[{\"question\": \"Choose all of the following that can be included in Jupyter notebooks?\", \"type\": \"many_choice\", \"answers\": [{\"answer\": \"Text and graphics output from Python\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Typeset mathematics\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Python executable code\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Formatted text\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Live snakes via Python\", \"correct\": false, \"feedback\": \"I hope not.\"}]}, {\"question\": \"Which of these are used to create formatted text in Jupyter notebooks?\", \"type\": \"multiple_choice\", \"answers\": [{\"answer\": \"Wiki markup\", \"correct\": false, \"feedback\": \"False.\"}, {\"answer\": \"SVG\", \"correct\": false, \"feedback\": \"False.\"}, {\"answer\": \"Markdown\", \"correct\": true, \"feedback\": \"Correct.\"}, {\"answer\": \"Rich Text\", \"correct\": false, \"feedback\": \"False.\"}]}];\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
              1. Copy the text in this cell below \"Answer String\"
              2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
              3. Select the whole \"Replace Me\" text
              4. Paste in your answer string and press shift-Enter.
              5. Save the notebook using the save icon or File->Save Notebook menu item



              6. Answer String:
                ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
                \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Handle asynchrony issues when re-running quizzes in Jupyter notebooks.\n", " * Ensures show_questions is called after the container div is in the DOM.\n", " */\n", "function try_show() {\n", " if (document.getElementById(\"JMqfrjPPyYuj\")) {\n", " show_questions(questionsJMqfrjPPyYuj, JMqfrjPPyYuj);\n", " } else {\n", " setTimeout(try_show, 200);\n", " }\n", "};\n", "// Invoke immediately\n", "{\n", " try_show();\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display_quiz(github_preview)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Test custom border radius, question alignment, max_width" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
                " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionsQUMwtImzEpUU=[\n", " {\n", " \"question\": \"Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Testing parameter to change number of colums for answers: Choose all of the following that can be included in Jupyter notebooks?\",\n", " \"type\": \"many_choice\",\n", " \"answer_cols\": 4,\n", " \"answers\": [\n", " {\n", " \"answer\": \"Text and graphics output from Python\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Typeset mathematics\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Python executable code\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Formatted text\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Live snakes via Python\",\n", " \"correct\": false,\n", " \"feedback\": \"I hope not.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these are used to create formatted text in Jupyter notebooks?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"Wiki markup\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"SVG\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " },\n", " {\n", " \"answer\": \"Markdown\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"Rich Text\",\n", " \"correct\": false,\n", " \"feedback\": \"False.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Enter the value of $\\\\pi$ to 2 decimal places.\",\n", " \"type\": \"numeric\",\n", " \"precision\": 3,\n", " \"answers\": [\n", " {\n", " \"type\": \"value\",\n", " \"value\": 3.14,\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " 3.142857,\n", " 3.142858\n", " ],\n", " \"correct\": true,\n", " \"feedback\": \"True to 2 decimal places, but you know $\\\\pi$ is not really 22/7, right?\"\n", " },\n", " {\n", " \"type\": \"range\",\n", " \"range\": [\n", " -100000000,\n", " 0\n", " ],\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\pi$ is the AREA of a circle of radius 1. Try again.\"\n", " },\n", " {\n", " \"type\": \"default\",\n", " \"feedback\": \"$\\\\pi$ is the area of a circle of radius 1. Try again.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Determine the output of the following Python code:\",\n", " \"code\": \"a=\\\"1\\\"\\nb=\\\"2\\\"\\nprint(a+b)\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"1\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"2\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"No. When strings are operated on by +, they are concatenated.\"\n", " },\n", " {\n", " \"answer\": \"12\",\n", " \"correct\": true,\n", " \"feedback\": \"Yes. The + operator will concatenate the strings \\\"1\\\" and \\\"2\\\".\"\n", " },\n", " {\n", " \"answer\": \"error\",\n", " \"correct\": false,\n", " \"feedback\": \"No. The + operator for strings performs string concatenation.\"\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"The variable mylist is a Python list. Choose which code snippet will append the item 3 to mylist.\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"code\": \"mylist+=3\",\n", " \"correct\": false\n", " },\n", " {\n", " \"code\": \"mylist+=[3]\",\n", " \"correct\": true\n", " },\n", " {\n", " \"code\": \"mylist+={3}\",\n", " \"correct\": false\n", " }\n", " ]\n", " },\n", " {\n", " \"question\": \"Which of these is the ratio of a circle's circumference to its diameter?\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"$\\\\pi$\",\n", " \"correct\": true,\n", " \"feedback\": \"Correct.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\frac{22}{7}$\",\n", " \"correct\": false,\n", " \"feedback\": \"$\\\\frac{22}{7}$ is only an approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"3\",\n", " \"correct\": false,\n", " \"feedback\": \"This is a crude approximation to the true value.\"\n", " },\n", " {\n", " \"answer\": \"$\\\\tau$\",\n", " \"correct\": false,\n", " \"feedback\": \"True for the ratio of the circle's circumference to its radius, not diameter.\"\n", " }\n", " ]\n", " }\n", "]\n", ";\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
                1. Copy the text in this cell below \"Answer String\"
                2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
                3. Select the whole \"Replace Me\" text
                4. Paste in your answer string and press shift-Enter.
                5. Save the notebook using the save icon or File->Save Notebook menu item



                6. Answer String:
                  ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
                  \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Attempt to fetch questions JSON, with timeout and fallback to embedded data.\n", " */\n", "{\n", " const controller = new AbortController();\n", " const signal = controller.signal;\n", " // Abort fetch after 5 seconds\n", " setTimeout(() => controller.abort(), 5000);\n", " fetch(\"https://raw.githubusercontent.com/jmshea/jupyterquiz/main/examples/questions.json\", { signal })\n", " .then(response => response.json())\n", " .then(json => show_questions(json, QUMwtImzEpUU))\n", " .catch(err => {\n", " console.log(\"Fetch error or timeout\", err);\n", " show_questions(questionsQUMwtImzEpUU, QUMwtImzEpUU);\n", " });\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display_quiz(git_path+\"questions.json\", border_radius=0, question_alignment='center', max_width=1000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Test URL support" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "example_link = [{\n", " \"question\": \"The variable mylist is a Python list. \" + \\\n", " \"Choose which code snippet will append the item 3 to mylist. \" +\n", " \"See [W3Schools: Python Join Two Lists]\" + \\\n", " \"(https://www.w3schools.com/python/gloss_python_join_lists.asp) for reference.\",\n", " \"type\": \"multiple_choice\",\n", " \"answers\": [\n", " {\n", " \"code\": \"mylist+=3\",\n", " \"correct\": False\n", " },\n", " {\n", " \"code\": \"mylist+=[3]\",\n", " \"correct\": True\n", " },\n", " {\n", " \"code\": \"mylist+={3}\",\n", " \"correct\": False\n", " }\n", " ]\n", " }]\n" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
                  " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionsyBkvUMkWmVaI=[{\"question\": \"The variable mylist is a Python list. Choose which code snippet will append the item 3 to mylist. See [W3Schools: Python Join Two Lists](https://www.w3schools.com/python/gloss_python_join_lists.asp) for reference.\", \"type\": \"multiple_choice\", \"answers\": [{\"code\": \"mylist+=3\", \"correct\": false}, {\"code\": \"mylist+=[3]\", \"correct\": true}, {\"code\": \"mylist+={3}\", \"correct\": false}]}];\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
                  1. Copy the text in this cell below \"Answer String\"
                  2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
                  3. Select the whole \"Replace Me\" text
                  4. Paste in your answer string and press shift-Enter.
                  5. Save the notebook using the save icon or File->Save Notebook menu item



                  6. Answer String:
                    ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
                    \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Handle asynchrony issues when re-running quizzes in Jupyter notebooks.\n", " * Ensures show_questions is called after the container div is in the DOM.\n", " */\n", "function try_show() {\n", " if (document.getElementById(\"yBkvUMkWmVaI\")) {\n", " show_questions(questionsyBkvUMkWmVaI, yBkvUMkWmVaI);\n", " } else {\n", " setTimeout(try_show, 200);\n", " }\n", "};\n", "// Invoke immediately\n", "{\n", " try_show();\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display_quiz(example_link)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# String Test" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "example_string = [{\n", " \"question\": \"Who was the 35th president (1961-63) of the US?\" ,\n", " \"type\": \"string\",\n", " \"answers\": [\n", " {\n", " \"answer\": \"John F. Kennedy\",\n", " \"correct\": True,\n", " \"feedback\": \"Correct. John F. Kennedy was the 35th president of the U.S.\",\n", " \"match_case\": False,\n", " \"fuzzy_threshold\": 0.78\n", " },\n", " {\n", " \"answer\": \"JFK\",\n", " \"correct\": True,\n", " \"feedback\": \"Correct. John F. Kennedy was the 35 preside of the U.S.\"\n", " },\n", " {\n", " \"answer\": \"Kennedy\",\n", " \"correct\": False,\n", " \"feedback\": \"Please also provide the first name.\",\n", " \"match_case\": False\n", " }, \n", " ]\n", " }]\n" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
                    " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "var questionsaRCrRhfLxyVv=[{\"question\": \"Who was the 35th president (1961-63) of the US?\", \"type\": \"string\", \"answers\": [{\"answer\": \"John F. Kennedy\", \"correct\": true, \"feedback\": \"Correct. John F. Kennedy was the 35th president of the U.S.\", \"match_case\": false, \"fuzzy_threshold\": 0.78}, {\"answer\": \"JFK\", \"correct\": true, \"feedback\": \"Correct. John F. Kennedy was the 35 preside of the U.S.\"}, {\"answer\": \"Kennedy\", \"correct\": false, \"feedback\": \"Please also provide the first name.\", \"match_case\": false}]}];\n", "\n", "if (typeof Question === 'undefined') {\n", "// Make a random ID\n", "function makeid(length) {\n", " var result = [];\n", " var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';\n", " var charactersLength = characters.length;\n", " for (var i = 0; i < length; i++) {\n", " result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));\n", " }\n", " return result.join('');\n", "}\n", "// Convert LaTeX delimiters and markdown links to HTML\n", "function jaxify(string) {\n", " let mystring = string;\n", " let count = 0, count2 = 0;\n", " let loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " let loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " while (loc >= 0 || loc2 >= 0) {\n", " if (loc2 >= 0) {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$\\$)/, count2 % 2 ? '$1\\\\]' : '$1\\\\[');\n", " count2++;\n", " } else {\n", " mystring = mystring.replace(/([^\\\\]|^)(\\$)/, count % 2 ? '$1\\\\)' : '$1\\\\(');\n", " count++;\n", " }\n", " loc = mystring.search(/([^\\\\]|^)(\\$)/);\n", " loc2 = mystring.search(/([^\\\\]|^)(\\$\\$)/);\n", " }\n", " // Replace markdown links\n", " mystring = mystring.replace(//g, 'http$1');\n", " mystring = mystring.replace(/\\[(.*?)\\]\\((.*?)\\)/g, '$1');\n", " return mystring;\n", "}\n", "\n", "// Base class for question types\n", "class Question {\n", " static registry = {};\n", " static register(type, cls) {\n", " Question.registry[type] = cls;\n", " }\n", " static create(qa, id, index, options, rootDiv) {\n", " const Cls = Question.registry[qa.type];\n", " if (!Cls) {\n", " console.error(`No question class registered for type \"${qa.type}\"`);\n", " return;\n", " }\n", " const q = new Cls(qa, id, index, options, rootDiv);\n", " q.render();\n", " }\n", "\n", " constructor(qa, id, index, options, rootDiv) {\n", " this.qa = qa;\n", " this.id = id;\n", " this.index = index;\n", " this.options = options;\n", " this.rootDiv = rootDiv;\n", " // wrapper\n", " this.wrapper = document.createElement('div');\n", " this.wrapper.id = `quizWrap${id}`;\n", " this.wrapper.className = 'Quiz';\n", " this.wrapper.dataset.qnum = index;\n", " this.wrapper.style.maxWidth = `${options.maxWidth}px`;\n", " rootDiv.appendChild(this.wrapper);\n", " // question container\n", " this.outerqDiv = document.createElement('div');\n", " this.outerqDiv.id = `OuterquizQn${id}${index}`;\n", " this.wrapper.appendChild(this.outerqDiv);\n", " // question text\n", " this.qDiv = document.createElement('div');\n", " this.qDiv.id = `quizQn${id}${index}`;\n", " if (qa.question) {\n", " this.qDiv.innerHTML = jaxify(qa.question);\n", " this.outerqDiv.appendChild(this.qDiv);\n", " }\n", " // code block\n", " if (qa.code) {\n", " const codeDiv = document.createElement('div');\n", " codeDiv.id = `code${id}${index}`;\n", " codeDiv.className = 'QuizCode';\n", " const pre = document.createElement('pre');\n", " const codeEl = document.createElement('code');\n", " codeEl.innerHTML = qa.code;\n", " pre.appendChild(codeEl);\n", " codeDiv.appendChild(pre);\n", " this.outerqDiv.appendChild(codeDiv);\n", " }\n", " // answer container\n", " this.aDiv = document.createElement('div');\n", " this.aDiv.id = `quizAns${id}${index}`;\n", " this.aDiv.className = 'Answer';\n", " this.wrapper.appendChild(this.aDiv);\n", " // feedback container (append after answers)\n", " this.fbDiv = document.createElement('div');\n", " this.fbDiv.id = `fb${id}`;\n", " this.fbDiv.className = 'Feedback';\n", " this.fbDiv.dataset.answeredcorrect = 0;\n", " }\n", "\n", " render() {\n", " throw new Error('render() not implemented');\n", " }\n", "\n", " preserveResponse(val) {\n", " if (!this.options.preserveResponses) return;\n", " const resp = document.getElementById(`responses${this.rootDiv.id}`);\n", " if (!resp) return;\n", " const arr = JSON.parse(resp.dataset.responses);\n", " arr[this.index] = val;\n", " resp.dataset.responses = JSON.stringify(arr);\n", " printResponses(resp);\n", " }\n", "\n", " typeset(container) {\n", " if (typeof MathJax !== 'undefined') {\n", " const v = MathJax.version;\n", " if (v[0] === '2') {\n", " MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " } else {\n", " MathJax.typeset([container]);\n", " }\n", " }\n", " }\n", "}\n", "\n", "// Choose a random subset of an array. Can also be used to shuffle the array\n", "function getRandomSubarray(arr, size) {\n", " var shuffled = arr.slice(0), i = arr.length, temp, index;\n", " while (i--) {\n", " index = Math.floor((i + 1) * Math.random());\n", " temp = shuffled[index];\n", " shuffled[index] = shuffled[i];\n", " shuffled[i] = temp;\n", " }\n", " return shuffled.slice(0, size);\n", "}\n", "\n", "function printResponses(responsesContainer) {\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " var stringResponses='IMPORTANT!To preserve this answer sequence for submission, when you have finalized your answers:
                    1. Copy the text in this cell below \"Answer String\"
                    2. Double click on the cell directly below the Answer String, labeled \"Replace Me\"
                    3. Select the whole \"Replace Me\" text
                    4. Paste in your answer string and press shift-Enter.
                    5. Save the notebook using the save icon or File->Save Notebook menu item



                    6. Answer String:
                      ';\n", " console.log(responses);\n", " responses.forEach((response, index) => {\n", " if (response) {\n", " console.log(index + ': ' + response);\n", " stringResponses+= index + ': ' + response +\"
                      \";\n", " }\n", " });\n", " responsesContainer.innerHTML=stringResponses;\n", "}\n", "/* Callback function to determine whether a selected multiple-choice\n", " button corresponded to a correct answer and to provide feedback\n", " based on the answer */\n", "function check_mc() {\n", " var id = this.id.split('-')[0];\n", " //var response = this.id.split('-')[1];\n", " //console.log(response);\n", " //console.log(\"In check_mc(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.correct) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var label = event.srcElement;\n", " //console.log(label, label.nodeName);\n", " var depth = 0;\n", " while ((label.nodeName != \"LABEL\") && (depth < 20)) {\n", " label = label.parentElement;\n", " console.log(depth, label);\n", " depth++;\n", " }\n", "\n", "\n", "\n", " var answers = label.parentElement.children;\n", " //console.log(answers);\n", "\n", " // Split behavior based on multiple choice vs many choice:\n", " var fb = document.getElementById(\"fb\" + id);\n", "\n", "\n", "\n", " /* Multiple choice (1 answer). Allow for 0 correct\n", " answers as an edge case */\n", " if (fb.dataset.numcorrect <= 1) {\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " responses[qnum]= response;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " //console.log(child);\n", " child.className = \"MCButton\";\n", " }\n", "\n", "\n", "\n", " if (label.dataset.correct == \"true\") {\n", " // console.log(\"Correct action\");\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Correct!\";\n", " }\n", " label.classList.add(\"correctButton\");\n", "\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " fb.innerHTML = jaxify(label.dataset.feedback);\n", " } else {\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", " }\n", " //console.log(\"Error action\");\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " }\n", " else { /* Many choice (more than 1 correct answer) */\n", " var reset = false;\n", " var feedback;\n", " if (label.dataset.correct == \"true\") {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Correct!\";\n", " }\n", " if (label.dataset.answered <= 0) {\n", " if (fb.dataset.answeredcorrect < 0) {\n", " fb.dataset.answeredcorrect = 1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect++;\n", " }\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"correctButton\");\n", " label.dataset.answered = 1;\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", "\n", " }\n", " } else {\n", " if (\"feedback\" in label.dataset) {\n", " feedback = jaxify(label.dataset.feedback);\n", " } else {\n", " feedback = \"Incorrect -- try again.\";\n", " }\n", " if (fb.dataset.answeredcorrect > 0) {\n", " fb.dataset.answeredcorrect = -1;\n", " reset = true;\n", " } else {\n", " fb.dataset.answeredcorrect--;\n", " }\n", "\n", " if (reset) {\n", " for (var i = 0; i < answers.length; i++) {\n", " var child = answers[i];\n", " child.className = \"MCButton\";\n", " child.dataset.answered = 0;\n", " }\n", " }\n", " label.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " //console.log(responsesContainer);\n", " var response = label.firstChild.innerText;\n", " if (label.querySelector(\".QuizCode\")){\n", " response+= label.querySelector(\".QuizCode\").firstChild.innerText;\n", " }\n", " console.log(response);\n", " //console.log(document.getElementById(\"quizWrap\"+id));\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " if (label.dataset.correct == \"true\") {\n", " if (typeof(responses[qnum]) == \"object\"){\n", " if (!responses[qnum].includes(response))\n", " responses[qnum].push(response);\n", " } else{\n", " responses[qnum]= [ response ];\n", " }\n", " } else {\n", " responses[qnum]= response;\n", " }\n", " console.log(responses);\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End save responses stuff\n", "\n", "\n", "\n", " var numcorrect = fb.dataset.numcorrect;\n", " var answeredcorrect = fb.dataset.answeredcorrect;\n", " if (answeredcorrect >= 0) {\n", " fb.innerHTML = feedback + \" [\" + answeredcorrect + \"/\" + numcorrect + \"]\";\n", " } else {\n", " fb.innerHTML = feedback + \" [\" + 0 + \"/\" + numcorrect + \"]\";\n", " }\n", "\n", "\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", "\n", "}\n", "\n", "\n", "/* Function to produce the HTML buttons for a multiple choice/\n", " many choice question and to update the CSS tags based on\n", " the question type */\n", "function make_mc(qa, shuffle_answers, outerqDiv, qDiv, aDiv, id) {\n", "\n", " var shuffled;\n", " if (shuffle_answers == true) {\n", " //console.log(shuffle_answers+\" read as true\");\n", " shuffled = getRandomSubarray(qa.answers, qa.answers.length);\n", " } else {\n", " //console.log(shuffle_answers+\" read as false\");\n", " shuffled = qa.answers;\n", " }\n", "\n", "\n", " var num_correct = 0;\n", "\n", " shuffled.forEach((item, index, ans_array) => {\n", " //console.log(answer);\n", "\n", " // Make label for input element\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"MCButton\";\n", " lab.id = id + '-' + index;\n", " lab.onclick = check_mc;\n", "\n", " // Make input element\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"radio\";\n", " inp.id = \"quizo\" + id + index;\n", " inp.name = \"mcgroup-\" + id; // for grouping radios\n", " inp.className = \"sr-only\"; // or \"visually-hidden\" or whatever you call it\n", "\n", "\n", " lab.append(inp); // input is now inside the label\n", "\n", " var aSpan = document.createElement('span');\n", " if (\"answer\" in item) {\n", " aSpan.innerHTML = jaxify(item.answer);\n", " }\n", " lab.append(aSpan);\n", "\n", " // Create div for code inside question\n", " if (\"code\" in item) {\n", " var codeSpan = document.createElement('span');\n", " codeSpan.id = \"code\" + id + index;\n", " codeSpan.className = \"QuizCode\";\n", " var codePre = document.createElement('pre');\n", " codeSpan.append(codePre);\n", " var codeCode = document.createElement('code');\n", " codePre.append(codeCode);\n", " codeCode.innerHTML = item.code;\n", " lab.append(codeSpan);\n", " }\n", "\n", " // Set the data attributes for the answer\n", " lab.setAttribute('data-correct', item.correct);\n", " if (item.correct) {\n", " num_correct++;\n", " }\n", " if (\"feedback\" in item) {\n", " lab.setAttribute('data-feedback', item.feedback);\n", " }\n", " lab.setAttribute('data-answered', 0);\n", "\n", " // Only append the label (input is inside)\n", " aDiv.append(lab);\n", "\n", "\n", " });\n", "\n", " if (num_correct > 1) {\n", " outerqDiv.className = \"ManyChoiceQn\";\n", " } else {\n", " outerqDiv.className = \"MultipleChoiceQn\";\n", " }\n", "\n", " return num_correct;\n", "\n", "}\n", "// Object-oriented wrapper for MC/MANY choice\n", "class MCQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) { super(qa, id, idx, opts, rootDiv); }\n", " render() {\n", " //console.log(\"options.shuffleAnswers \" + this.options.shuffleAnswers);\n", " const numCorrect = make_mc(\n", " this.qa,\n", " this.options.shuffleAnswers,\n", " this.outerqDiv,\n", " this.qDiv,\n", " this.aDiv,\n", " this.id\n", " );\n", " if ('answer_cols' in this.qa) {\n", " this.aDiv.style.gridTemplateColumns =\n", " 'repeat(' + this.qa.answer_cols + ', 1fr)';\n", " }\n", " this.fbDiv.dataset.numcorrect = numCorrect;\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('multiple_choice', MCQuestion);\n", "Question.register('many_choice', MCQuestion);\n", "function check_numeric(ths, event) {\n", "\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", "\n", " var submission = ths.value;\n", " if (submission.indexOf('/') != -1) {\n", " var sub_parts = submission.split('/');\n", " //console.log(sub_parts);\n", " submission = sub_parts[0] / sub_parts[1];\n", " }\n", " //console.log(\"Reader entered\", submission);\n", "\n", " if (\"precision\" in ths.dataset) {\n", " var precision = ths.dataset.precision;\n", " submission = Number(Number(submission).toPrecision(precision));\n", " }\n", "\n", "\n", " //console.log(\"In check_numeric(), id=\"+id);\n", " //console.log(event.srcElement.id) \n", " //console.log(event.srcElement.dataset.feedback)\n", "\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " //console.log(answers);\n", "\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", " answers.every(answer => {\n", " //console.log(answer.type);\n", "\n", " correct = false;\n", " // if (answer.type==\"value\"){\n", " if ('value' in answer) {\n", " var value;\n", " if (\"precision\" in ths.dataset) {\n", " value = answer.value.toPrecision(ths.dataset.precision);\n", " } else {\n", " value = answer.value;\n", " }\n", " if (submission == value) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " //console.log(answer.correct);\n", " done = true;\n", " }\n", "\n", " // } else if (answer.type==\"range\") {\n", " } else if ('range' in answer) {\n", " console.log(answer.range);\n", " console.log(submission, submission >=answer.range[0], submission < answer.range[1])\n", " if ((submission >= answer.range[0]) && (submission < answer.range[1])) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " correct = answer.correct;\n", " console.log(answer.correct);\n", " done = true;\n", " }\n", " } else if (answer.type == \"default\") {\n", " if (\"feedback\" in answer) {\n", " defaultFB = answer.feedback;\n", " } \n", " }\n", " if (done) {\n", " return false; // Break out of loop if this has been marked correct\n", " } else {\n", " return true; // Keep looking for case that includes this as a correct answer\n", " }\n", " });\n", " console.log(\"done:\", done);\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " //console.log(\"Default feedback\", defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " // What follows is for the saved responses stuff\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " console.log(submission);\n", " var qnum = document.getElementById(\"quizWrap\"+id).dataset.qnum;\n", " //console.log(\"Question \" + qnum);\n", " //console.log(id, \", got numcorrect=\",fb.dataset.numcorrect);\n", " var responses=JSON.parse(responsesContainer.dataset.responses);\n", " console.log(responses);\n", " if (submission == ths.value){\n", " responses[qnum]= submission;\n", " } else {\n", " responses[qnum]= ths.value + \"(\" + submission +\")\";\n", " }\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", " // End code to preserve responses\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " console.log('MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " // find the current question wrapper\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " console.log(height);\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "\n", "}\n", "// Object-oriented wrapper for numeric questions\n", "class NumericQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_numeric(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('numeric', NumericQuestion);\n", "\n", "function isValid(el, charC) {\n", " //console.log(\"Input char: \", charC);\n", " if (charC == 46) {\n", " if (el.value.indexOf('.') === -1) {\n", " return true;\n", " } else if (el.value.indexOf('/') != -1) {\n", " var parts = el.value.split('/');\n", " if (parts[1].indexOf('.') === -1) {\n", " return true;\n", " }\n", " }\n", " else {\n", " return false;\n", " }\n", " } else if (charC == 47) {\n", " if (el.value.indexOf('/') === -1) {\n", " if ((el.value != \"\") && (el.value != \".\")) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 45) {\n", " var edex = el.value.indexOf('e');\n", " if (edex == -1) {\n", " edex = el.value.indexOf('E');\n", " }\n", "\n", " if (el.value == \"\") {\n", " return true;\n", " } else if (edex == (el.value.length - 1)) { // If just after e or E\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else if (charC == 101) { // \"e\"\n", " if ((el.value.indexOf('e') === -1) && (el.value.indexOf('E') === -1) && (el.value.indexOf('/') == -1)) {\n", " // Prev symbol must be digit or decimal point:\n", " if (el.value.slice(-1).search(/\\d/) >= 0) {\n", " return true;\n", " } else if (el.value.slice(-1).search(/\\./) >= 0) {\n", " return true;\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " return false;\n", " }\n", " } else {\n", " if (charC > 31 && (charC < 48 || charC > 57))\n", " return false;\n", " }\n", " return true;\n", "}\n", "\n", "function numeric_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_numeric(this, evnt);\n", " } else {\n", " return isValid(this, charC);\n", " }\n", "}\n", "\n", "\n", "\n", "\n", "\n", "function make_numeric(qa, outerqDiv, qDiv, aDiv, id) {\n", "\n", "\n", "\n", " //console.log(answer);\n", "\n", "\n", " outerqDiv.className = \"NumericQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type numeric answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " //inp.id=\"input-\"+id;\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " if (\"precision\" in qa) {\n", " inp.setAttribute('data-precision', qa.precision);\n", " }\n", " aDiv.append(inp);\n", " //console.log(inp);\n", "\n", " //inp.addEventListener(\"keypress\", check_numeric);\n", " //inp.addEventListener(\"keypress\", numeric_keypress);\n", " /*\n", " inp.addEventListener(\"keypress\", function(event) {\n", " return numeric_keypress(this, event);\n", " }\n", " );\n", " */\n", " //inp.onkeypress=\"return numeric_keypress(this, event)\";\n", " inp.onkeypress = numeric_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " }\n", " );\n", "\n", "\n", "}\n", "// Override show_questions to use object-oriented Question API\n", "function show_questions(json, container) {\n", " // Accept container element or element ID\n", " if (typeof container === 'string') {\n", " container = document.getElementById(container);\n", " }\n", " if (!container) {\n", " console.error('show_questions: invalid container', container);\n", " return;\n", " }\n", "\n", " const shuffleQuestions = container.dataset.shufflequestions === 'True';\n", " const shuffleAnswers = container.dataset.shuffleanswers === 'True';\n", " const preserveResponses = container.dataset.preserveresponses === 'true';\n", " const maxWidth = parseInt(container.dataset.maxwidth, 10) || 0;\n", " let numQuestions = parseInt(container.dataset.numquestions, 10) || json.length;\n", " if (numQuestions > json.length) numQuestions = json.length;\n", "\n", " let questions = json;\n", " if (shuffleQuestions || numQuestions < json.length) {\n", " questions = getRandomSubarray(json, numQuestions);\n", " }\n", "\n", " questions.forEach((qa, index) => {\n", " const id = makeid(8);\n", " const options = {\n", " shuffleAnswers: shuffleAnswers,\n", " preserveResponses: preserveResponses,\n", " maxWidth: maxWidth\n", " };\n", " Question.create(qa, id, index, options, container);\n", " });\n", "\n", " if (preserveResponses) {\n", " const respDiv = document.createElement('div');\n", " respDiv.id = 'responses' + container.id;\n", " respDiv.className = 'JCResponses';\n", " respDiv.dataset.responses = JSON.stringify([]);\n", " respDiv.innerHTML = 'Select your answers and then follow the directions that will appear here.';\n", " container.appendChild(respDiv);\n", " }\n", "\n", " // Trigger MathJax typesetting if available\n", " if (typeof MathJax != 'undefined') {\n", " console.log(\"MathJax version\", MathJax.version);\n", " var version = MathJax.version;\n", " setTimeout(function(){\n", " var version = MathJax.version;\n", " console.log('After sleep, MathJax version', version);\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " }\n", " }, 500);\n", "if (typeof version == 'undefined') {\n", " } else\n", " {\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " if (MathJax.hasOwnProperty('typeset') ) {\n", " MathJax.typeset([container]);\n", " } else {\n", " console.log('WARNING: Trying to force load MathJax 3');\n", " window.MathJax = {\n", " tex: {\n", " inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]\n", " },\n", " svg: {\n", " fontCache: 'global'\n", " }\n", " };\n", "\n", " (function () {\n", " var script = document.createElement('script');\n", " script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';\n", " script.async = true;\n", " document.head.appendChild(script);\n", " })();\n", " }\n", " } else {\n", " console.log(\"MathJax not found\");\n", " }\n", " }\n", " }\n", " // if (typeof MathJax !== 'undefined') {\n", " // const v = MathJax.version;\n", " // if (v[0] === '2') {\n", " // MathJax.Hub.Queue(['Typeset', MathJax.Hub]);\n", " // } else if (v[0] === '3') {\n", " // MathJax.typeset([container]);\n", " // }\n", " // }\n", "\n", " // Prevent link clicks from bubbling up\n", " Array.from(container.getElementsByClassName('Link')).forEach(link => {\n", " link.addEventListener('click', e => e.stopPropagation());\n", " });\n", "}\n", "function levenshteinDistance(a, b) {\n", " if (a.length === 0) return b.length;\n", " if (b.length === 0) return a.length;\n", "\n", " const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));\n", "\n", " for (let i = 0; i <= a.length; i++) {\n", " matrix[0][i] = i;\n", " }\n", "\n", " for (let j = 0; j <= b.length; j++) {\n", " matrix[j][0] = j;\n", " }\n", "\n", " for (let j = 1; j <= b.length; j++) {\n", " for (let i = 1; i <= a.length; i++) {\n", " const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n", " matrix[j][i] = Math.min(\n", " matrix[j - 1][i] + 1, // Deletion\n", " matrix[j][i - 1] + 1, // Insertion\n", " matrix[j - 1][i - 1] + cost // Substitution\n", " );\n", " }\n", " }\n", " return matrix[b.length][a.length];\n", "}\n", "// Object-oriented wrapper for string input questions\n", "class StringQuestion extends Question {\n", " constructor(qa, id, idx, opts, rootDiv) {\n", " super(qa, id, idx, opts, rootDiv);\n", " }\n", " render() {\n", " make_string(this.qa, this.outerqDiv, this.qDiv, this.aDiv, this.id);\n", " this.wrapper.appendChild(this.fbDiv);\n", " }\n", "}\n", "Question.register('string', StringQuestion);\n", "\n", "function check_string(ths, event) {\n", " if (event.keyCode === 13) {\n", " ths.blur();\n", "\n", " var id = ths.id.split('-')[0];\n", " var submission = ths.value.trim();\n", " var fb = document.getElementById(\"fb\" + id);\n", " fb.style.display = \"none\";\n", " fb.innerHTML = \"Incorrect -- try again.\";\n", "\n", " var answers = JSON.parse(ths.dataset.answers);\n", " var defaultFB = \"Incorrect. Try again.\";\n", " var correct;\n", " var done = false;\n", "\n", " // Handle default answer pattern: filter out and capture default feedback\n", " var filteredAnswers = [];\n", " answers.forEach(answer => {\n", " if (answer.type === \"default\") {\n", " defaultFB = answer.feedback;\n", " } else {\n", " filteredAnswers.push(answer);\n", " }\n", " });\n", " answers = filteredAnswers;\n", "\n", " answers.every(answer => {\n", " correct = false;\n", "\n", " let match = false;\n", " if (answer.match_case) {\n", " match = submission === answer.answer;\n", " } else {\n", " match = submission.toLowerCase() === answer.answer.toLowerCase();\n", " }\n", " console.log(submission);\n", " console.log(answer.answer);\n", " console.log(match);\n", "\n", " if (match) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " } else if (answer.fuzzy_threshold) {\n", " var max_length = Math.max(submission.length, answer.answer.length);\n", " var ratio;\n", " if (answer.match_case) {\n", " ratio = 1- (levenshteinDistance(submission, answer.answer) / max_length);\n", " } else {\n", " ratio = 1- (levenshteinDistance(submission.toLowerCase(),\n", " answer.answer.toLowerCase()) / max_length);\n", " }\n", " if (ratio >= answer.fuzzy_threshold) {\n", " if (\"feedback\" in answer) {\n", " fb.innerHTML = jaxify(\"(Fuzzy) \" + answer.feedback);\n", " } else {\n", " fb.innerHTML = jaxify(\"Correct\");\n", " }\n", " correct = answer.correct;\n", " done = true;\n", " }\n", "\n", " }\n", "\n", " if (done) {\n", " return false;\n", " } else {\n", " return true;\n", " }\n", " });\n", "\n", " if ((!done) && (defaultFB != \"\")) {\n", " fb.innerHTML = jaxify(defaultFB);\n", " }\n", "\n", " fb.style.display = \"block\";\n", " if (correct) {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"correctButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"correct\");\n", " } else {\n", " ths.className = \"Input-text\";\n", " ths.classList.add(\"incorrectButton\");\n", " fb.className = \"Feedback\";\n", " fb.classList.add(\"incorrect\");\n", " }\n", "\n", " var outerContainer = fb.parentElement.parentElement;\n", " var responsesContainer = document.getElementById(\"responses\" + outerContainer.id);\n", " if (responsesContainer) {\n", " var qnum = document.getElementById(\"quizWrap\" + id).dataset.qnum;\n", " var responses = JSON.parse(responsesContainer.dataset.responses);\n", " responses[qnum] = submission;\n", " responsesContainer.setAttribute('data-responses', JSON.stringify(responses));\n", " printResponses(responsesContainer);\n", " }\n", "\n", " if (typeof MathJax != 'undefined') {\n", " var version = MathJax.version;\n", " if (version[0] == \"2\") {\n", " MathJax.Hub.Queue([\"Typeset\", MathJax.Hub]);\n", " } else if (version[0] == \"3\") {\n", " MathJax.typeset([fb]);\n", " }\n", " } else {\n", " console.log('MathJax not detected');\n", " }\n", " // After correct answer, if next JupyterQuiz question exists and has a text input, scroll by current question height\n", " if (correct) {\n", " var wrapper = ths.closest('.Quiz');\n", " if (wrapper) {\n", " var nextWrapper = wrapper.nextElementSibling;\n", " if (nextWrapper && nextWrapper.classList.contains('Quiz')) {\n", " var nextInput = nextWrapper.querySelector('input.Input-text');\n", " if (nextInput) {\n", " var height = wrapper.getBoundingClientRect().height;\n", " nextInput.focus();\n", " }\n", " }\n", " }\n", " }\n", " return false;\n", " }\n", "}\n", "\n", "function string_keypress(evnt) {\n", " var charC = (evnt.which) ? evnt.which : evnt.keyCode;\n", "\n", " if (charC == 13) {\n", " check_string(this, evnt);\n", " } \n", "}\n", "\n", "\n", "function make_string(qa, outerqDiv, qDiv, aDiv, id) {\n", " outerqDiv.className = \"StringQn\";\n", " aDiv.style.display = 'block';\n", "\n", " var lab = document.createElement(\"label\");\n", " lab.className = \"InpLabel\";\n", " lab.innerHTML = \"Type your answer here:\";\n", " aDiv.append(lab);\n", "\n", " var inp = document.createElement(\"input\");\n", " inp.type = \"text\";\n", " inp.id = id + \"-0\";\n", " inp.className = \"Input-text\";\n", " inp.setAttribute('data-answers', JSON.stringify(qa.answers));\n", " // Apply optional input width (approx. number of characters, in em units)\n", " if (qa.input_width != null) {\n", " inp.style['min-width'] = qa.input_width + 'em';\n", " }\n", " aDiv.append(inp);\n", "\n", " inp.onkeypress = string_keypress;\n", " inp.onpaste = event => false;\n", "\n", " inp.addEventListener(\"focus\", function (event) {\n", " this.value = \"\";\n", " return false;\n", " });\n", "}\n", "/*\n", " * Handle asynchrony issues when re-running quizzes in Jupyter notebooks.\n", " * Ensures show_questions is called after the container div is in the DOM.\n", " */\n", "function try_show() {\n", " if (document.getElementById(\"aRCrRhfLxyVv\")) {\n", " show_questions(questionsaRCrRhfLxyVv, aRCrRhfLxyVv);\n", " } else {\n", " setTimeout(try_show, 200);\n", " }\n", "};\n", "// Invoke immediately\n", "{\n", " try_show();\n", "}\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display_quiz(example_string)" ] } ], "metadata": { "finalized": { "timestamp": 1622215912635, "trusted": true }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.0" } }, "nbformat": 4, "nbformat_minor": 4 }