Commit 25215446 authored by 0815-xyz's avatar 0815-xyz
Browse files

Initial commit

parents
Moodle Adaptive Test Activity - Project KNIGHT at HFT Stuttgart
=================================================================
This is a fork of the upstream plugin mod_adaptivequiz version 2.3.6 (2024041900) for which you will find the original README below.
The modifications in this version 2.3.6dev (2024041901) were made during the KNIGHT project (2019-2025) and inspired by teacher feedback and our students' experiences utilizing the plugin in CBL-based courses. They encompass the following:
* We improved the usability for partial credit questions by introducing a threshold in the settings, that allows the teacher to adjust the point at which the questions are considered to have been answered correctly. This modification had the objective of utilising existing question pools for adaptive testing.
* An additional item was incorporated into the settings, providing educators with the option to permit students to review their own attempts by accessing the question details tab of the corresponding attempt review. This modification was requested by our students, with the intention of facilitating their learning by taking into account the teacher feedback on incorrect responses.
* We also implemented a notification flag for admins and teachers, which serves to remind them of the necessity to review the question analysis page. This serves the purpose of quality assurance on the basis of ethical considerations regarding adaptive tests, as it ensures that the difficulty levels assigned to the questions are regularly checked for validity. The frequency of these reminders is tied to the number of student attempts and can be adjusted (or turned off) in the activity settings.
___________________________________________________________________________________
# README for the mod_adaptivequiz plugin, version 2.3.6 (2024041900)
The Adaptive Test activity enables a teacher to create tests that efficiently measure
the takers' abilities. Adaptive tests are comprised of questions selected from the
question bank that are tagged with a score of their difficulty. The questions are
chosen to match the estimated ability level of the current test-taker. If the
test-taker succeeds on a question, a more challenging question is presented next. If
the test-taker answers a question incorrectly, a less-challenging question is
presented next. This technique will develop into a sequence of questions converging
on the test-taker's effective ability level. The test stops when the test-taker's
ability is determined to the required accuracy.
The Adaptive Test activity uses the ["Practical Adaptive Testing CAT Algorithm" by
B.D. Wright][1] published in *Rasch Measurement Transactions, 1988, 2:2 p.24* and
discussed in John Linacre's ["Computer-Adaptive Testing: A Methodology Whose Time Has
Come."][2] *MESA Memorandum No. 69 (2000)*.
[1]: http://www.rasch.org/rmt/rmt22g.htm
[2]: http://www.rasch.org/memo69.pdf
This Moodle activity module was originally created as a collaborative effort between [Middlebury
College][3] and [Remote Learner][4]. The current repository was forked from
[https://github.com/middlebury/moodle-mod_adaptivequiz][5].
The current branch of the repository is compatible with Moodle versions from 4.1 to 4.3.
[3]: http://www.middlebury.edu/
[4]: http://remote-learner.net/
[5]: https://github.com/middlebury/moodle-mod_adaptivequiz
The Question Bank
-----------------
To begin with, questions to be used with this activity are added or imported into
Moodle's question bank. Only questions that can automatically be graded may be used.
As well, questions should not award partial credit. The questions can be placed in
one or more categories.
This activity is best suited to determining an ability measure along a unidimensional
scale. While the scale can be very broad, the questions must all provide a measure of
ability or aptitude on the same scale. In a placement test for example, questions low
on the scale that novices are able to answer correctly should also be answerable by
experts, while questions higher on the scale should only be answerable by experts or
a lucky guess. Questions that do not discriminate between takers of different
abilities on will make the test ineffective and may provide inconclusive results.
Take for example a language placement test. Low-difficulty vocabulary and
reading-comprehension questions would likely be answerable by all but the most novice
test-takers. Likewise, high-difficulty questions involving advanced grammatical
constructs and nuanced reading-comprehension would be likely only be correctly
answered by advanced, high-level test-takers. Such questions would all be good
candidates for usage in an Adaptive Test. In contrast, a question like "Is 25¥ a good
price for a sandwich?" would not measure language ability but rather local knowledge
and would be as likely to be answered correctly by a novice speaker who has recently
been to China as it would be answered incorrectly by an advanced speaker who comes
from Taiwan -- where a different currency is used. Such questions should not be
included in the question-pool.
Questions must be tagged with a 'difficulty score' using the format
'adpq\_*n*' where *n* is a positive integer, e.g. 'adpq\_1' or 'adpq\_57'. The range
of the scale is arbitrary (e.g. 1-10, 0-99, 1-1000), but should have enough levels to
distinguish between
question difficulties.
The Testing Process
-------------------
The Adaptive Test activity is configured with a fixed starting level. The test will
begin by presenting the test-taker with a random question from that starting level.
As described in [Linacre (2000)][2], it often makes sense to have the starting level
be in the lower part of the difficulty range so that most test-takers get to answer
at least one of the first few questions correctly, helping their moral.
After the test-taker submits their answer, the system calculates the target question
difficulty it will select next. If the last question was answered correctly, the next
question will be harder; if the last question was answered incorrectly, the next
question will be easier. The system also calculates a measure of the test-taker's
ability and the standard error for that measure. A next random question at or near
the target difficulty is selected and presented to the user.
This process of alternating harder questions following correct answers and easier
questions following wrong answers continues until one of the stopping conditions is
met. The possible stopping conditions are as follows:
* There are no remaining easier questions to ask after a wrong answer.
* There are no remaining harder questions to ask after a correct answer.
* The standard error in the measure has become precise enough to stop.
* The maximum number of questions has been exceeded.
Test Parameters and Operation
==============================
The primary parameters for tuning the operation of the test are:
* The starting level
* The minimum number of questions
* The maximum number of questions
* The standard error to stop
Relationship between maximum number of questions and Standard Error
--------------------------------------------------------------------
As discussed in [Wright (1988)][1], the formula for calculating the standard error is
given by:
Standard Error (± logits) = sqrt((R+W)/(R*W))
where `R` is the number of right answers and `W` is the number of wrong answers. This
value is on a [logit](http://en.wikipedia.org/wiki/Logit) scale, so we can apply the
inverse-logit function to convert it to an percentage scale:
Standard Error (± %) = ((1 / ( 1 + e^( -1 * sqrt((R+W)/(R*W)) ) ) ) - 0.5) * 100
Looking at the Standard Error function, it is important to note that it depends only
on the difference between the number of right and wrong answers and the total number
of answers, not on any other features such as which answers were right and which
answers were wrong. For a given number of questions asked, the Standard Error will be
smallest when half the answers are right and half are wrong. From this, we can deduce
the minimum standard error possible to achieve for any number of questions asked:
* 10 questions (5 right, 5 wrong) → Minimum Standard Error = ± 15.30%
* 20 questions (10 right, 10 wrong) → Minimum Standard Error = ± 11.00%
* 30 questions (15 right, 15 wrong) → Minimum Standard Error = ± 9.03%
* 40 questions (20 right, 20 wrong) → Minimum Standard Error = ± 7.84%
* 50 questions (25 right, 25 wrong) → Minimum Standard Error = ± 7.02%
* 60 questions (30 right, 30 wrong) → Minimum Standard Error = ± 6.42%
* 70 questions (35 right, 35 wrong) → Minimum Standard Error = ± 5.95%
* 80 questions (40 right, 40 wrong) → Minimum Standard Error = ± 5.57%
* 90 questions (45 right, 45 wrong) → Minimum Standard Error = ± 5.25%
* 100 questions (50 right, 50 wrong) → Minimum Standard Error = ± 4.98%
* 110 questions (55 right, 55 wrong) → Minimum Standard Error = ± 4.75%
* 120 questions (60 right, 60 wrong) → Minimum Standard Error = ± 4.55%
* 130 questions (65 right, 65 wrong) → Minimum Standard Error = ± 4.37%
* 140 questions (70 right, 70 wrong) → Minimum Standard Error = ± 4.22%
* 150 questions (75 right, 75 wrong) → Minimum Standard Error = ± 4.07%
* 160 questions (80 right, 80 wrong) → Minimum Standard Error = ± 3.94%
* 170 questions (85 right, 85 wrong) → Minimum Standard Error = ± 3.83%
* 180 questions (90 right, 90 wrong) → Minimum Standard Error = ± 3.72%
* 190 questions (95 right, 95 wrong) → Minimum Standard Error = ± 3.62%
* 200 questions (100 right, 100 wrong) → Minimum Standard Error = ± 3.53%
What this listing indicates is that for a test configured with a maximum of 50
questions and a "standard error to stop" of 7%, the maximum number of questions will
always be encountered first and stop the test. Conversely, if you are looking for a
standard error of 5% or better, the test must ask at least 100 questions.
Note that these are best-case scenarios for the number of questions asked. If a
test-taker answers a lopsided run of questions right or wrong the test will require
more questions to reach a target standard of error.
Minimum number of questions
----------------------------
For most purposes this value can be set to `1` since the standard of error to stop
will generally set a base-line for the number of questions required. This could be
configured to be greater than the minimum number of questions needed to achieve the
standard of error to stop if you wish to ensure that all test-takers answer
additional questions.
Starting level
---------------
As mentioned above, this will usually be set in the lower part of the difficulty
range (about 1/3 of the way up from the bottom) so that most test takers will be able
to answer one of the first two questions correctly and get a moral boost from their
correct answers. If the starting level is too high, low-ability users would be asked
several questions they can't answer before the test begins asking them questions at a
level they can answer.
Scoring
========
As discussed in [Wright (1988)][1], the formula for calculating the ability measure is given by:
Ability Measure = H/L + ln(R/W)
where `H` is the sum of all question difficulties answered, `L` is the number of
questions answered, `R` is the number of right answers, and `W` is the number of
wrong answers.
Note that this measure is not affected by the order of answers, just the total
difficulty and number of right and wrong answers. This measure is dependent on the
test algorithm presenting alternating easier/harder questions as the user answers
wrong/right and may not be applicable to other algorithms. In practice, this means
that the ability measure should not be greatly affected by a small number of spurious
right or wrong answers.
As discussed in [Linacre (2000)][2], the ability measure of the test taker aligns
with the question-difficulty at which the test-taker has a 50% probability of
answering a question correctly.
For example, given a test with levels 1-10 and a test-taker that answered every
question 5 and below correctly and every question 6 and up wrong, the test-taker's
ability measure would fall close to 5.5.
Remember that the ability measure does have error associated with it. Be sure to take the standard error amount into account
when acting on the score.
define("mod_adaptivequiz/attempt_administration_chart_dataset_config",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={orderWeights:{ABILITY_MEASURE:10,ADMINISTERED_DIFFICULTY:20,TARGET_DIFFICULTY:30,STANDARD_ERROR_BORDER:40,STANDARD_ERROR_PERCENT:50,CORRECT_WRONG_FLAG:60},indices:{TARGET_DIFFICULTY:0,ADMINISTERED_DIFFICULTY:1,CORRECT_WRONG_FLAG:2,ABILITY_MEASURE:3,STANDARD_ERROR_MAX:4,STANDARD_ERROR_MIN:5,STANDARD_ERROR_PERCENT:6}},_exports.default}));
//# sourceMappingURL=attempt_administration_chart_dataset_config.min.js.map
\ No newline at end of file
{"version":3,"file":"attempt_administration_chart_dataset_config.min.js","sources":["../src/attempt_administration_chart_dataset_config.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Defines order and indices for datasets.\n *\n * @module mod_adaptivequiz/attempt_administration_chart_dataset_config\n * @copyright 2024 Vitaly Potenko <potenkov@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n orderWeights: {\n ABILITY_MEASURE: 10,\n ADMINISTERED_DIFFICULTY: 20,\n TARGET_DIFFICULTY: 30,\n STANDARD_ERROR_BORDER: 40,\n STANDARD_ERROR_PERCENT: 50,\n CORRECT_WRONG_FLAG: 60,\n },\n indices: {\n TARGET_DIFFICULTY: 0,\n ADMINISTERED_DIFFICULTY: 1,\n CORRECT_WRONG_FLAG: 2,\n ABILITY_MEASURE: 3,\n STANDARD_ERROR_MAX: 4,\n STANDARD_ERROR_MIN: 5,\n STANDARD_ERROR_PERCENT: 6,\n },\n};\n"],"names":["orderWeights","ABILITY_MEASURE","ADMINISTERED_DIFFICULTY","TARGET_DIFFICULTY","STANDARD_ERROR_BORDER","STANDARD_ERROR_PERCENT","CORRECT_WRONG_FLAG","indices","STANDARD_ERROR_MAX","STANDARD_ERROR_MIN"],"mappings":"8MAuBe,CACXA,aAAc,CACVC,gBAAiB,GACjBC,wBAAyB,GACzBC,kBAAmB,GACnBC,sBAAuB,GACvBC,uBAAwB,GACxBC,mBAAoB,IAExBC,QAAS,CACLJ,kBAAmB,EACnBD,wBAAyB,EACzBI,mBAAoB,EACpBL,gBAAiB,EACjBO,mBAAoB,EACpBC,mBAAoB,EACpBJ,uBAAwB"}
\ No newline at end of file
/**
* Customized output for the attempt administration chart.
*
* The class overrides some definitions from core/chart_output_chartjs for custom output.
*
* @module mod_adaptivequiz/attempt_administration_chart_output
* @copyright 2024 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("mod_adaptivequiz/attempt_administration_chart_output",["core/chart_output_chartjs","core/chartjs","mod_adaptivequiz/attempt_administration_chart_dataset_config"],(function(Output,Chartjs,DatasetConfig){const tooltipItemsFilter=function(tooltipItem){return!(tooltipItem.datasetIndex===DatasetConfig.indices.STANDARD_ERROR_MAX||tooltipItem.datasetIndex===DatasetConfig.indices.STANDARD_ERROR_MIN)},afterTooltipItemLabel=function(tooltipItem){if(tooltipItem.datasetIndex!==DatasetConfig.indices.ABILITY_MEASURE&&tooltipItem.datasetIndex!==DatasetConfig.indices.ADMINISTERED_DIFFICULTY)return"";if(tooltipItem.datasetIndex===DatasetConfig.indices.ABILITY_MEASURE){const stdErrorSeries=this._chart.getSeries()[DatasetConfig.indices.STANDARD_ERROR_PERCENT],stdErrorValue=stdErrorSeries.getValues()[tooltipItem.dataIndex];return"".concat(stdErrorSeries.getLabel(),": ").concat(stdErrorValue)}const rightWrongSeries=this._chart.getSeries()[DatasetConfig.indices.CORRECT_WRONG_FLAG],rightWrongValue=rightWrongSeries.getValues()[tooltipItem.dataIndex];return"".concat(rightWrongSeries.getLabel(),": ").concat(rightWrongValue)},legendConfig=function(){return{labels:{generateLabels(chart){let labels=Chartjs.defaults.plugins.legend.labels.generateLabels(chart);const stdErrorLabelText=labels.find((label=>label.datasetIndex===DatasetConfig.indices.STANDARD_ERROR_PERCENT)).text;labels=labels.filter((label=>!label.hidden&&label.datasetIndex!==DatasetConfig.indices.STANDARD_ERROR_MIN)),labels.sort(((label1,label2)=>label1.datasetIndex-label2.datasetIndex));const stdErrorMaxIndex=labels.findIndex((label=>label.datasetIndex===DatasetConfig.indices.STANDARD_ERROR_MAX));return labels[stdErrorMaxIndex].text=stdErrorLabelText,labels[stdErrorMaxIndex].strokeStyle="#ffcce0",labels[stdErrorMaxIndex].fillStyle="#ffcce0",labels}},onClick:function(){return!1}}};function AttemptAdministrationChartOutput(){Output.apply(this,arguments)}return AttemptAdministrationChartOutput.prototype=Object.create(Output.prototype),AttemptAdministrationChartOutput.prototype._makeConfig=function(){let config=Output.prototype._makeConfig.apply(this,arguments);return config.data.datasets[DatasetConfig.indices.ABILITY_MEASURE].order=DatasetConfig.orderWeights.ABILITY_MEASURE,config.data.datasets[DatasetConfig.indices.TARGET_DIFFICULTY].order=DatasetConfig.orderWeights.TARGET_DIFFICULTY,config.data.datasets[DatasetConfig.indices.ADMINISTERED_DIFFICULTY].order=DatasetConfig.orderWeights.ADMINISTERED_DIFFICULTY,config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].order=DatasetConfig.orderWeights.STANDARD_ERROR_BORDER,config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].order=DatasetConfig.orderWeights.STANDARD_ERROR_BORDER,config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].pointRadius=0,config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].pointRadius=0,config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].pointStyle=!1,config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].pointStyle=!1,config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].showLine=!1,config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].showLine=!1,config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_PERCENT].hidden=!0,config.data.datasets[DatasetConfig.indices.CORRECT_WRONG_FLAG].hidden=!0,config.options.plugins.tooltip.filter=tooltipItemsFilter,config.options.plugins.tooltip.callbacks.afterLabel=afterTooltipItemLabel.bind(this),config.options.plugins.tooltip.itemSort=function(tooltip1,tooltip2){return tooltip1.datasetIndex-tooltip2.datasetIndex},config.options.plugins.legend=legendConfig(),config},AttemptAdministrationChartOutput}));
//# sourceMappingURL=attempt_administration_chart_output.min.js.map
\ No newline at end of file
{"version":3,"file":"attempt_administration_chart_output.min.js","sources":["../src/attempt_administration_chart_output.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Customized output for the attempt administration chart.\n *\n * The class overrides some definitions from core/chart_output_chartjs for custom output.\n *\n * @module mod_adaptivequiz/attempt_administration_chart_output\n * @copyright 2024 Vitaly Potenko <potenkov@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'core/chart_output_chartjs',\n 'core/chartjs',\n 'mod_adaptivequiz/attempt_administration_chart_dataset_config'\n], function(\n Output,\n Chartjs,\n DatasetConfig\n) {\n\n /**\n * Used to set the color as a hex code to avoid inconsistencies from Chartjs when displaying the color with opacity specified.\n */\n const STANDARD_ERROR_LABEL_COLOR = '#ffcce0';\n\n /**\n * A filter callback for tooltip items.\n *\n * @param {Object} tooltipItem\n * @return {Boolean}\n */\n const tooltipItemsFilter = function (tooltipItem) {\n return !(tooltipItem.datasetIndex === DatasetConfig.indices.STANDARD_ERROR_MAX\n || tooltipItem.datasetIndex === DatasetConfig.indices.STANDARD_ERROR_MIN);\n };\n\n /**\n * A callback to add text after a tooltip item.\n *\n * @param {Object} tooltipItem\n * @return {String}\n */\n const afterTooltipItemLabel = function (tooltipItem) {\n // Show extra text only after the ability measure and administered difficulty items.\n if (!(tooltipItem.datasetIndex === DatasetConfig.indices.ABILITY_MEASURE\n || tooltipItem.datasetIndex === DatasetConfig.indices.ADMINISTERED_DIFFICULTY)) {\n\n return '';\n }\n\n // If this is the ability measure item.\n if (tooltipItem.datasetIndex === DatasetConfig.indices.ABILITY_MEASURE) {\n // Reach out to the standard error data.\n const stdErrorSeries = this._chart.getSeries()[DatasetConfig.indices.STANDARD_ERROR_PERCENT];\n const stdErrorValue = stdErrorSeries.getValues()[tooltipItem.dataIndex];\n\n return `${stdErrorSeries.getLabel()}: ${stdErrorValue}`;\n }\n\n // The rest case - administered difficulty item.\n\n // Reach out to the right/wrong data.\n const rightWrongSeries = this._chart.getSeries()[DatasetConfig.indices.CORRECT_WRONG_FLAG];\n const rightWrongValue = rightWrongSeries.getValues()[tooltipItem.dataIndex];\n\n return `${rightWrongSeries.getLabel()}: ${rightWrongValue}`;\n };\n\n /**\n * Returns part of the config to set up the legend.\n *\n * @return {Object}\n */\n const legendConfig = function () {\n return {\n labels: {\n generateLabels(chart) {\n let labels = Chartjs.defaults.plugins.legend.labels.generateLabels(chart);\n\n // Store the standard error label for future use.\n const stdErrorLabelText = labels.find((label) =>\n label.datasetIndex === DatasetConfig.indices.STANDARD_ERROR_PERCENT).text;\n\n // Remove everything hidden and one of the standard error labels.\n labels = labels.filter((label) =>\n !label.hidden && label.datasetIndex !== DatasetConfig.indices.STANDARD_ERROR_MIN);\n\n // Order by dataset index.\n labels.sort((label1, label2) => label1.datasetIndex - label2.datasetIndex);\n\n // Convert one of the standard error labels to a proper one.\n const stdErrorMaxIndex = labels.findIndex((label) =>\n label.datasetIndex === DatasetConfig.indices.STANDARD_ERROR_MAX);\n labels[stdErrorMaxIndex].text = stdErrorLabelText;\n labels[stdErrorMaxIndex].strokeStyle = STANDARD_ERROR_LABEL_COLOR;\n labels[stdErrorMaxIndex].fillStyle = STANDARD_ERROR_LABEL_COLOR;\n\n return labels;\n }\n },\n onClick: function () {\n return false;\n }\n };\n };\n\n /**\n * Output for the attempt administration chart.\n *\n * @class\n * @extends {module:core/chart_output_chartjs}\n */\n function AttemptAdministrationChartOutput() {\n Output.apply(this, arguments);\n }\n AttemptAdministrationChartOutput.prototype = Object.create(Output.prototype);\n\n /**\n * Overrides config definition to add more custom features.\n *\n * @protected\n * @override\n * @return {Object}\n */\n AttemptAdministrationChartOutput.prototype._makeConfig = function () {\n let config = Output.prototype._makeConfig.apply(this, arguments);\n\n // Define draw order.\n config.data.datasets[DatasetConfig.indices.ABILITY_MEASURE].order = DatasetConfig.orderWeights.ABILITY_MEASURE;\n config.data.datasets[DatasetConfig.indices.TARGET_DIFFICULTY].order = DatasetConfig.orderWeights.TARGET_DIFFICULTY;\n config.data.datasets[DatasetConfig.indices.ADMINISTERED_DIFFICULTY].order =\n DatasetConfig.orderWeights.ADMINISTERED_DIFFICULTY;\n config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].order = DatasetConfig.orderWeights.STANDARD_ERROR_BORDER;\n config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].order = DatasetConfig.orderWeights.STANDARD_ERROR_BORDER;\n\n // Hide lines and points for standard error min/max datasets.\n\n // In Chart.js version prior to 4.2.1 points visibility can be controlled with the 'pointRadius' property.\n config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].pointRadius = 0;\n config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].pointRadius = 0;\n // For Chart.js since 4.2.1.\n config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].pointStyle = false;\n config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].pointStyle = false;\n\n config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].showLine = false;\n config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].showLine = false;\n\n // Hide entire datasets with standard error percentages and right/wrong flags.\n config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_PERCENT].hidden = true;\n config.data.datasets[DatasetConfig.indices.CORRECT_WRONG_FLAG].hidden = true;\n\n // Tooltip.\n config.options.plugins.tooltip.filter = tooltipItemsFilter;\n config.options.plugins.tooltip.callbacks.afterLabel = afterTooltipItemLabel.bind(this);\n config.options.plugins.tooltip.itemSort = function (tooltip1, tooltip2) {\n return tooltip1.datasetIndex - tooltip2.datasetIndex;\n };\n\n // Legend.\n config.options.plugins.legend = legendConfig();\n\n return config;\n };\n\n return AttemptAdministrationChartOutput;\n});\n"],"names":["define","Output","Chartjs","DatasetConfig","tooltipItemsFilter","tooltipItem","datasetIndex","indices","STANDARD_ERROR_MAX","STANDARD_ERROR_MIN","afterTooltipItemLabel","ABILITY_MEASURE","ADMINISTERED_DIFFICULTY","stdErrorSeries","this","_chart","getSeries","STANDARD_ERROR_PERCENT","stdErrorValue","getValues","dataIndex","getLabel","rightWrongSeries","CORRECT_WRONG_FLAG","rightWrongValue","legendConfig","labels","generateLabels","chart","defaults","plugins","legend","stdErrorLabelText","find","label","text","filter","hidden","sort","label1","label2","stdErrorMaxIndex","findIndex","strokeStyle","fillStyle","onClick","AttemptAdministrationChartOutput","apply","arguments","prototype","Object","create","_makeConfig","config","data","datasets","order","orderWeights","TARGET_DIFFICULTY","STANDARD_ERROR_BORDER","pointRadius","pointStyle","showLine","options","tooltip","callbacks","afterLabel","bind","itemSort","tooltip1","tooltip2"],"mappings":";;;;;;;;;AAwBAA,8DAAO,CACH,4BACA,eACA,iEACD,SACCC,OACAC,QACAC,qBAcMC,mBAAqB,SAAUC,qBACxBA,YAAYC,eAAiBH,cAAcI,QAAQC,oBACrDH,YAAYC,eAAiBH,cAAcI,QAAQE,qBASxDC,sBAAwB,SAAUL,gBAE9BA,YAAYC,eAAiBH,cAAcI,QAAQI,iBAClDN,YAAYC,eAAiBH,cAAcI,QAAQK,8BAE/C,MAIPP,YAAYC,eAAiBH,cAAcI,QAAQI,gBAAiB,OAE9DE,eAAiBC,KAAKC,OAAOC,YAAYb,cAAcI,QAAQU,wBAC/DC,cAAgBL,eAAeM,YAAYd,YAAYe,2BAEnDP,eAAeQ,wBAAeH,qBAMtCI,iBAAmBR,KAAKC,OAAOC,YAAYb,cAAcI,QAAQgB,oBACjEC,gBAAkBF,iBAAiBH,YAAYd,YAAYe,2BAEvDE,iBAAiBD,wBAAeG,kBAQxCC,aAAe,iBACV,CACHC,OAAQ,CACJC,eAAeC,WACPF,OAASxB,QAAQ2B,SAASC,QAAQC,OAAOL,OAAOC,eAAeC,aAG7DI,kBAAoBN,OAAOO,MAAMC,OACnCA,MAAM5B,eAAiBH,cAAcI,QAAQU,yBAAwBkB,KAGzET,OAASA,OAAOU,QAAQF,QACnBA,MAAMG,QAAUH,MAAM5B,eAAiBH,cAAcI,QAAQE,qBAGlEiB,OAAOY,MAAK,CAACC,OAAQC,SAAWD,OAAOjC,aAAekC,OAAOlC,qBAGvDmC,iBAAmBf,OAAOgB,WAAWR,OACvCA,MAAM5B,eAAiBH,cAAcI,QAAQC,4BACjDkB,OAAOe,kBAAkBN,KAAOH,kBAChCN,OAAOe,kBAAkBE,YAvEN,UAwEnBjB,OAAOe,kBAAkBG,UAxEN,UA0EZlB,SAGfmB,QAAS,kBACE,cAWVC,mCACL7C,OAAO8C,MAAMjC,KAAMkC,kBAEvBF,iCAAiCG,UAAYC,OAAOC,OAAOlD,OAAOgD,WASlEH,iCAAiCG,UAAUG,YAAc,eACjDC,OAASpD,OAAOgD,UAAUG,YAAYL,MAAMjC,KAAMkC,kBAGtDK,OAAOC,KAAKC,SAASpD,cAAcI,QAAQI,iBAAiB6C,MAAQrD,cAAcsD,aAAa9C,gBAC/F0C,OAAOC,KAAKC,SAASpD,cAAcI,QAAQmD,mBAAmBF,MAAQrD,cAAcsD,aAAaC,kBACjGL,OAAOC,KAAKC,SAASpD,cAAcI,QAAQK,yBAAyB4C,MAChErD,cAAcsD,aAAa7C,wBAC/ByC,OAAOC,KAAKC,SAASpD,cAAcI,QAAQC,oBAAoBgD,MAAQrD,cAAcsD,aAAaE,sBAClGN,OAAOC,KAAKC,SAASpD,cAAcI,QAAQE,oBAAoB+C,MAAQrD,cAAcsD,aAAaE,sBAKlGN,OAAOC,KAAKC,SAASpD,cAAcI,QAAQC,oBAAoBoD,YAAc,EAC7EP,OAAOC,KAAKC,SAASpD,cAAcI,QAAQE,oBAAoBmD,YAAc,EAE7EP,OAAOC,KAAKC,SAASpD,cAAcI,QAAQC,oBAAoBqD,YAAa,EAC5ER,OAAOC,KAAKC,SAASpD,cAAcI,QAAQE,oBAAoBoD,YAAa,EAE5ER,OAAOC,KAAKC,SAASpD,cAAcI,QAAQC,oBAAoBsD,UAAW,EAC1ET,OAAOC,KAAKC,SAASpD,cAAcI,QAAQE,oBAAoBqD,UAAW,EAG1ET,OAAOC,KAAKC,SAASpD,cAAcI,QAAQU,wBAAwBoB,QAAS,EAC5EgB,OAAOC,KAAKC,SAASpD,cAAcI,QAAQgB,oBAAoBc,QAAS,EAGxEgB,OAAOU,QAAQjC,QAAQkC,QAAQ5B,OAAShC,mBACxCiD,OAAOU,QAAQjC,QAAQkC,QAAQC,UAAUC,WAAaxD,sBAAsByD,KAAKrD,MACjFuC,OAAOU,QAAQjC,QAAQkC,QAAQI,SAAW,SAAUC,SAAUC,iBACnDD,SAAS/D,aAAegE,SAAShE,cAI5C+C,OAAOU,QAAQjC,QAAQC,OAASN,eAEzB4B,QAGJP"}
\ No newline at end of file
/**
* Customized output for the attempt administration chart data.
*
* @module mod_adaptivequiz/attempt_administration_chart_output_htmltable
* @copyright 2024 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("mod_adaptivequiz/attempt_administration_chart_output_htmltable",["jquery","core/chart_output_htmltable","mod_adaptivequiz/attempt_administration_chart_dataset_config"],(function($,OutputTable,DatasetConfig){function AttemptAdministrationChartOutputTable(){OutputTable.apply(this,arguments)}return AttemptAdministrationChartOutputTable.prototype=Object.create(OutputTable.prototype),AttemptAdministrationChartOutputTable.prototype._makeTable=function(){let tbl=OutputTable.prototype._makeTable.apply(this,arguments);const dataIndicesToRemove=[DatasetConfig.indices.STANDARD_ERROR_MAX,DatasetConfig.indices.STANDARD_ERROR_MIN],selectorsToRemove=dataIndicesToRemove.flatMap((dataIndexToRemove=>["th:nth-child(".concat(dataIndexToRemove+2,")"),"td:nth-child(".concat(dataIndexToRemove+2,")")]));return tbl.find(selectorsToRemove.join()).remove(),tbl},AttemptAdministrationChartOutputTable}));
//# sourceMappingURL=attempt_administration_chart_output_htmltable.min.js.map
\ No newline at end of file
{"version":3,"file":"attempt_administration_chart_output_htmltable.min.js","sources":["../src/attempt_administration_chart_output_htmltable.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Customized output for the attempt administration chart data.\n *\n * @module mod_adaptivequiz/attempt_administration_chart_output_htmltable\n * @copyright 2024 Vitaly Potenko <potenkov@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/chart_output_htmltable',\n 'mod_adaptivequiz/attempt_administration_chart_dataset_config'\n], function (\n $,\n OutputTable,\n DatasetConfig\n) {\n\n /**\n * Output for the attempt administration chart data.\n *\n * @class\n * @extends {module:core/chart_output_htmltable}\n */\n function AttemptAdministrationChartOutputTable() {\n OutputTable.apply(this, arguments);\n }\n AttemptAdministrationChartOutputTable.prototype = Object.create(OutputTable.prototype);\n\n /**\n * Overrides building the table.\n *\n * @override\n * @protected\n * @return {Object} Modified table node.\n */\n AttemptAdministrationChartOutputTable.prototype._makeTable = function() {\n let tbl = OutputTable.prototype._makeTable.apply(this, arguments);\n\n // Remove columns with standard error min/max.\n const dataIndicesToRemove = [DatasetConfig.indices.STANDARD_ERROR_MAX, DatasetConfig.indices.STANDARD_ERROR_MIN];\n\n const selectorsToRemove = dataIndicesToRemove.flatMap(\n (dataIndexToRemove) => [`th:nth-child(${dataIndexToRemove + 2})`, `td:nth-child(${dataIndexToRemove + 2})`]\n );\n\n tbl.find(selectorsToRemove.join()).remove();\n\n return tbl;\n };\n\n return AttemptAdministrationChartOutputTable;\n});\n"],"names":["define","$","OutputTable","DatasetConfig","AttemptAdministrationChartOutputTable","apply","this","arguments","prototype","Object","create","_makeTable","tbl","dataIndicesToRemove","indices","STANDARD_ERROR_MAX","STANDARD_ERROR_MIN","selectorsToRemove","flatMap","dataIndexToRemove","find","join","remove"],"mappings":";;;;;;;AAsBAA,wEAAO,CACH,SACA,8BACA,iEACD,SACCC,EACAC,YACAC,wBASSC,wCACLF,YAAYG,MAAMC,KAAMC,kBAE5BH,sCAAsCI,UAAYC,OAAOC,OAAOR,YAAYM,WAS5EJ,sCAAsCI,UAAUG,WAAa,eACrDC,IAAMV,YAAYM,UAAUG,WAAWN,MAAMC,KAAMC,iBAGjDM,oBAAsB,CAACV,cAAcW,QAAQC,mBAAoBZ,cAAcW,QAAQE,oBAEvFC,kBAAoBJ,oBAAoBK,SACzCC,mBAAsB,wBAAiBA,kBAAoB,8BAAsBA,kBAAoB,iBAG1GP,IAAIQ,KAAKH,kBAAkBI,QAAQC,SAE5BV,KAGJR"}
\ No newline at end of file
define("mod_adaptivequiz/attempt_answers_distribution_chart_manager",["exports","core/ajax","core/notification"],(function(_exports,_ajax,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Module to manage appearance of the answers distribution chart.
*
* @module mod_adaptivequiz/attempt_answers_distribution_chart_manager
* @copyright 2024 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification);_exports.init=(chartOutput,chartBar,userId,adaptiveQuizId)=>{document.querySelector('[data-action="set-answers-distribution-chart-stacked"]').addEventListener("change",(e=>{const setStacked=e.target.checked;chartBar.setStacked(setStacked),chartOutput.update(),_ajax.default.call([{methodname:"core_user_set_user_preferences",args:{preferences:[{name:"mod_adaptivequiz_answers_distribution_chart_settings_".concat(adaptiveQuizId),value:JSON.stringify({showstacked:setStacked}),userid:userId}]}}])[0].catch(_notification.default.exception)}))}}));
//# sourceMappingURL=attempt_answers_distribution_chart_manager.min.js.map
\ No newline at end of file
{"version":3,"file":"attempt_answers_distribution_chart_manager.min.js","sources":["../src/attempt_answers_distribution_chart_manager.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Module to manage appearance of the answers distribution chart.\n *\n * @module mod_adaptivequiz/attempt_answers_distribution_chart_manager\n * @copyright 2024 Vitaly Potenko <potenkov@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\n\n/**\n * Entry point of the module.\n *\n * @param {Object} chartOutput An instance of Output.\n * @param {Object} chartBar An instance of Bar.\n * @param {Number} userId Current user to set the preferences for.\n * @param {Number} adaptiveQuizId Adaptive quiz instance to set the preferences for.\n */\nexport const init = (chartOutput, chartBar, userId, adaptiveQuizId) => {\n let stackedFlagControl = document.querySelector('[data-action=\"set-answers-distribution-chart-stacked\"]');\n\n stackedFlagControl.addEventListener('change', (e) => {\n const setStacked = e.target.checked;\n\n chartBar.setStacked(setStacked);\n chartOutput.update();\n\n Ajax.call([{\n methodname: 'core_user_set_user_preferences',\n args: {\n preferences: [{\n name: `mod_adaptivequiz_answers_distribution_chart_settings_${adaptiveQuizId}`,\n value: JSON.stringify({\n showstacked: setStacked,\n }),\n userid: userId,\n }]\n }\n }])[0].catch(Notification.exception);\n });\n};\n"],"names":["chartOutput","chartBar","userId","adaptiveQuizId","document","querySelector","addEventListener","e","setStacked","target","checked","update","call","methodname","args","preferences","name","value","JSON","stringify","showstacked","userid","catch","Notification","exception"],"mappings":";;;;;;;wLAkCoB,CAACA,YAAaC,SAAUC,OAAQC,kBACvBC,SAASC,cAAc,0DAE7BC,iBAAiB,UAAWC,UACrCC,WAAaD,EAAEE,OAAOC,QAE5BT,SAASO,WAAWA,YACpBR,YAAYW,uBAEPC,KAAK,CAAC,CACPC,WAAY,iCACZC,KAAM,CACFC,YAAa,CAAC,CACVC,oEAA8Db,gBAC9Dc,MAAOC,KAAKC,UAAU,CAClBC,YAAaZ,aAEjBa,OAAQnB,aAGhB,GAAGoB,MAAMC,sBAAaC"}
\ No newline at end of file
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines order and indices for datasets.
*
* @module mod_adaptivequiz/attempt_administration_chart_dataset_config
* @copyright 2024 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
orderWeights: {
ABILITY_MEASURE: 10,
ADMINISTERED_DIFFICULTY: 20,
TARGET_DIFFICULTY: 30,
STANDARD_ERROR_BORDER: 40,
STANDARD_ERROR_PERCENT: 50,
CORRECT_WRONG_FLAG: 60,
},
indices: {
TARGET_DIFFICULTY: 0,
ADMINISTERED_DIFFICULTY: 1,
CORRECT_WRONG_FLAG: 2,
ABILITY_MEASURE: 3,
STANDARD_ERROR_MAX: 4,
STANDARD_ERROR_MIN: 5,
STANDARD_ERROR_PERCENT: 6,
},
};
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Customized output for the attempt administration chart.
*
* The class overrides some definitions from core/chart_output_chartjs for custom output.
*
* @module mod_adaptivequiz/attempt_administration_chart_output
* @copyright 2024 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'core/chart_output_chartjs',
'core/chartjs',
'mod_adaptivequiz/attempt_administration_chart_dataset_config'
], function(
Output,
Chartjs,
DatasetConfig
) {
/**
* Used to set the color as a hex code to avoid inconsistencies from Chartjs when displaying the color with opacity specified.
*/
const STANDARD_ERROR_LABEL_COLOR = '#ffcce0';
/**
* A filter callback for tooltip items.
*
* @param {Object} tooltipItem
* @return {Boolean}
*/
const tooltipItemsFilter = function (tooltipItem) {
return !(tooltipItem.datasetIndex === DatasetConfig.indices.STANDARD_ERROR_MAX
|| tooltipItem.datasetIndex === DatasetConfig.indices.STANDARD_ERROR_MIN);
};
/**
* A callback to add text after a tooltip item.
*
* @param {Object} tooltipItem
* @return {String}
*/
const afterTooltipItemLabel = function (tooltipItem) {
// Show extra text only after the ability measure and administered difficulty items.
if (!(tooltipItem.datasetIndex === DatasetConfig.indices.ABILITY_MEASURE
|| tooltipItem.datasetIndex === DatasetConfig.indices.ADMINISTERED_DIFFICULTY)) {
return '';
}
// If this is the ability measure item.
if (tooltipItem.datasetIndex === DatasetConfig.indices.ABILITY_MEASURE) {
// Reach out to the standard error data.
const stdErrorSeries = this._chart.getSeries()[DatasetConfig.indices.STANDARD_ERROR_PERCENT];
const stdErrorValue = stdErrorSeries.getValues()[tooltipItem.dataIndex];
return `${stdErrorSeries.getLabel()}: ${stdErrorValue}`;
}
// The rest case - administered difficulty item.
// Reach out to the right/wrong data.
const rightWrongSeries = this._chart.getSeries()[DatasetConfig.indices.CORRECT_WRONG_FLAG];
const rightWrongValue = rightWrongSeries.getValues()[tooltipItem.dataIndex];
return `${rightWrongSeries.getLabel()}: ${rightWrongValue}`;
};
/**
* Returns part of the config to set up the legend.
*
* @return {Object}
*/
const legendConfig = function () {
return {
labels: {
generateLabels(chart) {
let labels = Chartjs.defaults.plugins.legend.labels.generateLabels(chart);
// Store the standard error label for future use.
const stdErrorLabelText = labels.find((label) =>
label.datasetIndex === DatasetConfig.indices.STANDARD_ERROR_PERCENT).text;
// Remove everything hidden and one of the standard error labels.
labels = labels.filter((label) =>
!label.hidden && label.datasetIndex !== DatasetConfig.indices.STANDARD_ERROR_MIN);
// Order by dataset index.
labels.sort((label1, label2) => label1.datasetIndex - label2.datasetIndex);
// Convert one of the standard error labels to a proper one.
const stdErrorMaxIndex = labels.findIndex((label) =>
label.datasetIndex === DatasetConfig.indices.STANDARD_ERROR_MAX);
labels[stdErrorMaxIndex].text = stdErrorLabelText;
labels[stdErrorMaxIndex].strokeStyle = STANDARD_ERROR_LABEL_COLOR;
labels[stdErrorMaxIndex].fillStyle = STANDARD_ERROR_LABEL_COLOR;
return labels;
}
},
onClick: function () {
return false;
}
};
};
/**
* Output for the attempt administration chart.
*
* @class
* @extends {module:core/chart_output_chartjs}
*/
function AttemptAdministrationChartOutput() {
Output.apply(this, arguments);
}
AttemptAdministrationChartOutput.prototype = Object.create(Output.prototype);
/**
* Overrides config definition to add more custom features.
*
* @protected
* @override
* @return {Object}
*/
AttemptAdministrationChartOutput.prototype._makeConfig = function () {
let config = Output.prototype._makeConfig.apply(this, arguments);
// Define draw order.
config.data.datasets[DatasetConfig.indices.ABILITY_MEASURE].order = DatasetConfig.orderWeights.ABILITY_MEASURE;
config.data.datasets[DatasetConfig.indices.TARGET_DIFFICULTY].order = DatasetConfig.orderWeights.TARGET_DIFFICULTY;
config.data.datasets[DatasetConfig.indices.ADMINISTERED_DIFFICULTY].order =
DatasetConfig.orderWeights.ADMINISTERED_DIFFICULTY;
config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].order = DatasetConfig.orderWeights.STANDARD_ERROR_BORDER;
config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].order = DatasetConfig.orderWeights.STANDARD_ERROR_BORDER;
// Hide lines and points for standard error min/max datasets.
// In Chart.js version prior to 4.2.1 points visibility can be controlled with the 'pointRadius' property.
config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].pointRadius = 0;
config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].pointRadius = 0;
// For Chart.js since 4.2.1.
config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].pointStyle = false;
config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].pointStyle = false;
config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MAX].showLine = false;
config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_MIN].showLine = false;
// Hide entire datasets with standard error percentages and right/wrong flags.
config.data.datasets[DatasetConfig.indices.STANDARD_ERROR_PERCENT].hidden = true;
config.data.datasets[DatasetConfig.indices.CORRECT_WRONG_FLAG].hidden = true;
// Tooltip.
config.options.plugins.tooltip.filter = tooltipItemsFilter;
config.options.plugins.tooltip.callbacks.afterLabel = afterTooltipItemLabel.bind(this);
config.options.plugins.tooltip.itemSort = function (tooltip1, tooltip2) {
return tooltip1.datasetIndex - tooltip2.datasetIndex;
};
// Legend.
config.options.plugins.legend = legendConfig();
return config;
};
return AttemptAdministrationChartOutput;
});
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Customized output for the attempt administration chart data.
*
* @module mod_adaptivequiz/attempt_administration_chart_output_htmltable
* @copyright 2024 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/chart_output_htmltable',
'mod_adaptivequiz/attempt_administration_chart_dataset_config'
], function (
$,
OutputTable,
DatasetConfig
) {
/**
* Output for the attempt administration chart data.
*
* @class
* @extends {module:core/chart_output_htmltable}
*/
function AttemptAdministrationChartOutputTable() {
OutputTable.apply(this, arguments);
}
AttemptAdministrationChartOutputTable.prototype = Object.create(OutputTable.prototype);
/**
* Overrides building the table.
*
* @override
* @protected
* @return {Object} Modified table node.
*/
AttemptAdministrationChartOutputTable.prototype._makeTable = function() {
let tbl = OutputTable.prototype._makeTable.apply(this, arguments);
// Remove columns with standard error min/max.
const dataIndicesToRemove = [DatasetConfig.indices.STANDARD_ERROR_MAX, DatasetConfig.indices.STANDARD_ERROR_MIN];
const selectorsToRemove = dataIndicesToRemove.flatMap(
(dataIndexToRemove) => [`th:nth-child(${dataIndexToRemove + 2})`, `td:nth-child(${dataIndexToRemove + 2})`]
);
tbl.find(selectorsToRemove.join()).remove();
return tbl;
};
return AttemptAdministrationChartOutputTable;
});
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to manage appearance of the answers distribution chart.
*
* @module mod_adaptivequiz/attempt_answers_distribution_chart_manager
* @copyright 2024 Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
import Notification from 'core/notification';
/**
* Entry point of the module.
*
* @param {Object} chartOutput An instance of Output.
* @param {Object} chartBar An instance of Bar.
* @param {Number} userId Current user to set the preferences for.
* @param {Number} adaptiveQuizId Adaptive quiz instance to set the preferences for.
*/
export const init = (chartOutput, chartBar, userId, adaptiveQuizId) => {
let stackedFlagControl = document.querySelector('[data-action="set-answers-distribution-chart-stacked"]');
stackedFlagControl.addEventListener('change', (e) => {
const setStacked = e.target.checked;
chartBar.setStacked(setStacked);
chartOutput.update();
Ajax.call([{
methodname: 'core_user_set_user_preferences',
args: {
preferences: [{
name: `mod_adaptivequiz_answers_distribution_chart_settings_${adaptiveQuizId}`,
value: JSON.stringify({
showstacked: setStacked,
}),
userid: userId,
}]
}
}])[0].catch(Notification.exception);
});
};
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Adaptive quiz attempt script.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
require_once($CFG->dirroot . '/tag/lib.php');
use mod_adaptivequiz\local\attempt;
use mod_adaptivequiz\local\catalgo;
use mod_adaptivequiz\local\fetchquestion;
$id = required_param('cmid', PARAM_INT); // Course module id.
$uniqueid = optional_param('uniqueid', 0, PARAM_INT); // Unique id of the attempt.
$difflevel = optional_param('dl', 0, PARAM_INT); // Difficulty level of question.
if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) {
throw new moodle_exception('invalidcoursemodule');
}
if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
throw new moodle_exception('coursemisconf');
}
global $USER, $DB, $SESSION;
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
$passwordattempt = false;
try {
$adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*', MUST_EXIST);
} catch (dml_exception $e) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $id));
$debuginfo = '';
if (!empty($e->debuginfo)) {
$debuginfo = $e->debuginfo;
}
throw new moodle_exception('invalidmodule', 'error', $url, $e->getMessage(), $debuginfo);
}
// Setup page global for standard viewing.
$viewurl = new moodle_url('/mod/adaptivequiz/view.php', array('id' => $cm->id));
$PAGE->set_url('/mod/adaptivequiz/view.php', array('id' => $cm->id));
$PAGE->set_title(format_string($adaptivequiz->name));
$PAGE->set_context($context);
$PAGE->activityheader->disable();
$PAGE->add_body_class('limitedwidth');
// Check if the user has the attempt capability.
require_capability('mod/adaptivequiz:attempt', $context);
// Check if the user has any previous attempts at this activity.
$count = adaptivequiz_count_user_previous_attempts($adaptivequiz->id, $USER->id);
if (!adaptivequiz_allowed_attempt($adaptivequiz->attempts, $count)) {
throw new moodle_exception('noattemptsallowed', 'adaptivequiz');
}
// Create an instance of the module renderer class.
$output = $PAGE->get_renderer('mod_adaptivequiz');
// Setup password required form.
$mform = $output->display_password_form($cm->id);
// Check if a password is required.
if (!empty($adaptivequiz->password)) {
// Check if the user has alredy entered in their password.
$condition = adaptivequiz_user_entered_password($adaptivequiz->id);
if (empty($condition) && $mform->is_cancelled()) {
// Return user to landing page.
redirect($viewurl);
} else if (empty($condition) && $data = $mform->get_data()) {
$SESSION->passwordcheckedadpq = array();
if (0 == strcmp($data->quizpassword, $adaptivequiz->password)) {
$SESSION->passwordcheckedadpq[$adaptivequiz->id] = true;
} else {
$SESSION->passwordcheckedadpq[$adaptivequiz->id] = false;
$passwordattempt = true;
}
}
}
// Create an instance of the adaptiveattempt class.
$adaptiveattempt = new attempt($adaptivequiz, $USER->id);
$algo = new stdClass();
$nextdiff = null;
$standarderror = 0.0;
$message = '';
// If uniqueid is not empty the process respones.
if (!empty($uniqueid) && confirm_sesskey()) {
// Check if the uniqueid belongs to the same attempt record the user is currently using.
$attemptrec = $adaptiveattempt->get_attempt();
if (!adaptivequiz_uniqueid_part_of_attempt($uniqueid, $cm->instance, $USER->id)) {
throw new moodle_exception('uniquenotpartofattempt', 'adaptivequiz');
}
// Process student's responses.
try {
// Set a time stamp for the actions below.
$time = time();
// Load the user's current usage from the DB.
$quba = question_engine::load_questions_usage_by_activity((int) $uniqueid);
// Update the actions done to the question.
$quba->process_all_actions($time);
// Finish the grade attempt at the question.
$quba->finish_all_questions($time);
// Save the data about the usage to the DB.
question_engine::save_questions_usage_by_activity($quba);
if (!empty($difflevel)) {
// Check if the minimum number of attempts have been reached.
$minattemptreached = adaptivequiz_min_attempts_reached($uniqueid, $cm->instance, $USER->id);
// Create an instance of the CAT algo class.
$algo = new catalgo($quba, (int) $attemptrec->id, $minattemptreached, (int) $difflevel);
// Calculate the next difficulty level.
$nextdiff = $algo->perform_calculation_steps();
// Increment difficulty level for attempt.
$everythingokay = false;
$difflogit = $algo->get_levellogit();
$standarderror = $algo->get_standarderror();
$measure = $algo->get_measure();
$everythingokay = adaptivequiz_update_attempt_data($uniqueid, $cm->instance, $USER->id, $difflogit, $standarderror,
$measure);
// Something went wrong with updating the attempt. Print an error.
if (!$everythingokay) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $id));
throw new moodle_exception('unableupdatediffsum', 'adaptivequiz', $url);
}
// Check whether the status property is empty.
$message = $algo->get_status();
if (!empty($message)) {
$standarderror = $algo->get_standarderror();
// Set the attempt to complete, update the standard error and attempt message, then redirect the user to the
// attempt finished page.
adaptivequiz_complete_attempt($uniqueid, $adaptivequiz, $context, $USER->id, $standarderror, $message);
$param = array('cmid' => $cm->id, 'id' => $cm->instance, 'uattid' => $uniqueid);
$url = new moodle_url('/mod/adaptivequiz/attemptfinished.php', $param);
redirect($url);
}
// Lastly decrement the sum of questions for the attempted difficulty level.
$fetchquestion = new fetchquestion($quba, $difflevel, $adaptivequiz->lowestlevel, $adaptivequiz->highestlevel);
$tagquestcount = $fetchquestion->get_tagquestsum();
$tagquestcount = $fetchquestion->decrement_question_sum_from_difficulty($tagquestcount, $difflevel);
$fetchquestion->set_tagquestsum($tagquestcount);
// Force the class to deconstruct the object and save the updated mapping to the session global.
unset($fetchquestion);
}
} catch (question_out_of_sequence_exception $e) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $id));
throw new moodle_exception('submissionoutofsequencefriendlymessage', 'question', $url);
} catch (Exception $e) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $id));
$debuginfo = '';
if (!empty($e->debuginfo)) {
$debuginfo = $e->debuginfo;
}
throw new moodle_exception('errorprocessingresponses', 'question', $url, $e->getMessage(), $debuginfo);
}
}
$adaptivequiz->context = $context;
$adaptivequiz->cm = $cm;
// If value is null then set the difficulty level to the starting level for the attempt.
if (!is_null($nextdiff)) {
$adaptiveattempt->set_level((int) $nextdiff);
} else {
$adaptiveattempt->set_level((int) $adaptivequiz->startinglevel);
}
// If we have a previous difficulty level, pass that off to the attempt so that it
// can modify the next-question search process based on this level.
if (isset($difflevel) && !is_null($difflevel)) {
$adaptiveattempt->set_last_difficulty_level($difflevel);
}
$attemptstatus = $adaptiveattempt->start_attempt();
// Check if attempt status is set to ready.
if (empty($attemptstatus)) {
// Retrieve the most recent status message for the attempt.
$message = $adaptiveattempt->get_status();
// Set the attempt to complete, update the standard error and attempt message, then redirect the user to the attempt-finished
// page.
if ($algo instanceof catalgo) {
$standarderror = $algo->get_standarderror();
}
$noquestionsfetchedforattempt = $uniqueid == 0;
if ($noquestionsfetchedforattempt) {
// The script will try to complete an 'empty' attempt as it couldn't fetch the first question for some reason.
// This is an invalid behaviour, which could be caused by a misconfigured questions pool. Stop it here.
throw new moodle_exception('attemptnofirstquestion', 'adaptivequiz',
(new moodle_url('/mod/adaptivequiz/view.php', ['id' => $cm->id]))->out());
}
adaptivequiz_complete_attempt($uniqueid, $adaptivequiz, $context, $USER->id, $standarderror, $message);
// Redirect the user to the attemptfeedback page.
$param = array('cmid' => $cm->id, 'id' => $cm->instance, 'uattid' => $uniqueid);
$url = new moodle_url('/mod/adaptivequiz/attemptfinished.php', $param);
redirect($url);
}
// Retrieve the question slot id.
$slot = $adaptiveattempt->get_question_slot_number();
// Retrieve the question_usage_by_activity object.
$quba = $adaptiveattempt->get_quba();
// If $nextdiff is null then this is either a new attempt or a continuation of an previous attempt. Calculate the current
// difficulty level the attempt should be at.
if (is_null($nextdiff)) {
// Calculate the current difficulty level.
$adaptivequiz->lowestlevel = (int) $adaptivequiz->lowestlevel;
$adaptivequiz->highestlevel = (int) $adaptivequiz->highestlevel;
$adaptivequiz->startinglevel = (int) $adaptivequiz->startinglevel;
// Create an instance of the catalgo class, however constructor arguments are not important.
$algo = new catalgo($quba, 1, false, 1);
$level = $algo->get_current_diff_level($quba, $adaptivequiz->startinglevel, $adaptivequiz);
} else {
// Retrieve the currently set difficulty level.
$level = $adaptiveattempt->get_level();
}
$headtags = $output->init_metadata($quba, $slot);
$PAGE->requires->js_init_call('M.mod_adaptivequiz.init_attempt_form', array($viewurl->out(), $adaptivequiz->browsersecurity),
false, $output->adaptivequiz_get_js_module());
// Init secure window if enabled.
if (!empty($adaptivequiz->browsersecurity)) {
$PAGE->blocks->show_only_fake_blocks();
$output->init_browser_security();
} else {
$PAGE->set_heading(format_string($course->fullname));
}
echo $output->header();
// Check if the user entered a password.
$condition = adaptivequiz_user_entered_password($adaptivequiz->id);
if (!empty($adaptivequiz->password) && empty($condition)) {
if ($passwordattempt) {
$mform->set_data(array('message' => get_string('wrongpassword', 'adaptivequiz')));
}
$mform->display();
} else {
$attemptrecord = $adaptiveattempt->get_attempt();
if ($adaptivequiz->showattemptprogress) {
echo $output->container_start('attempt-progress-container');
echo $output->attempt_progress($attemptrecord->questionsattempted, $adaptivequiz->maximumquestions);
echo $output->container_end();
}
echo $output->question_submit_form($id, $quba, $slot, $level, $attemptrecord->questionsattempted + 1);
}
echo $output->print_footer();
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Adaptive quiz attempt script
*
* @copyright 2013 Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/adaptivequiz/locallib.php');
use mod_adaptivequiz\output\ability_measure;
$cmid = required_param('cmid', PARAM_INT); // Course module id.
$instance = required_param('id', PARAM_INT); // Activity instance id.
$uniqueid = required_param('uattid', PARAM_INT); // Attempt unique id.
if (!$cm = get_coursemodule_from_id('adaptivequiz', $cmid)) {
throw new moodle_exception('invalidcoursemodule');
}
if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
throw new moodle_exception('coursemisconf');
}
$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $cm->instance], '*', MUST_EXIST);
$abilitymeasurerenderable = null;
if ($adaptivequiz->showabilitymeasure) {
$abilitymeasurevalue = $DB->get_field('adaptivequiz_attempt', 'measure', ['uniqueid' => $uniqueid], MUST_EXIST);
$abilitymeasurerenderable = ability_measure::of_attempt_on_adaptive_quiz($adaptivequiz, $abilitymeasurevalue);
}
require_login($course, true, $cm);
$context = context_module::instance($cm->id);
// TODO - check if user has capability to attempt.
// Check if this is the owner of the attempt.
$validattempt = adaptivequiz_uniqueid_part_of_attempt($uniqueid, $instance, $USER->id);
// Display an error message if this is not the owner of the attempt.
if (!$validattempt) {
$url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $cm->id));
throw new moodle_exception('notyourattempt', 'adaptivequiz', $url);
}
//KNIGHT: Count number of attempts for comparison with trigger of reminder flag (for checking question difficulty level by viewing question analysis page)
$completedattemptscount = adaptivequiz_count_user_previous_attempts($adaptivequiz->id, $USER->id);
if ($adaptivequiz->questionchecktrigger > 0 && $adaptivequiz->questionschecked == 1 && (($completedattemptscount%$adaptivequiz->questionchecktrigger) == 0)){
$adaptivequiz->questionschecked = 0;
$DB->update_record('adaptivequiz', $adaptivequiz);
}
$PAGE->set_url('/mod/adaptivequiz/view.php', array('id' => $cm->id));
$PAGE->set_title(format_string($adaptivequiz->name));
$PAGE->set_context($context);
$PAGE->activityheader->disable();
$PAGE->add_body_class('limitedwidth');
$output = $PAGE->get_renderer('mod_adaptivequiz');
// Init secure window if enabled.
$popup = false;
if (!empty($adaptivequiz->browsersecurity)) {
$PAGE->blocks->show_only_fake_blocks();
$output->init_browser_security(false);
$popup = true;
} else {
$PAGE->set_heading(format_string($course->fullname));
}
echo $output->header();
echo $output->attempt_feedback($adaptivequiz->attemptfeedback, $cm->id, $abilitymeasurerenderable, $popup);
echo $output->footer();
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides the steps to perform one complete backup of the adaptivequiz instance.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/mod/adaptivequiz/backup/moodle2/backup_adaptivequiz_stepslib.php');
class backup_adaptivequiz_activity_task extends backup_activity_task {
/**
* No specific settings for this activity
*/
protected function define_my_settings() {
}
/**
* Defines backup steps to store the instance data and required questions
*/
protected function define_my_steps() {
// Generate the adaptivequiz.xml file containing all the quiz information
// and annotating used questions.
$this->add_step(new backup_adaptivequiz_activity_structure_step('adaptivequiz_structure', 'adaptivequiz.xml'));
// Note: Following steps must be present
// in all the activities using question banks.
// Process all the annotated questions to calculate the question
// categories needing to be included in backup for this activity
// plus the categories belonging to the activity context itself.
$this->add_step(new backup_calculate_question_categories('activity_question_categories'));
// Clean backup_temp_ids table from questions. We already
// have used them to detect question_categories and aren't
// needed anymore.
$this->add_step(new backup_delete_temp_questions('clean_temp_questions'));
}
/**
* Encodes URLs to the index.php and view.php scripts
* @param string $content some HTML text that eventually contains URLs to the activity instance scripts
* @return string the content with the URLs encoded
*/
public static function encode_content_links($content) {
global $CFG;
$base = preg_quote($CFG->wwwroot, '/');
// Link to the list of adatpivequizzes.
$search = "/(".$base."\/mod\/adaptivequiz\/index.php\?id\=)([0-9]+)/";
$content = preg_replace($search, '$@ADAPTIVEQUIZINDEX*$2@$', $content);
// Link to adaptivequiz view by moduleid.
$search = "/(".$base."\/mod\/adaptivequiz\/view.php\?id\=)([0-9]+)/";
$content = preg_replace($search, '$@ADAPTIVEQUIZVIEWBYID*$2@$', $content);
// Link to adaptivequiz view by adaptivequizid.
$search = "/(".$base."\/mod\/adaptivequiz\/view.php\?q\=)([0-9]+)/";
$content = preg_replace($search, '$@ADAPTIVEQUIZVIEWBYQ*$2@$', $content);
return $content;
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Define all the backup steps that will be used by the backup_adaptivequiz_activity_task.
*
* @package mod_adaptivequiz
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_adaptivequiz_activity_structure_step extends backup_questions_activity_structure_step {
/**
* Define the backup structure.
*
* @return backup_nested_element The root element (adaptivequiz), wrapped into standard activity structure.
*/
protected function define_structure() {
// To know if we are including userinfo.
$userinfo = $this->get_setting_value('userinfo');
// Define each element separated.
$nodes = ['name', 'intro', 'introformat', 'attempts', 'password', 'browsersecurity', 'attemptfeedback',
'attemptfeedbackformat', 'showabilitymeasure', 'showattemptprogress', 'highestlevel', 'lowestlevel', 'minimumquestions',
'maximumquestions', 'standarderror', 'startinglevel', 'timecreated', 'timemodified', 'completionattemptcompleted'];
$adaptivequiz = new backup_nested_element('adaptivequiz', ['id'], $nodes);
// Attempts.
$adaptiveattempts = new backup_nested_element('adaptiveattempts');
$nodes = ['userid', 'uniqueid', 'attemptstate', 'attemptstopcriteria', 'questionsattempted', 'difficultysum',
'standarderror', 'measure', 'timecreated', 'timemodified'];
$adaptiveattempt = new backup_nested_element('adaptiveattempt', ['id'], $nodes);
// This module is using questions, so produce the related question states and sessions.
// attaching them to the $attempt element based in 'uniqueid' matching.
$this->add_question_usages($adaptiveattempt, 'uniqueid');
// Activity to question categories reference.
$adaptivequestioncats = new backup_nested_element('adatpivequestioncats');
$adaptivequestioncat = new backup_nested_element('adatpivequestioncat', ['id'], ['questioncategory']);
// Build the tree.
$adaptivequiz->add_child($adaptiveattempts);
$adaptiveattempts->add_child($adaptiveattempt);
$adaptivequiz->add_child($adaptivequestioncats);
$adaptivequestioncats->add_child($adaptivequestioncat);
// Define sources.
$adaptivequiz->set_source_table('adaptivequiz', ['id' => backup::VAR_ACTIVITYID]);
$adaptivequestioncat->set_source_table('adaptivequiz_question', ['instance' => backup::VAR_PARENTID]);
// All the rest of elements only happen if we are including user info.
if ($userinfo) {
$sql = 'SELECT *
FROM {adaptivequiz_attempt}
WHERE instance = :instance';
$param = array('instance' => backup::VAR_PARENTID);
$adaptiveattempt->set_source_sql($sql, $param);
}
// Define id annotations.
$adaptivequestioncat->annotate_ids('question_categories', 'questioncategory');
$adaptiveattempt->annotate_ids('user', 'userid');
$adaptivequiz->annotate_files('mod_adaptivequiz', 'intro', null); // This file area hasn't itemid.
return $this->prepare_activity_structure($adaptivequiz);
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Restore task that provides all the settings and steps to perform one complete restore of the activity.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/mod/adaptivequiz/backup/moodle2/restore_adaptivequiz_stepslib.php');
class restore_adaptivequiz_activity_task extends restore_activity_task {
/**
* Define (add) particular settings this activity can have
*/
protected function define_my_settings() {
// No particular settings for this activity.
}
/**
* Define (add) particular steps this activity can have
*/
protected function define_my_steps() {
// Adaptivequiz only has one structure step.
$this->add_step(new restore_adaptivequiz_activity_structure_step('adaptivequiz_structure', 'adaptivequiz.xml'));
}
/**
* Define the contents in the activity that must be
* processed by the link decoder
* @return array an array of restore_decode_content objects
*/
public static function define_decode_contents() {
$contents = array();
$contents[] = new restore_decode_content('adaptivequiz', array('intro'), 'adaptivequiz');
return $contents;
}
/**
* Define the decoding rules for links belonging
* to the activity to be executed by the link decoder
* @return array an array of restore_decode_rule objects
*/
public static function define_decode_rules() {
$rules = array();
$rules[] = new restore_decode_rule('ADAPTIVEQUIZVIEWBYID', '/mod/adaptivequiz/view.php?id=$1', 'course_module');
$rules[] = new restore_decode_rule('ADAPTIVEQUIZVIEWBYQ', '/mod/adaptivequiz/view.php?q=$1', 'adaptivequiz');
$rules[] = new restore_decode_rule('ADAPTIVEQUIZINDEX', '/mod/adaptivequiz/index.php?id=$1', 'course');
return $rules;
}
/**
* Define the restore log rules that will be applied
* by the {@link restore_logs_processor} when restoring
* adaptivequiz logs. It must return one array
* of {@link restore_log_rule} objects
* @return array an array of restore_log_rule objects
*/
public static function define_restore_log_rules() {
$rules = array();
// TODO update this method when logging statemtns have been added to the code.
return $rules;
}
/**
* Define the restore log rules that will be applied
* by the {@link restore_logs_processor} when restoring
* course logs. It must return one array
* of {@link restore_log_rule} objects
*
* Note this rules are applied when restoring course logs
* by the restore final task, but are defined here at
* activity level. All them are rules not linked to any module instance (cmid = 0)
* @return array an array of of restore_log_rule objects
*/
public static function define_restore_log_rules_for_course() {
$rules = array();
return $rules;
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Structure step to restore one adaptivequiz activity.
*
* @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/}
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_adaptivequiz_activity_structure_step extends restore_questions_activity_structure_step {
/**
* Define the a structure for restoring the activity
* @return backup_nested_element the $activitystructure wrapped by the common 'activity' element
*/
protected function define_structure() {
$paths = array();
$userinfo = $this->get_setting_value('userinfo');
$adaptivequiz = new restore_path_element('adaptivequiz', '/activity/adaptivequiz');
$paths[] = $adaptivequiz;
$paths[] = new restore_path_element('adaptivequiz_question',
'/activity/adaptivequiz/adatpivequestioncats/adatpivequestioncat');
if ($userinfo) {
$attempt = new restore_path_element('adaptivequiz_attempt', '/activity/adaptivequiz/adaptiveattempts/adaptiveattempt');
$paths[] = $attempt;
// Add states and sessions.
$this->add_question_usages($attempt, $paths);
}
// Return the paths wrapped into standard activity structure.
return $this->prepare_activity_structure($paths);
}
/**
* Process the adaptivequiz element
* @param stdClass an object whose properties are nodes in the adatpviequiz structure
*/
protected function process_adaptivequiz($data) {
global $CFG, $DB;
$data = (object)$data;
$oldid = $data->id;
$data->course = $this->get_courseid();
$data->timecreated = $this->apply_date_offset($data->timecreated);
$data->timemodified = $this->apply_date_offset($data->timemodified);
// Insert the quiz record.
$newitemid = $DB->insert_record('adaptivequiz', $data);
// Immediately after inserting "activity" record, call this.
$this->apply_activity_instance($newitemid);
}
/**
* Process the activity instance to question categories relation structure\
* @param stdClass an object whose properties are nodes in the adatpviequiz_question structure
*/
protected function process_adaptivequiz_question($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->instance = $this->get_new_parentid('adaptivequiz');
// Check if catid is not empty and update the record with the new category id.
$catid = $this->get_mappingid('question_category', $data->questioncategory);
if (!empty($catid)) {
$data->questioncategory = $catid;
}
$DB->insert_record('adaptivequiz_question', $data);
}
/**
* Process the activity instance to question categories relation structure
* @param stdClass an object whose properties are nodes in the adatpviequiz_attempt structure
*/
protected function process_adaptivequiz_attempt($data) {
$data = (object)$data;
$oldid = $data->id;
$data->instance = $this->get_new_parentid('adaptivequiz');
$data->userid = $this->get_mappingid('user', $data->userid);
$data->timemodified = $this->apply_date_offset($data->timemodified);
// The data is actually inserted into the database later in inform_new_usage_id.
$this->currentadatpivequizattempt = clone($data);
}
/**
* This function assigns the new question usage by activity id to the attempt
* @param int $newusageid a new question usage by activity id
*/
protected function inform_new_usage_id($newusageid) {
global $DB;
$data = $this->currentadatpivequizattempt;
$oldid = $data->id;
$data->uniqueid = $newusageid;
$newitemid = $DB->insert_record('adaptivequiz_attempt', $data);
// Save quiz_attempt->id mapping, because logs use it. (logs will be implemented latter).
$this->set_mapping('adaptivequiz_attempt', $oldid, $newitemid, false);
}
/**
* This function adds any files assocaited with the intro field after the restore process has run
*/
protected function after_execute() {
parent::after_execute();
// Add quiz related files, no need to match by itemname (just internally handled context).
$this->add_related_files('mod_adaptivequiz', 'intro', null);
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Handles our own events to make some reactive changes, for example, update activity completion state (if completion is enabled).
*
* @copyright 2022 onwards Vitaly Potenko <potenkov@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_adaptivequiz;
use completion_info;
use core\event\base;
class attempt_state_change_observers {
public static function attempt_completed(base $event): void {
global $DB;
// Update completion state if enabled.
if (!$attempt = $event->get_record_snapshot('adaptivequiz_attempt', $event->objectid)) {
return;
}
if (!$adaptivequiz = $DB->get_record('adaptivequiz', ['id' => $attempt->instance])) {
return;
}
if (!$course = $DB->get_record('course', ['id' => $adaptivequiz->course])) {
return;
}
$completion = new completion_info($course);
if (!$completion->is_enabled()) {
return;
}
if (!$adaptivequiz->completionattemptcompleted) {
return;
}
if (!$cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequiz->id, $adaptivequiz->course)) {
return;
}
$completion->update_state($cm, COMPLETION_COMPLETE, $event->userid);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment