diff --git a/amd/build/instantiation.min.js b/amd/build/instantiation.min.js index 6bb98854..f86d5a10 100644 --- a/amd/build/instantiation.min.js +++ b/amd/build/instantiation.min.js @@ -6,6 +6,6 @@ define("qtype_formulas/instantiation",["exports","core/notification","core/str", * @copyright 2022 Philipp Imhof * @author Philipp Imhof * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Notification=_interopRequireWildcard(Notification),String=_interopRequireWildcard(String),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};var numberOfParts=0;const extendTabulator=()=>{_tabulator.TabulatorFull.extendModule("columnCalcs","calculations",{stats:values=>{var count=0,min=1/0,max=-1/0,sum=0;for(let value of values)sum+=parseFloat(value),min=Math.min(min,value),max=Math.max(max,value),count++;return min===max?["","",""]:count>0&&!isNaN(sum)?[(sum/count).toFixed(1),min,max]:["","",""]}})},initTable=()=>{new _tabulator.TabulatorFull("#varsdata_display",{selectable:1,movableColumns:!0,pagination:"local",paginationSize:10,paginationButtonCount:0,columns:[{title:"#",field:"id"}],langs:{default:{pagination:{first:"⏮",last:"⏭",prev:"⏪",next:"⏩"}}}}).on("rowSelected",previewQuestionWithDataset)},quoteNonNumericValue=value=>{if(!isNaN(value))return value;if(value.startsWith("[")){let quotedElements=[],elements=(value=value.substring(1,value.length-1)).split(/\s*,\s*/);for(let element of elements)quotedElements.push(quoteNonNumericValue(element));return"["+quotedElements.join(", ")+"]"}return'"'.concat(value,'"')},fetchTextFromEditor=id=>void 0!==window.tinyMCE&&null!==window.tinyMCE.get(id)?window.tinyMCE.get(id).getContent():document.getElementById(id).value,previewQuestionWithDataset=async row=>{if(row.getElement().classList.contains("tabulator-calcs"))return;let data=row.getData(),questionvars="",partvars=Array(numberOfParts).fill("");for(let varname in data)if(varname.match(/^(random|global)_/)&&(questionvars+=varname.replace(/^(random|global)_([^*]+)\*?$/,"$2")+"=",questionvars+=quoteNonNumericValue(data[varname])+";"),varname.match(/^part_(\d+)_/)){if(varname.match(/^part_(\d+)__/))continue;let index=parseInt(varname.replace(/^part_(\d+)_.*$/,"$1"));partvars[index]+=varname.replace(/^part_(\d+)_([^*]+)\*?$/,"$2")+"=",partvars[index]+=quoteNonNumericValue(data[varname])+";"}let parttexts=[];for(let i=0;i{let div=document.getElementById("qtextpreview_display");div.innerHTML=data.question;for(let text of data.parts)div.innerHTML+=text;(element=>{if(void 0===window.MathJax)return;let version=window.MathJax.version;"2"!=version[0]?"3"==version[0]&&window.MathJax.typesetPromise([element]):window.MathJax.Hub.Queue(["Typeset",window.MathJax.Hub,element])})(div)},localizeColumnGroupNames=async()=>{let partStringRequests=[];for(let i=0;i{element.setAttribute("aria-title",title),element.querySelector("div.tabulator-col-title").innerText=title},fillTable=data=>{let allRows=[],rowCounter=0;for(let row of data){let thisRow={id:++rowCounter};for(let thisVar of row.randomvars)thisRow["random_".concat(thisVar.name)]=thisVar.value;for(let thisVar of row.globalvars)thisRow["global_".concat(thisVar.name)]=thisVar.value;let partCounter=0;for(let thisPart of row.parts){for(let thisVar of thisPart)thisRow["part_".concat(partCounter,"_").concat(thisVar.name)]=thisVar.value;partCounter++}allRows.push(thisRow)}_tabulator.TabulatorFull.findTable("#varsdata_display")[0].setData(allRows)};var _default={init:noParts=>{numberOfParts=noParts,extendTabulator(),initTable()},instantiate:async()=>{let howMany=document.getElementById("id_numdataset").value,localvars=[],answers=[];for(let i=0;i").concat(response.message)}else document.getElementById("qtextpreview_display").innerHTML="",(data=>{let firstRow=data[0],calcOptions={bottomCalc:"stats",bottomCalcFormatter:cell=>cell.getValue().join("
")},columnDescription=[{title:"#",field:"id",bottomCalcFormatter:()=>"⌀
min
max"}],randomColumns=[];for(let column of firstRow.randomvars)randomColumns.push({title:column.name,field:"random_".concat(column.name),...calcOptions});randomColumns.length>0&&columnDescription.push({title:"Random variables",columns:randomColumns});let globalColumns=[];for(let column of firstRow.globalvars)globalColumns.push({title:column.name,field:"global_".concat(column.name),...calcOptions});globalColumns.length>0&&columnDescription.push({title:"Global variables",columns:globalColumns});let partColumns=[],partIndex=0;for(let part of firstRow.parts){let thisPartsColumns=[];for(let vars of part)thisPartsColumns.push({title:vars.name,field:"part_".concat(partIndex,"_").concat(vars.name),...calcOptions});partColumns.push({title:"Part ".concat(partIndex+1),columns:thisPartsColumns}),partIndex++}columnDescription=[...columnDescription,...partColumns],_tabulator.TabulatorFull.findTable("#varsdata_display")[0].setColumns(columnDescription),fillTable(data),localizeColumnGroupNames();let holders=document.querySelectorAll("div.tabulator-calcs-holder");for(let holder of holders)holder.style.display=data.length>1?"block":"none"})(response.data)}catch(err){Notification.exception(err)}pendingPromise.resolve()}};return _exports.default=_default,_exports.default})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Notification=_interopRequireWildcard(Notification),String=_interopRequireWildcard(String),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};var numberOfParts=0;const extendTabulator=()=>{_tabulator.TabulatorFull.extendModule("columnCalcs","calculations",{stats:values=>{var count=0,min=1/0,max=-1/0,sum=0;for(let value of values)sum+=parseFloat(value),min=Math.min(min,value),max=Math.max(max,value),count++;return min===max?["","",""]:count>0&&!isNaN(sum)?[(sum/count).toFixed(1),min,max]:["","",""]}})},initTable=()=>{new _tabulator.TabulatorFull("#varsdata_display",{selectable:1,movableColumns:!0,pagination:"local",paginationSize:10,paginationButtonCount:0,columns:[{title:"#",field:"id"}],langs:{default:{pagination:{first:"⏮",last:"⏭",prev:"⏪",next:"⏩"}}}}).on("rowSelected",previewQuestionWithDataset)},quoteNonNumericValue=value=>{if(!isNaN(value))return value;if(value.startsWith("[")){let quotedElements=[],elements=(value=value.substring(1,value.length-1)).split(/\s*,\s*/);for(let element of elements)quotedElements.push(quoteNonNumericValue(element));return"["+quotedElements.join(", ")+"]"}return'"'.concat(value,'"')},fetchTextFromEditor=id=>void 0!==window.tinyMCE&&null!==window.tinyMCE.get(id)?window.tinyMCE.get(id).getContent():document.getElementById(id).value,previewQuestionWithDataset=async row=>{if(row.getElement().classList.contains("tabulator-calcs"))return;let data=row.getData(),questionvars="",partvars=Array(numberOfParts).fill("");for(let varname in data)if(varname.match(/^(random|global)_/)&&(questionvars+=varname.replace(/^(random|global)_([^*]+)\*?$/,"$2")+"=",questionvars+=quoteNonNumericValue(data[varname])+";"),varname.match(/^part_(\d+)_/)){if(varname.match(/^part_(\d+)__/))continue;let index=parseInt(varname.replace(/^part_(\d+)_.*$/,"$1"));partvars[index]+=varname.replace(/^part_(\d+)_([^*]+)\*?$/,"$2")+"=",partvars[index]+=quoteNonNumericValue(data[varname])+";"}let parttexts=[];for(let i=0;i{let div=document.getElementById("qtextpreview_display");div.innerHTML=data.question;for(let text of data.parts)div.innerHTML+=text;(element=>{if(void 0===window.MathJax)return;let version=window.MathJax.version;"2"!=version[0]?"3"==version[0]&&window.MathJax.typesetPromise([element]):window.MathJax.Hub.Queue(["Typeset",window.MathJax.Hub,element])})(div)},localizeColumnGroupNames=async()=>{let partStringRequests=[];for(let i=0;i{element.setAttribute("aria-title",title),element.querySelector("div.tabulator-col-title").innerText=title},fillTable=data=>{let allRows=[],rowCounter=0;for(let row of data){let thisRow={id:++rowCounter};for(let thisVar of row.randomvars)thisRow["random_".concat(thisVar.name)]=thisVar.value;for(let thisVar of row.globalvars)thisRow["global_".concat(thisVar.name)]=thisVar.value;let partCounter=0;for(let thisPart of row.parts){for(let thisVar of thisPart)thisRow["part_".concat(partCounter,"_").concat(thisVar.name)]=thisVar.value;partCounter++}allRows.push(thisRow)}_tabulator.TabulatorFull.findTable("#varsdata_display")[0].setData(allRows)};var _default={init:noParts=>{numberOfParts=noParts,extendTabulator(),initTable()},instantiate:async()=>{let howMany=document.getElementById("id_numdataset").value,localvars=[],answers=[];for(let i=0;i{let firstRow=data[0],calcOptions={bottomCalc:"stats",bottomCalcFormatter:cell=>cell.getValue().join("
")},columnDescription=[{title:"#",field:"id",bottomCalcFormatter:()=>"⌀
min
max"}],randomColumns=[];for(let column of firstRow.randomvars)randomColumns.push({title:column.name,field:"random_".concat(column.name),...calcOptions});randomColumns.length>0&&columnDescription.push({title:"Random variables",columns:randomColumns});let globalColumns=[];for(let column of firstRow.globalvars)globalColumns.push({title:column.name,field:"global_".concat(column.name),...calcOptions});globalColumns.length>0&&columnDescription.push({title:"Global variables",columns:globalColumns});let partColumns=[],partIndex=0;for(let part of firstRow.parts){let thisPartsColumns=[];for(let vars of part)thisPartsColumns.push({title:vars.name,field:"part_".concat(partIndex,"_").concat(vars.name),...calcOptions});partColumns.push({title:"Part ".concat(partIndex+1),columns:thisPartsColumns}),partIndex++}columnDescription=[...columnDescription,...partColumns],_tabulator.TabulatorFull.findTable("#varsdata_display")[0].setColumns(columnDescription),fillTable(data),localizeColumnGroupNames();let holders=document.querySelectorAll("div.tabulator-calcs-holder");for(let holder of holders)holder.style.display=data.length>1?"block":"none"})(response.data))}catch(err){Notification.exception(err)}pendingPromise.resolve()}};return _exports.default=_default,_exports.default})); //# sourceMappingURL=instantiation.min.js.map \ No newline at end of file diff --git a/amd/build/instantiation.min.js.map b/amd/build/instantiation.min.js.map index 5450bd20..dc8b81c6 100644 --- a/amd/build/instantiation.min.js.map +++ b/amd/build/instantiation.min.js.map @@ -1 +1 @@ -{"version":3,"file":"instantiation.min.js","sources":["../src/instantiation.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Helper functions to check instantiation of variables\n *\n * @module qtype_formulas/instantiation\n * @copyright 2022 Philipp Imhof\n * @author Philipp Imhof\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n import * as Notification from 'core/notification';\n import * as String from 'core/str';\n import Pending from 'core/pending';\n import {call as fetchMany} from 'core/ajax';\n import {TabulatorFull as Tabulator} from 'qtype_formulas/tabulator';\n\n/**\n * Number of subquestions (parts)\n */\nvar numberOfParts = 0;\n\nconst init = (noParts) => {\n numberOfParts = noParts;\n extendTabulator();\n initTable();\n};\n\n/**\n * Add some customizations to Tabulator.js\n */\nconst extendTabulator = () => {\n Tabulator.extendModule('columnCalcs', 'calculations', {\n 'stats': (values) => {\n var count = 0;\n var min = Infinity;\n var max = -Infinity;\n var sum = 0;\n\n for (let value of values) {\n sum += parseFloat(value);\n min = Math.min(min, value);\n max = Math.max(max, value);\n count++;\n }\n\n // If minimum and maximum are the same, we don't display the stats, because\n // the values are constant.\n if (min === max) {\n return ['', '', ''];\n }\n\n if (count > 0 && !isNaN(sum)) {\n return [(sum / count).toFixed(1), min, max];\n }\n return ['', '', ''];\n },\n });\n};\n\n/**\n * Init the table we use for checking the variables' instantiation.\n */\nconst initTable = () => {\n let table = new Tabulator('#varsdata_display', {\n selectable: 1,\n movableColumns: true,\n pagination: 'local',\n paginationSize: 10,\n paginationButtonCount: 0,\n columns: [\n {title: '#', field: 'id'},\n ],\n langs: {\n 'default': {\n 'pagination': {\n 'first': '⏮',\n 'last': '⏭',\n 'prev': '⏪',\n 'next': '⏩'\n }\n }\n },\n });\n table.on('rowSelected', previewQuestionWithDataset);\n};\n\n/**\n * For proper parsing in the backend, strings must be enclosed in double quotes,\n * but numbers must not.\n *\n * @param {string} value representation of a numberic, string or list (array) value\n * @returns {string} the same value, but with quotes added, if necessary\n */\nconst quoteNonNumericValue = (value) => {\n // Numbers must not be quoted.\n if (!isNaN(value)) {\n return value;\n }\n // For arrays, we have to check each element individually and quote, if necessary.\n // Formulas question does not currently support nested arrays, so we don't have to deal with that.\n if (value.startsWith('[')) {\n let quotedElements = [];\n // Remove leading and trailing bracket\n value = value.substring(1, value.length - 1);\n let elements = value.split(/\\s*,\\s*/);\n for (let element of elements) {\n quotedElements.push(quoteNonNumericValue(element));\n }\n return '[' + quotedElements.join(', ') + ']';\n }\n // Not a number and not an array, so we enclose it in double quotes.\n // This includes the case where the variable is an \"algebraic variable\",\n // because those are represented as {variablename}, e.g. {a} for the variable a.\n return `\"${value}\"`;\n};\n\n/**\n * The question text and the parts' text are stored in the editor. For some editors,\n * we can take the content from the textarea's value attribute. For TinyMCE (and maybe others),\n * we must use the corresponding API.\n *\n * @param {string} id id of the textarea\n * @returns {string} the question or part's text\n */\nconst fetchTextFromEditor = (id) => {\n if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.get(id) !== null) {\n return window.tinyMCE.get(id).getContent();\n }\n return document.getElementById(id).value;\n};\n\n/**\n * Extract data from the instantiation table (selected row) and send them to the backend,\n * in order to have the question text and parts' text rendered for the preview.\n *\n * @param {object} row RowComponent from Tabulator.js\n */\nconst previewQuestionWithDataset = async(row) => {\n // The statistics row is clickable, but we cannot use its data to preview the question.\n if (row.getElement().classList.contains('tabulator-calcs')) {\n return;\n }\n let data = row.getData();\n let questionvars = '';\n let partvars = Array(numberOfParts).fill('');\n\n for (let varname in data) {\n // Variables for the main question are all random or global.\n // Also, as random variables have already been instantiated, they are not random anymore.\n if (varname.match(/^(random|global)_/)) {\n questionvars += varname.replace(/^(random|global)_([^*]+)\\*?$/, '$2') + '=';\n questionvars += quoteNonNumericValue(data[varname]) + ';';\n }\n // Variables for a question part always start with part_ + number of the part\n if (varname.match(/^part_(\\d+)_/)) {\n // If the variable name starts with _ it should be removed, as these are\n // answers (or otherwise reserved names, but that should not be the case)\n if (varname.match(/^part_(\\d+)__/)) {\n continue;\n }\n let index = parseInt(varname.replace(/^part_(\\d+)_.*$/, '$1'));\n partvars[index] += varname.replace(/^part_(\\d+)_([^*]+)\\*?$/, '$2') + '=';\n partvars[index] += quoteNonNumericValue(data[varname]) + ';';\n }\n }\n\n let parttexts = [];\n for (let i = 0; i < numberOfParts; i++) {\n parttexts[i] = fetchTextFromEditor(`id_subqtext_${i}`);\n }\n\n let pendingPromise = new Pending('qtype_formulas/questionpreview');\n try {\n let renderedTexts = await fetchMany([{\n methodname: 'qtype_formulas_render_question_text',\n args: {\n questiontext: fetchTextFromEditor('id_questiontext'),\n parttexts: parttexts,\n globalvars: questionvars,\n partvars: partvars\n }\n }])[0];\n showRenderedQuestionAndParts(renderedTexts);\n } catch (err) {\n Notification.exception(err);\n }\n pendingPromise.resolve();\n};\n\n/**\n * Trigger MathJax rendering for the question.\n *\n * @param {Element} element the
element where the question text is shown\n */\nconst triggerMathJax = (element) => {\n if (typeof window.MathJax === 'undefined') {\n return;\n }\n let version = window.MathJax.version;\n if (version[0] == '2') {\n window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub, element]);\n return;\n }\n if (version[0] == '3') {\n window.MathJax.typesetPromise([element]);\n }\n};\n\n/**\n * This function is called after the AJAX request to the backend is completed. It will inject\n * the rendered texts into the preview div.\n *\n * @param {object} data rendered version of question text and parts' text\n */\nconst showRenderedQuestionAndParts = (data) => {\n let div = document.getElementById('qtextpreview_display');\n div.innerHTML = data.question;\n for (let text of data.parts) {\n div.innerHTML += text;\n }\n triggerMathJax(div);\n};\n\n/**\n * Derive the column description from the instantiated variables.\n *\n * @param {object} data instantiation data as received from the backend\n */\nconst prepareTableColumns = (data) => {\n let firstRow = data[0];\n let calcOptions = {bottomCalc: 'stats', bottomCalcFormatter: (cell) => cell.getValue().join('
')};\n let columnDescription = [{title: '#', field: 'id', bottomCalcFormatter: () => '⌀
min
max'}];\n\n // Random variables come first\n let randomColumns = [];\n for (let column of firstRow.randomvars) {\n randomColumns.push({\n title: column.name,\n field: `random_${column.name}`,\n ...calcOptions\n });\n }\n if (randomColumns.length > 0) {\n columnDescription.push({title: 'Random variables', columns: randomColumns});\n }\n\n // Then we take the global variables\n let globalColumns = [];\n for (let column of firstRow.globalvars) {\n globalColumns.push({\n title: column.name,\n field: `global_${column.name}`,\n ...calcOptions\n });\n }\n if (globalColumns.length > 0) {\n columnDescription.push({title: 'Global variables', columns: globalColumns});\n }\n\n // Finally, we prepare the groups for each part\n let partColumns = [];\n let partIndex = 0;\n for (let part of firstRow.parts) {\n let thisPartsColumns = [];\n for (let vars of part) {\n thisPartsColumns.push({\n title: vars.name,\n field: `part_${partIndex}_${vars.name}`,\n ...calcOptions\n });\n }\n partColumns.push({title: `Part ${partIndex + 1}`, columns: thisPartsColumns});\n partIndex++;\n }\n columnDescription = [...columnDescription, ...partColumns];\n Tabulator.findTable(\"#varsdata_display\")[0].setColumns(columnDescription);\n fillTable(data);\n // Fetch and show localized column group titles for random/global/part variables.\n localizeColumnGroupNames();\n\n\n // We do not show the calculation row in the footer if there's just one data set.\n let holders = document.querySelectorAll('div.tabulator-calcs-holder');\n for (let holder of holders) {\n holder.style.display = (data.length > 1 ? 'block' : 'none');\n }\n};\n\n/**\n * Make sure the column titles for random, global and part variables are localized.\n *\n * @returns {void}\n */\nconst localizeColumnGroupNames = async() => {\n // For proper localization, we need to fetch the text for each part separately, because\n // in some languages, the number might come before the word.\n let partStringRequests = [];\n for (let i = 0; i < numberOfParts; i++) {\n partStringRequests.push({key: 'answerno', component: 'qtype_formulas', param: i + 1});\n }\n let strings = null;\n let pendingPromise = new Pending('qtype_formulas/localization');\n try {\n strings = await String.get_strings([\n {key: 'varsrandom', component: 'qtype_formulas'},\n {key: 'varsglobal', component: 'qtype_formulas'},\n ...partStringRequests\n ]);\n } catch (err) {\n Notification.exception(err);\n }\n pendingPromise.resolve();\n // If fetching of strings was not successful, we quit here.\n if (strings === null) {\n return;\n }\n\n // Fetch all column groups. Unfortunately, Tabulator.js does currently only offer\n // an API to change column titles if the columns are not grouped. Therefore, we're\n // doing it manually.\n let columnGroups = document.querySelectorAll('div.tabulator-col-group');\n let i = 1;\n for (let group of columnGroups) {\n // We do not always have random and global variables, so it's better to make sure.\n if (group.getAttribute('aria-title') == 'Random variables') {\n setTitleForColumnGroup(group, strings[0]);\n continue;\n }\n if (group.getAttribute('aria-title') == 'Global variables') {\n setTitleForColumnGroup(group, strings[1]);\n continue;\n }\n // Remaining groups are for parts and there will always be at least one part.\n setTitleForColumnGroup(group, strings[1 + i]);\n i++;\n }\n};\n\n/**\n * Helper function to set the title and aria-title for a column group header.\n * @param {Element} element the
holding the column title\n * @param {string} title the new title\n */\nconst setTitleForColumnGroup = (element, title) => {\n element.setAttribute('aria-title', title);\n element.querySelector('div.tabulator-col-title').innerText = title;\n};\n\n/**\n * Prepare the data and send it to the Tabulator.js table for display.\n *\n * @param {object} data instantiation data as received from the backend\n */\nconst fillTable = (data) => {\n let allRows = [];\n let rowCounter = 0;\n for (let row of data) {\n let thisRow = {id: ++rowCounter};\n for (let thisVar of row.randomvars) {\n thisRow[`random_${thisVar.name}`] = thisVar.value;\n }\n for (let thisVar of row.globalvars) {\n thisRow[`global_${thisVar.name}`] = thisVar.value;\n }\n let partCounter = 0;\n for (let thisPart of row.parts) {\n for (let thisVar of thisPart) {\n thisRow[`part_${partCounter}_${thisVar.name}`] = thisVar.value;\n }\n partCounter++;\n }\n allRows.push(thisRow);\n }\n\n Tabulator.findTable(\"#varsdata_display\")[0].setData(allRows);\n};\n\n/**\n * Send the definition of random variables, global variables and parts' local variables\n * to the backend for instantiation. This will generate a certain number of rows, based\n * on the number the user has selected in the corresponding dropdown field. Once the\n * AJAX requeset is completed, the data will be forwarded to {@link prepareTableColumns}.\n */\nconst instantiate = async() => {\n let howMany = document.getElementById('id_numdataset').value;\n let localvars = [];\n let answers = [];\n for (let i = 0; i < numberOfParts; i++) {\n localvars[i] = document.getElementById(`id_vars1_${i}`).value;\n answers[i] = document.getElementById(`id_answer_${i}`).value;\n }\n let pendingPromise = new Pending('qtype_formulas/instantiate');\n try {\n let response = await fetchMany([{\n methodname: 'qtype_formulas_instantiate',\n args: {\n n: howMany,\n randomvars: document.getElementById('id_varsrandom').value,\n globalvars: document.getElementById('id_varsglobal').value,\n localvars: localvars,\n answers: answers\n }\n }])[0];\n if (response.status == 'error') {\n let str = await String.get_string('previewerror', 'qtype_formulas');\n document.getElementById('qtextpreview_display').innerHTML = `${str}
${response.message}`;\n } else {\n document.getElementById('qtextpreview_display').innerHTML = '';\n prepareTableColumns(response.data);\n }\n } catch (err) {\n Notification.exception(err);\n }\n pendingPromise.resolve();\n};\n\nexport default {init, instantiate};\n"],"names":["numberOfParts","extendTabulator","extendModule","values","count","min","Infinity","max","sum","value","parseFloat","Math","isNaN","toFixed","initTable","Tabulator","selectable","movableColumns","pagination","paginationSize","paginationButtonCount","columns","title","field","langs","on","previewQuestionWithDataset","quoteNonNumericValue","startsWith","quotedElements","elements","substring","length","split","element","push","join","fetchTextFromEditor","id","window","tinyMCE","get","getContent","document","getElementById","async","row","getElement","classList","contains","data","getData","questionvars","partvars","Array","fill","varname","match","replace","index","parseInt","parttexts","i","pendingPromise","Pending","renderedTexts","methodname","args","questiontext","globalvars","showRenderedQuestionAndParts","err","Notification","exception","resolve","div","innerHTML","question","text","parts","MathJax","version","typesetPromise","Hub","Queue","triggerMathJax","localizeColumnGroupNames","partStringRequests","key","component","param","strings","String","get_strings","columnGroups","querySelectorAll","group","getAttribute","setTitleForColumnGroup","setAttribute","querySelector","innerText","fillTable","allRows","rowCounter","thisRow","thisVar","randomvars","name","partCounter","thisPart","findTable","setData","init","noParts","instantiate","howMany","localvars","answers","response","n","status","str","get_string","message","firstRow","calcOptions","bottomCalc","bottomCalcFormatter","cell","getValue","columnDescription","randomColumns","column","globalColumns","partColumns","partIndex","part","thisPartsColumns","vars","setColumns","holders","holder","style","display","prepareTableColumns"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"instantiation.min.js","sources":["../src/instantiation.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Helper functions to check instantiation of variables\n *\n * @module qtype_formulas/instantiation\n * @copyright 2022 Philipp Imhof\n * @author Philipp Imhof\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n import * as Notification from 'core/notification';\n import * as String from 'core/str';\n import Pending from 'core/pending';\n import {call as fetchMany} from 'core/ajax';\n import {TabulatorFull as Tabulator} from 'qtype_formulas/tabulator';\n\n/**\n * Number of subquestions (parts)\n */\nvar numberOfParts = 0;\n\nconst init = (noParts) => {\n numberOfParts = noParts;\n extendTabulator();\n initTable();\n};\n\n/**\n * Add some customizations to Tabulator.js\n */\nconst extendTabulator = () => {\n Tabulator.extendModule('columnCalcs', 'calculations', {\n 'stats': (values) => {\n var count = 0;\n var min = Infinity;\n var max = -Infinity;\n var sum = 0;\n\n for (let value of values) {\n sum += parseFloat(value);\n min = Math.min(min, value);\n max = Math.max(max, value);\n count++;\n }\n\n // If minimum and maximum are the same, we don't display the stats, because\n // the values are constant.\n if (min === max) {\n return ['', '', ''];\n }\n\n if (count > 0 && !isNaN(sum)) {\n return [(sum / count).toFixed(1), min, max];\n }\n return ['', '', ''];\n },\n });\n};\n\n/**\n * Init the table we use for checking the variables' instantiation.\n */\nconst initTable = () => {\n let table = new Tabulator('#varsdata_display', {\n selectable: 1,\n movableColumns: true,\n pagination: 'local',\n paginationSize: 10,\n paginationButtonCount: 0,\n columns: [\n {title: '#', field: 'id'},\n ],\n langs: {\n 'default': {\n 'pagination': {\n 'first': '⏮',\n 'last': '⏭',\n 'prev': '⏪',\n 'next': '⏩'\n }\n }\n },\n });\n table.on('rowSelected', previewQuestionWithDataset);\n};\n\n/**\n * For proper parsing in the backend, strings must be enclosed in double quotes,\n * but numbers must not.\n *\n * @param {string} value representation of a numberic, string or list (array) value\n * @returns {string} the same value, but with quotes added, if necessary\n */\nconst quoteNonNumericValue = (value) => {\n // Numbers must not be quoted.\n if (!isNaN(value)) {\n return value;\n }\n // For arrays, we have to check each element individually and quote, if necessary.\n // Formulas question does not currently support nested arrays, so we don't have to deal with that.\n if (value.startsWith('[')) {\n let quotedElements = [];\n // Remove leading and trailing bracket\n value = value.substring(1, value.length - 1);\n let elements = value.split(/\\s*,\\s*/);\n for (let element of elements) {\n quotedElements.push(quoteNonNumericValue(element));\n }\n return '[' + quotedElements.join(', ') + ']';\n }\n // Not a number and not an array, so we enclose it in double quotes.\n // This includes the case where the variable is an \"algebraic variable\",\n // because those are represented as {variablename}, e.g. {a} for the variable a.\n return `\"${value}\"`;\n};\n\n/**\n * The question text and the parts' text are stored in the editor. For some editors,\n * we can take the content from the textarea's value attribute. For TinyMCE (and maybe others),\n * we must use the corresponding API.\n *\n * @param {string} id id of the textarea\n * @returns {string} the question or part's text\n */\nconst fetchTextFromEditor = (id) => {\n if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.get(id) !== null) {\n return window.tinyMCE.get(id).getContent();\n }\n return document.getElementById(id).value;\n};\n\n/**\n * Extract data from the instantiation table (selected row) and send them to the backend,\n * in order to have the question text and parts' text rendered for the preview.\n *\n * @param {object} row RowComponent from Tabulator.js\n */\nconst previewQuestionWithDataset = async(row) => {\n // The statistics row is clickable, but we cannot use its data to preview the question.\n if (row.getElement().classList.contains('tabulator-calcs')) {\n return;\n }\n let data = row.getData();\n let questionvars = '';\n let partvars = Array(numberOfParts).fill('');\n\n for (let varname in data) {\n // Variables for the main question are all random or global.\n // Also, as random variables have already been instantiated, they are not random anymore.\n if (varname.match(/^(random|global)_/)) {\n questionvars += varname.replace(/^(random|global)_([^*]+)\\*?$/, '$2') + '=';\n questionvars += quoteNonNumericValue(data[varname]) + ';';\n }\n // Variables for a question part always start with part_ + number of the part\n if (varname.match(/^part_(\\d+)_/)) {\n // If the variable name starts with _ it should be removed, as these are\n // answers (or otherwise reserved names, but that should not be the case)\n if (varname.match(/^part_(\\d+)__/)) {\n continue;\n }\n let index = parseInt(varname.replace(/^part_(\\d+)_.*$/, '$1'));\n partvars[index] += varname.replace(/^part_(\\d+)_([^*]+)\\*?$/, '$2') + '=';\n partvars[index] += quoteNonNumericValue(data[varname]) + ';';\n }\n }\n\n let parttexts = [];\n for (let i = 0; i < numberOfParts; i++) {\n parttexts[i] = fetchTextFromEditor(`id_subqtext_${i}`);\n }\n\n let pendingPromise = new Pending('qtype_formulas/questionpreview');\n try {\n let renderedTexts = await fetchMany([{\n methodname: 'qtype_formulas_render_question_text',\n args: {\n questiontext: fetchTextFromEditor('id_questiontext'),\n parttexts: parttexts,\n globalvars: questionvars,\n partvars: partvars\n }\n }])[0];\n showRenderedQuestionAndParts(renderedTexts);\n } catch (err) {\n Notification.exception(err);\n }\n pendingPromise.resolve();\n};\n\n/**\n * Trigger MathJax rendering for the question.\n *\n * @param {Element} element the
element where the question text is shown\n */\nconst triggerMathJax = (element) => {\n if (typeof window.MathJax === 'undefined') {\n return;\n }\n let version = window.MathJax.version;\n if (version[0] == '2') {\n window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub, element]);\n return;\n }\n if (version[0] == '3') {\n window.MathJax.typesetPromise([element]);\n }\n};\n\n/**\n * This function is called after the AJAX request to the backend is completed. It will inject\n * the rendered texts into the preview div.\n *\n * @param {object} data rendered version of question text and parts' text\n */\nconst showRenderedQuestionAndParts = (data) => {\n let div = document.getElementById('qtextpreview_display');\n div.innerHTML = data.question;\n for (let text of data.parts) {\n div.innerHTML += text;\n }\n triggerMathJax(div);\n};\n\n/**\n * Derive the column description from the instantiated variables.\n *\n * @param {object} data instantiation data as received from the backend\n */\nconst prepareTableColumns = (data) => {\n let firstRow = data[0];\n let calcOptions = {bottomCalc: 'stats', bottomCalcFormatter: (cell) => cell.getValue().join('
')};\n let columnDescription = [{title: '#', field: 'id', bottomCalcFormatter: () => '⌀
min
max'}];\n\n // Random variables come first\n let randomColumns = [];\n for (let column of firstRow.randomvars) {\n randomColumns.push({\n title: column.name,\n field: `random_${column.name}`,\n ...calcOptions\n });\n }\n if (randomColumns.length > 0) {\n columnDescription.push({title: 'Random variables', columns: randomColumns});\n }\n\n // Then we take the global variables\n let globalColumns = [];\n for (let column of firstRow.globalvars) {\n globalColumns.push({\n title: column.name,\n field: `global_${column.name}`,\n ...calcOptions\n });\n }\n if (globalColumns.length > 0) {\n columnDescription.push({title: 'Global variables', columns: globalColumns});\n }\n\n // Finally, we prepare the groups for each part\n let partColumns = [];\n let partIndex = 0;\n for (let part of firstRow.parts) {\n let thisPartsColumns = [];\n for (let vars of part) {\n thisPartsColumns.push({\n title: vars.name,\n field: `part_${partIndex}_${vars.name}`,\n ...calcOptions\n });\n }\n partColumns.push({title: `Part ${partIndex + 1}`, columns: thisPartsColumns});\n partIndex++;\n }\n columnDescription = [...columnDescription, ...partColumns];\n Tabulator.findTable(\"#varsdata_display\")[0].setColumns(columnDescription);\n fillTable(data);\n // Fetch and show localized column group titles for random/global/part variables.\n localizeColumnGroupNames();\n\n\n // We do not show the calculation row in the footer if there's just one data set.\n let holders = document.querySelectorAll('div.tabulator-calcs-holder');\n for (let holder of holders) {\n holder.style.display = (data.length > 1 ? 'block' : 'none');\n }\n};\n\n/**\n * Make sure the column titles for random, global and part variables are localized.\n *\n * @returns {void}\n */\nconst localizeColumnGroupNames = async() => {\n // For proper localization, we need to fetch the text for each part separately, because\n // in some languages, the number might come before the word.\n let partStringRequests = [];\n for (let i = 0; i < numberOfParts; i++) {\n partStringRequests.push({key: 'answerno', component: 'qtype_formulas', param: i + 1});\n }\n let strings = null;\n let pendingPromise = new Pending('qtype_formulas/localization');\n try {\n strings = await String.get_strings([\n {key: 'varsrandom', component: 'qtype_formulas'},\n {key: 'varsglobal', component: 'qtype_formulas'},\n ...partStringRequests\n ]);\n } catch (err) {\n Notification.exception(err);\n }\n pendingPromise.resolve();\n // If fetching of strings was not successful, we quit here.\n if (strings === null) {\n return;\n }\n\n // Fetch all column groups. Unfortunately, Tabulator.js does currently only offer\n // an API to change column titles if the columns are not grouped. Therefore, we're\n // doing it manually.\n let columnGroups = document.querySelectorAll('div.tabulator-col-group');\n let i = 1;\n for (let group of columnGroups) {\n // We do not always have random and global variables, so it's better to make sure.\n if (group.getAttribute('aria-title') == 'Random variables') {\n setTitleForColumnGroup(group, strings[0]);\n continue;\n }\n if (group.getAttribute('aria-title') == 'Global variables') {\n setTitleForColumnGroup(group, strings[1]);\n continue;\n }\n // Remaining groups are for parts and there will always be at least one part.\n setTitleForColumnGroup(group, strings[1 + i]);\n i++;\n }\n};\n\n/**\n * Helper function to set the title and aria-title for a column group header.\n * @param {Element} element the
holding the column title\n * @param {string} title the new title\n */\nconst setTitleForColumnGroup = (element, title) => {\n element.setAttribute('aria-title', title);\n element.querySelector('div.tabulator-col-title').innerText = title;\n};\n\n/**\n * Prepare the data and send it to the Tabulator.js table for display.\n *\n * @param {object} data instantiation data as received from the backend\n */\nconst fillTable = (data) => {\n let allRows = [];\n let rowCounter = 0;\n for (let row of data) {\n let thisRow = {id: ++rowCounter};\n for (let thisVar of row.randomvars) {\n thisRow[`random_${thisVar.name}`] = thisVar.value;\n }\n for (let thisVar of row.globalvars) {\n thisRow[`global_${thisVar.name}`] = thisVar.value;\n }\n let partCounter = 0;\n for (let thisPart of row.parts) {\n for (let thisVar of thisPart) {\n thisRow[`part_${partCounter}_${thisVar.name}`] = thisVar.value;\n }\n partCounter++;\n }\n allRows.push(thisRow);\n }\n\n Tabulator.findTable(\"#varsdata_display\")[0].setData(allRows);\n};\n\n/**\n * Send the definition of random variables, global variables and parts' local variables\n * to the backend for instantiation. This will generate a certain number of rows, based\n * on the number the user has selected in the corresponding dropdown field. Once the\n * AJAX requeset is completed, the data will be forwarded to {@link prepareTableColumns}.\n */\nconst instantiate = async() => {\n let howMany = document.getElementById('id_numdataset').value;\n let localvars = [];\n let answers = [];\n for (let i = 0; i < numberOfParts; i++) {\n localvars[i] = document.getElementById(`id_vars1_${i}`).value;\n answers[i] = document.getElementById(`id_answer_${i}`).value;\n }\n let pendingPromise = new Pending('qtype_formulas/instantiate');\n try {\n let response = await fetchMany([{\n methodname: 'qtype_formulas_instantiate',\n args: {\n n: howMany,\n randomvars: document.getElementById('id_varsrandom').value,\n globalvars: document.getElementById('id_varsglobal').value,\n localvars: localvars,\n answers: answers\n }\n }])[0];\n if (response.status == 'error') {\n document.getElementById('qtextpreview_display').innerHTML = await String.get_string(\n 'previewerror', 'qtype_formulas', response.message\n );\n } else {\n document.getElementById('qtextpreview_display').innerHTML = '';\n prepareTableColumns(response.data);\n }\n } catch (err) {\n Notification.exception(err);\n }\n pendingPromise.resolve();\n};\n\nexport default {init, instantiate};\n"],"names":["numberOfParts","extendTabulator","extendModule","values","count","min","Infinity","max","sum","value","parseFloat","Math","isNaN","toFixed","initTable","Tabulator","selectable","movableColumns","pagination","paginationSize","paginationButtonCount","columns","title","field","langs","on","previewQuestionWithDataset","quoteNonNumericValue","startsWith","quotedElements","elements","substring","length","split","element","push","join","fetchTextFromEditor","id","window","tinyMCE","get","getContent","document","getElementById","async","row","getElement","classList","contains","data","getData","questionvars","partvars","Array","fill","varname","match","replace","index","parseInt","parttexts","i","pendingPromise","Pending","renderedTexts","methodname","args","questiontext","globalvars","showRenderedQuestionAndParts","err","Notification","exception","resolve","div","innerHTML","question","text","parts","MathJax","version","typesetPromise","Hub","Queue","triggerMathJax","localizeColumnGroupNames","partStringRequests","key","component","param","strings","String","get_strings","columnGroups","querySelectorAll","group","getAttribute","setTitleForColumnGroup","setAttribute","querySelector","innerText","fillTable","allRows","rowCounter","thisRow","thisVar","randomvars","name","partCounter","thisPart","findTable","setData","init","noParts","instantiate","howMany","localvars","answers","response","n","status","get_string","message","firstRow","calcOptions","bottomCalc","bottomCalcFormatter","cell","getValue","columnDescription","randomColumns","column","globalColumns","partColumns","partIndex","part","thisPartsColumns","vars","setColumns","holders","holder","style","display","prepareTableColumns"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/instantiation.js b/amd/src/instantiation.js index 11452007..c1ea4e34 100644 --- a/amd/src/instantiation.js +++ b/amd/src/instantiation.js @@ -416,8 +416,9 @@ const instantiate = async() => { } }])[0]; if (response.status == 'error') { - let str = await String.get_string('previewerror', 'qtype_formulas'); - document.getElementById('qtextpreview_display').innerHTML = `${str}
${response.message}`; + document.getElementById('qtextpreview_display').innerHTML = await String.get_string( + 'previewerror', 'qtype_formulas', response.message + ); } else { document.getElementById('qtextpreview_display').innerHTML = ''; prepareTableColumns(response.data); diff --git a/classes/external/instantiation.php b/classes/external/instantiation.php index f853d8be..deb17e03 100644 --- a/classes/external/instantiation.php +++ b/classes/external/instantiation.php @@ -137,8 +137,13 @@ protected static function fetch_one_instance($context, $parsedglobalvars, $parse $partevaluator->evaluate($parsedlocalvars[$i]); } $localvars[$i] = self::variable_context_to_array($partevaluator->export_variable_context()); - // Finally, evaluate the answer(s). - $answers[$i] = $partevaluator->evaluate($parsedanswers[$i])[0]; + // Finally, evaluate the answer(s). If the model answer was empty, we cannot move on. As we + // are in a try-catch, we simply throw an exception with the appropriate error message. + $evaluationresult = $partevaluator->evaluate($parsedanswers[$i]); + if (empty($evaluationresult)) { + throw new Exception(get_string('error_answer_missing_in_part', 'qtype_formulas', $i + 1)); + } + $answers[$i] = reset($evaluationresult); } } catch (Exception $e) { return $e->getMessage(); @@ -512,7 +517,7 @@ public static function render_question_text($questiontext, $parttexts, $globalva $renderedquestiontext = $evaluator->substitute_variables_in_text($params['questiontext']); } catch (Exception $e) { return [ - 'question' => get_string('previewerror', 'qtype_formulas') . ' ' . $e->getMessage(), + 'question' => get_string('previewerror', 'qtype_formulas', $e->getMessage()), 'parts' => [], ]; } @@ -529,7 +534,7 @@ public static function render_question_text($questiontext, $parttexts, $globalva $renderedparttexts[$i] = $partevaluator->substitute_variables_in_text($params['parttexts'][$i]); } catch (Exception $e) { return [ - 'question' => get_string('previewerror', 'qtype_formulas') . ' ' . $e->getMessage(), + 'question' => get_string('previewerror', 'qtype_formulas', $e->getMessage()), 'parts' => [], ]; } diff --git a/lang/en/qtype_formulas.php b/lang/en/qtype_formulas.php index 0b2706d8..dfd45b8a 100644 --- a/lang/en/qtype_formulas.php +++ b/lang/en/qtype_formulas.php @@ -86,6 +86,7 @@ $string['error_algebraic_relerr'] = 'Relative error (_relerr) cannot be used with answer type algebraic formula.'; $string['error_algvar_numbers'] = 'Algebraic variables can only be initialized with a list of numbers.'; $string['error_answer_missing'] = 'No answer has been defined.'; +$string['error_answer_missing_in_part'] = 'No answer has been defined for part {$a}.'; $string['error_answerbox_duplicate'] = 'Answer box placeholders must be unique, found second instance of {$a}.'; $string['error_bitshift_integer'] = 'Bit shift operator should only be used with integers.'; $string['error_bitshift_negative'] = 'Bit shift by negative number {$a} is not allowed.'; @@ -288,7 +289,7 @@ Students are required to use the same input format. Examples:
1 m
0.1 m^2
20 m s^(-1)
400 kg m/s
100 kW
'; -$string['previewerror'] = 'No preview available. Check your definition of random variables, global variables, parts\' local variables and answers. Original error message:'; +$string['previewerror'] = 'No preview available. Check your definition of random variables, global variables, parts\' local variables and answers. Original error message: {$a}'; $string['privacy:metadata'] = 'The Formulas question type plugin does not store any personal data.'; $string['qtextpreview'] = 'Preview'; $string['questiontext'] = 'Question text'; diff --git a/tests/externallib_test.php b/tests/externallib_test.php index 2ee82318..34ebfed8 100644 --- a/tests/externallib_test.php +++ b/tests/externallib_test.php @@ -306,6 +306,24 @@ public function test_render_question_text($expected, $input): void { */ public static function provide_instantiation_data(): array { return [ + [ + ['status' => 'error', 'message' => "No answer has been defined for part 1."], + [ + 'n' => 1, 'randomvars' => '', 'globalvars' => '', + 'localvars' => [''], 'answers' => [''], + ], + ], + [ + ['status' => 'ok', 'data' => [[ + 'randomvars' => [], + 'globalvars' => [], + 'parts' => [[['name' => '_0', 'value' => '1']]], + ]]], + [ + 'n' => 1, 'randomvars' => '', 'globalvars' => '', + 'localvars' => [''], 'answers' => ['1'], + ], + ], [ ['status' => 'ok', 'data' => [[ 'randomvars' => [],