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