See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * A collection of all the question statistics calculated for an activity instance ie. the stats calculated for slots and 19 * sub-questions and variants of those questions. 20 * 21 * @package core_question 22 * @copyright 2014 The Open University 23 * @author James Pratt me@jamiep.org 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 */ 26 27 namespace core_question\statistics\questions; 28 29 use question_bank; 30 31 /** 32 * A collection of all the question statistics calculated for an activity instance. 33 * 34 * @package core_question 35 * @copyright 2014 The Open University 36 * @author James Pratt me@jamiep.org 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 class all_calculated_for_qubaid_condition { 40 41 /** @var int No longer used. Previously, the time after which statistics are automatically recomputed. */ 42 const TIME_TO_CACHE = 900; // 15 minutes. 43 44 /** 45 * @var object[] 46 */ 47 public $subquestions = []; 48 49 /** 50 * Holds slot (position) stats and stats for variants of questions in slots. 51 * 52 * @var calculated[] 53 */ 54 public $questionstats = array(); 55 56 /** 57 * Holds sub-question stats and stats for variants of subqs. 58 * 59 * @var calculated_for_subquestion[] 60 */ 61 public $subquestionstats = array(); 62 63 /** 64 * Set up a calculated_for_subquestion instance ready to store a randomly selected question's stats. 65 * 66 * @param object $step 67 * @param int|null $variant Is this to keep track of a variant's stats? If so what is the variant, if not null. 68 */ 69 public function initialise_for_subq($step, $variant = null) { 70 $newsubqstat = new calculated_for_subquestion($step, $variant); 71 if ($variant === null) { 72 $this->subquestionstats[$step->questionid] = $newsubqstat; 73 } else { 74 $this->subquestionstats[$step->questionid]->variantstats[$variant] = $newsubqstat; 75 } 76 } 77 78 /** 79 * Set up a calculated instance ready to store a slot question's stats. 80 * 81 * @param int $slot 82 * @param object $question 83 * @param int|null $variant Is this to keep track of a variant's stats? If so what is the variant, if not null. 84 */ 85 public function initialise_for_slot($slot, $question, $variant = null) { 86 $newqstat = new calculated($question, $slot, $variant); 87 if ($variant === null) { 88 $this->questionstats[$slot] = $newqstat; 89 } else { 90 $this->questionstats[$slot]->variantstats[$variant] = $newqstat; 91 } 92 } 93 94 /** 95 * Do we have stats for a particular quesitonid (and optionally variant)? 96 * 97 * @param int $questionid The id of the sub question. 98 * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats. 99 * @return bool whether those stats exist (yet). 100 */ 101 public function has_subq($questionid, $variant = null) { 102 if ($variant === null) { 103 return isset($this->subquestionstats[$questionid]); 104 } else { 105 return isset($this->subquestionstats[$questionid]->variantstats[$variant]); 106 } 107 } 108 109 /** 110 * Reference for a item stats instance for a questionid and optional variant no. 111 * 112 * @param int $questionid The id of the sub question. 113 * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats. 114 * @return calculated|calculated_for_subquestion stats instance for a questionid and optional variant no. 115 * Will be a calculated_for_subquestion if no variant specified. 116 * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats. 117 */ 118 public function for_subq($questionid, $variant = null) { 119 if ($variant === null) { 120 if (!isset($this->subquestionstats[$questionid])) { 121 throw new \coding_exception('Reference to unknown question id ' . $questionid); 122 } else { 123 return $this->subquestionstats[$questionid]; 124 } 125 } else { 126 if (!isset($this->subquestionstats[$questionid]->variantstats[$variant])) { 127 throw new \coding_exception('Reference to unknown question id ' . $questionid . 128 ' variant ' . $variant); 129 } else { 130 return $this->subquestionstats[$questionid]->variantstats[$variant]; 131 } 132 } 133 } 134 135 /** 136 * ids of all randomly selected question for all slots. 137 * 138 * @return int[] An array of all sub-question ids. 139 */ 140 public function get_all_subq_ids() { 141 return array_keys($this->subquestionstats); 142 } 143 144 /** 145 * All slots nos that stats have been calculated for. 146 * 147 * @return int[] An array of all slot nos. 148 */ 149 public function get_all_slots() { 150 return array_keys($this->questionstats); 151 } 152 153 /** 154 * Do we have stats for a particular slot (and optionally variant)? 155 * 156 * @param int $slot The slot no. 157 * @param int|null $variant if provided then we want the object which stores a variant of a position's stats. 158 * @return bool whether those stats exist (yet). 159 */ 160 public function has_slot($slot, $variant = null) { 161 if ($variant === null) { 162 return isset($this->questionstats[$slot]); 163 } else { 164 return isset($this->questionstats[$slot]->variantstats[$variant]); 165 } 166 } 167 168 /** 169 * Get position stats instance for a slot and optional variant no. 170 * 171 * @param int $slot The slot no. 172 * @param int|null $variant if provided then we want the object which stores a variant of a position's stats. 173 * @return calculated|calculated_for_subquestion An instance of the class storing the calculated position stats. 174 * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats. 175 */ 176 public function for_slot($slot, $variant = null) { 177 if ($variant === null) { 178 if (!isset($this->questionstats[$slot])) { 179 throw new \coding_exception('Reference to unknown slot ' . $slot); 180 } else { 181 return $this->questionstats[$slot]; 182 } 183 } else { 184 if (!isset($this->questionstats[$slot]->variantstats[$variant])) { 185 throw new \coding_exception('Reference to unknown slot ' . $slot . ' variant ' . $variant); 186 } else { 187 return $this->questionstats[$slot]->variantstats[$variant]; 188 } 189 } 190 } 191 192 /** 193 * Load cached statistics from the database. 194 * 195 * @param \qubaid_condition $qubaids Which question usages to load stats for? 196 */ 197 public function get_cached($qubaids) { 198 global $DB; 199 200 $timemodified = self::get_last_calculated_time($qubaids); 201 $questionstatrecs = $DB->get_records('question_statistics', 202 ['hashcode' => $qubaids->get_hash_code(), 'timemodified' => $timemodified]); 203 204 $questionids = array(); 205 foreach ($questionstatrecs as $fromdb) { 206 if (is_null($fromdb->variant) && !$fromdb->slot) { 207 $questionids[] = $fromdb->questionid; 208 } 209 } 210 $this->subquestions = question_load_questions($questionids); 211 foreach ($questionstatrecs as $fromdb) { 212 if (is_null($fromdb->variant)) { 213 if ($fromdb->slot) { 214 if (!isset($this->questionstats[$fromdb->slot])) { 215 debugging('Statistics found for slot ' . $fromdb->slot . 216 ' in stats ' . json_encode($qubaids->from_where_params()) . 217 ' which is not an analysable question.', DEBUG_DEVELOPER); 218 } 219 $this->questionstats[$fromdb->slot]->populate_from_record($fromdb); 220 } else { 221 $this->subquestionstats[$fromdb->questionid] = new calculated_for_subquestion(); 222 $this->subquestionstats[$fromdb->questionid]->populate_from_record($fromdb); 223 if (isset($this->subquestions[$fromdb->questionid])) { 224 $this->subquestionstats[$fromdb->questionid]->question = 225 $this->subquestions[$fromdb->questionid]; 226 } else { 227 $this->subquestionstats[$fromdb->questionid]->question = question_bank::get_qtype( 228 'missingtype', false)->make_deleted_instance($fromdb->questionid, 1); 229 } 230 } 231 } 232 } 233 // Add cached variant stats to data structure. 234 foreach ($questionstatrecs as $fromdb) { 235 if (!is_null($fromdb->variant)) { 236 if ($fromdb->slot) { 237 if (!isset($this->questionstats[$fromdb->slot])) { 238 debugging('Statistics found for slot ' . $fromdb->slot . 239 ' in stats ' . json_encode($qubaids->from_where_params()) . 240 ' which is not an analysable question.', DEBUG_DEVELOPER); 241 continue; 242 } 243 $newcalcinstance = new calculated(); 244 $this->questionstats[$fromdb->slot]->variantstats[$fromdb->variant] = $newcalcinstance; 245 $newcalcinstance->question = $this->questionstats[$fromdb->slot]->question; 246 } else { 247 $newcalcinstance = new calculated_for_subquestion(); 248 $this->subquestionstats[$fromdb->questionid]->variantstats[$fromdb->variant] = $newcalcinstance; 249 if (isset($this->subquestions[$fromdb->questionid])) { 250 $newcalcinstance->question = $this->subquestions[$fromdb->questionid]; 251 } else { 252 $newcalcinstance->question = question_bank::get_qtype( 253 'missingtype', false)->make_deleted_instance($fromdb->questionid, 1); 254 } 255 } 256 $newcalcinstance->populate_from_record($fromdb); 257 } 258 } 259 } 260 261 /** 262 * Find time of non-expired statistics in the database. 263 * 264 * @param \qubaid_condition $qubaids Which question usages to look for stats for? 265 * @return int|bool Time of cached record that matches this qubaid_condition or false if non found. 266 */ 267 public function get_last_calculated_time($qubaids) { 268 global $DB; 269 $lastcalculatedtime = $DB->get_field('question_statistics', 'COALESCE(MAX(timemodified), 0)', 270 ['hashcode' => $qubaids->get_hash_code()]); 271 if ($lastcalculatedtime) { 272 return $lastcalculatedtime; 273 } else { 274 return false; 275 } 276 } 277 278 /** 279 * Save stats to db, first cleaning up any old ones. 280 * 281 * @param \qubaid_condition $qubaids Which question usages are we caching the stats of? 282 */ 283 public function cache($qubaids) { 284 global $DB; 285 286 $transaction = $DB->start_delegated_transaction(); 287 $timemodified = time(); 288 289 foreach ($this->get_all_slots() as $slot) { 290 $this->for_slot($slot)->cache($qubaids, $timemodified); 291 } 292 293 foreach ($this->get_all_subq_ids() as $subqid) { 294 $this->for_subq($subqid)->cache($qubaids, $timemodified); 295 } 296 297 $transaction->allow_commit(); 298 } 299 300 /** 301 * Return all sub-questions used. 302 * 303 * @return \object[] array of questions. 304 */ 305 public function get_sub_questions() { 306 return $this->subquestions; 307 } 308 309 /** 310 * Return all stats for one slot, stats for the slot itself, and either : 311 * - variants of question 312 * - variants of randomly selected questions 313 * - randomly selected questions 314 * 315 * @param int $slot the slot no 316 * @param bool|int $limitvariants limit number of variants and sub-questions displayed? 317 * @return calculated|calculated_for_subquestion[] stats to display 318 */ 319 public function structure_analysis_for_one_slot($slot, $limitvariants = false) { 320 return array_merge(array($this->for_slot($slot)), $this->all_subq_and_variant_stats_for_slot($slot, $limitvariants)); 321 } 322 323 /** 324 * Call after calculations to output any error messages. 325 * 326 * @return string[] Array of strings describing error messages found during stats calculation. 327 */ 328 public function any_error_messages() { 329 $errors = array(); 330 foreach ($this->get_all_slots() as $slot) { 331 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { 332 if ($this->for_subq($subqid)->differentweights) { 333 $name = $this->for_subq($subqid)->question->name; 334 $errors[] = get_string('erroritemappearsmorethanoncewithdifferentweight', 'question', $name); 335 } 336 } 337 } 338 return $errors; 339 } 340 341 /** 342 * Return all stats for variants of question in slot $slot. 343 * 344 * @param int $slot The slot no. 345 * @return calculated[] The instances storing the calculated stats. 346 */ 347 protected function all_variant_stats_for_one_slot($slot) { 348 $toreturn = array(); 349 foreach ($this->for_slot($slot)->get_variants() as $variant) { 350 $toreturn[] = $this->for_slot($slot, $variant); 351 } 352 return $toreturn; 353 } 354 355 /** 356 * Return all stats for variants of randomly selected questions for one slot $slot. 357 * 358 * @param int $slot The slot no. 359 * @return calculated[] The instances storing the calculated stats. 360 */ 361 protected function all_subq_variants_for_one_slot($slot) { 362 $toreturn = array(); 363 $displayorder = 1; 364 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { 365 if ($variants = $this->for_subq($subqid)->get_variants()) { 366 foreach ($variants as $variant) { 367 $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid, $variant); 368 } 369 } 370 $displayorder++; 371 } 372 return $toreturn; 373 } 374 375 /** 376 * Return all stats for randomly selected questions for one slot $slot. 377 * 378 * @param int $slot The slot no. 379 * @return calculated[] The instances storing the calculated stats. 380 */ 381 protected function all_subqs_for_one_slot($slot) { 382 $displayorder = 1; 383 $toreturn = array(); 384 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { 385 $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid); 386 $displayorder++; 387 } 388 return $toreturn; 389 } 390 391 /** 392 * Return all variant or 'sub-question' stats one slot, either : 393 * - variants of question 394 * - variants of randomly selected questions 395 * - randomly selected questions 396 * 397 * @param int $slot the slot no 398 * @param bool $limited limit number of variants and sub-questions displayed? 399 * @return calculated|calculated_for_subquestion|calculated_question_summary[] stats to display 400 */ 401 protected function all_subq_and_variant_stats_for_slot($slot, $limited) { 402 // Random question in this slot? 403 if ($this->for_slot($slot)->get_sub_question_ids()) { 404 $toreturn = array(); 405 406 if ($limited) { 407 $randomquestioncalculated = $this->for_slot($slot); 408 409 if ($subqvariantstats = $this->all_subq_variants_for_one_slot($slot)) { 410 // There are some variants from randomly selected questions. 411 // If we're showing a limited view of the statistics then add a question summary stat 412 // rather than a stat for each subquestion. 413 $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqvariantstats); 414 415 $toreturn = array_merge($toreturn, [$summarystat]); 416 } 417 418 if ($subqstats = $this->all_subqs_for_one_slot($slot)) { 419 // There are some randomly selected questions. 420 // If we're showing a limited view of the statistics then add a question summary stat 421 // rather than a stat for each subquestion. 422 $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqstats); 423 424 $toreturn = array_merge($toreturn, [$summarystat]); 425 } 426 427 foreach ($toreturn as $index => $calculated) { 428 $calculated->subqdisplayorder = $index; 429 } 430 } else { 431 $displaynumber = 1; 432 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { 433 $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid); 434 if ($variants = $this->for_subq($subqid)->get_variants()) { 435 foreach ($variants as $variant) { 436 $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant); 437 } 438 } 439 $displaynumber++; 440 } 441 } 442 443 return $toreturn; 444 } else { 445 $variantstats = $this->all_variant_stats_for_one_slot($slot); 446 if ($limited && $variantstats) { 447 $variantquestioncalculated = $this->for_slot($slot); 448 449 // If we're showing a limited view of the statistics then add a question summary stat 450 // rather than a stat for each variation. 451 $summarystat = $this->make_new_calculated_question_summary_stat($variantquestioncalculated, $variantstats); 452 453 return [$summarystat]; 454 } else { 455 return $variantstats; 456 } 457 } 458 } 459 460 /** 461 * We need a new object for display. Sub-question stats can appear more than once in different slots. 462 * So we create a clone of the object and then we can set properties on the object that are per slot. 463 * 464 * @param int $displaynumber The display number for this sub question. 465 * @param int $slot The slot number. 466 * @param int $subqid The sub question id. 467 * @param null|int $variant The variant no. 468 * @return calculated_for_subquestion The object for display. 469 */ 470 protected function make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant = null) { 471 $slotstat = fullclone($this->for_subq($subqid, $variant)); 472 $slotstat->question->number = $this->for_slot($slot)->question->number; 473 $slotstat->subqdisplayorder = $displaynumber; 474 return $slotstat; 475 } 476 477 /** 478 * Create a summary calculated object for a calculated question. This is used as a placeholder 479 * to indicate that a calculated question has sub questions or variations to show rather than listing each 480 * subquestion or variation directly. 481 * 482 * @param calculated $randomquestioncalculated The calculated instance for the random question slot. 483 * @param calculated[] $subquestionstats The instances of the calculated stats of the questions that are being summarised. 484 * @return calculated_question_summary 485 */ 486 protected function make_new_calculated_question_summary_stat($randomquestioncalculated, $subquestionstats) { 487 $question = $randomquestioncalculated->question; 488 $slot = $randomquestioncalculated->slot; 489 $calculatedsummary = new calculated_question_summary($question, $slot, $subquestionstats); 490 491 return $calculatedsummary; 492 } 493 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body