Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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 $this->questionstats[$fromdb->slot]->populate_from_record($fromdb); 215 // Array created in constructor and populated from question. 216 } else { 217 $this->subquestionstats[$fromdb->questionid] = new calculated_for_subquestion(); 218 $this->subquestionstats[$fromdb->questionid]->populate_from_record($fromdb); 219 if (isset($this->subquestions[$fromdb->questionid])) { 220 $this->subquestionstats[$fromdb->questionid]->question = 221 $this->subquestions[$fromdb->questionid]; 222 } else { 223 $this->subquestionstats[$fromdb->questionid]->question = 224 question_bank::get_qtype('missingtype', false)->make_deleted_instance($fromdb->questionid, 1); 225 } 226 } 227 } 228 } 229 // Add cached variant stats to data structure. 230 foreach ($questionstatrecs as $fromdb) { 231 if (!is_null($fromdb->variant)) { 232 if ($fromdb->slot) { 233 $newcalcinstance = new calculated(); 234 $this->questionstats[$fromdb->slot]->variantstats[$fromdb->variant] = $newcalcinstance; 235 $newcalcinstance->question = $this->questionstats[$fromdb->slot]->question; 236 } else { 237 $newcalcinstance = new calculated_for_subquestion(); 238 $this->subquestionstats[$fromdb->questionid]->variantstats[$fromdb->variant] = $newcalcinstance; 239 $newcalcinstance->question = $this->subquestions[$fromdb->questionid]; 240 } 241 $newcalcinstance->populate_from_record($fromdb); 242 } 243 } 244 } 245 246 /** 247 * Find time of non-expired statistics in the database. 248 * 249 * @param \qubaid_condition $qubaids Which question usages to look for stats for? 250 * @return int|bool Time of cached record that matches this qubaid_condition or false if non found. 251 */ 252 public function get_last_calculated_time($qubaids) { 253 global $DB; 254 $lastcalculatedtime = $DB->get_field('question_statistics', 'COALESCE(MAX(timemodified), 0)', 255 ['hashcode' => $qubaids->get_hash_code()]); 256 if ($lastcalculatedtime) { 257 return $lastcalculatedtime; 258 } else { 259 return false; 260 } 261 } 262 263 /** 264 * Save stats to db, first cleaning up any old ones. 265 * 266 * @param \qubaid_condition $qubaids Which question usages are we caching the stats of? 267 */ 268 public function cache($qubaids) { 269 global $DB; 270 271 $transaction = $DB->start_delegated_transaction(); 272 $timemodified = time(); 273 274 foreach ($this->get_all_slots() as $slot) { 275 $this->for_slot($slot)->cache($qubaids, $timemodified); 276 } 277 278 foreach ($this->get_all_subq_ids() as $subqid) { 279 $this->for_subq($subqid)->cache($qubaids, $timemodified); 280 } 281 282 $transaction->allow_commit(); 283 } 284 285 /** 286 * Return all sub-questions used. 287 * 288 * @return \object[] array of questions. 289 */ 290 public function get_sub_questions() { 291 return $this->subquestions; 292 } 293 294 /** 295 * Return all stats for one slot, stats for the slot itself, and either : 296 * - variants of question 297 * - variants of randomly selected questions 298 * - randomly selected questions 299 * 300 * @param int $slot the slot no 301 * @param bool|int $limitvariants limit number of variants and sub-questions displayed? 302 * @return calculated|calculated_for_subquestion[] stats to display 303 */ 304 public function structure_analysis_for_one_slot($slot, $limitvariants = false) { 305 return array_merge(array($this->for_slot($slot)), $this->all_subq_and_variant_stats_for_slot($slot, $limitvariants)); 306 } 307 308 /** 309 * Call after calculations to output any error messages. 310 * 311 * @return string[] Array of strings describing error messages found during stats calculation. 312 */ 313 public function any_error_messages() { 314 $errors = array(); 315 foreach ($this->get_all_slots() as $slot) { 316 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { 317 if ($this->for_subq($subqid)->differentweights) { 318 $name = $this->for_subq($subqid)->question->name; 319 $errors[] = get_string('erroritemappearsmorethanoncewithdifferentweight', 'question', $name); 320 } 321 } 322 } 323 return $errors; 324 } 325 326 /** 327 * Return all stats for variants of question in slot $slot. 328 * 329 * @param int $slot The slot no. 330 * @return calculated[] The instances storing the calculated stats. 331 */ 332 protected function all_variant_stats_for_one_slot($slot) { 333 $toreturn = array(); 334 foreach ($this->for_slot($slot)->get_variants() as $variant) { 335 $toreturn[] = $this->for_slot($slot, $variant); 336 } 337 return $toreturn; 338 } 339 340 /** 341 * Return all stats for variants of randomly selected questions for one slot $slot. 342 * 343 * @param int $slot The slot no. 344 * @return calculated[] The instances storing the calculated stats. 345 */ 346 protected function all_subq_variants_for_one_slot($slot) { 347 $toreturn = array(); 348 $displayorder = 1; 349 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { 350 if ($variants = $this->for_subq($subqid)->get_variants()) { 351 foreach ($variants as $variant) { 352 $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid, $variant); 353 } 354 } 355 $displayorder++; 356 } 357 return $toreturn; 358 } 359 360 /** 361 * Return all stats for randomly selected questions for one slot $slot. 362 * 363 * @param int $slot The slot no. 364 * @return calculated[] The instances storing the calculated stats. 365 */ 366 protected function all_subqs_for_one_slot($slot) { 367 $displayorder = 1; 368 $toreturn = array(); 369 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { 370 $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid); 371 $displayorder++; 372 } 373 return $toreturn; 374 } 375 376 /** 377 * Return all variant or 'sub-question' stats one slot, either : 378 * - variants of question 379 * - variants of randomly selected questions 380 * - randomly selected questions 381 * 382 * @param int $slot the slot no 383 * @param bool $limited limit number of variants and sub-questions displayed? 384 * @return calculated|calculated_for_subquestion|calculated_question_summary[] stats to display 385 */ 386 protected function all_subq_and_variant_stats_for_slot($slot, $limited) { 387 // Random question in this slot? 388 if ($this->for_slot($slot)->get_sub_question_ids()) { 389 $toreturn = array(); 390 391 if ($limited) { 392 $randomquestioncalculated = $this->for_slot($slot); 393 394 if ($subqvariantstats = $this->all_subq_variants_for_one_slot($slot)) { 395 // There are some variants from randomly selected questions. 396 // If we're showing a limited view of the statistics then add a question summary stat 397 // rather than a stat for each subquestion. 398 $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqvariantstats); 399 400 $toreturn = array_merge($toreturn, [$summarystat]); 401 } 402 403 if ($subqstats = $this->all_subqs_for_one_slot($slot)) { 404 // There are some randomly selected questions. 405 // If we're showing a limited view of the statistics then add a question summary stat 406 // rather than a stat for each subquestion. 407 $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqstats); 408 409 $toreturn = array_merge($toreturn, [$summarystat]); 410 } 411 412 foreach ($toreturn as $index => $calculated) { 413 $calculated->subqdisplayorder = $index; 414 } 415 } else { 416 $displaynumber = 1; 417 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) { 418 $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid); 419 if ($variants = $this->for_subq($subqid)->get_variants()) { 420 foreach ($variants as $variant) { 421 $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant); 422 } 423 } 424 $displaynumber++; 425 } 426 } 427 428 return $toreturn; 429 } else { 430 $variantstats = $this->all_variant_stats_for_one_slot($slot); 431 if ($limited && $variantstats) { 432 $variantquestioncalculated = $this->for_slot($slot); 433 434 // If we're showing a limited view of the statistics then add a question summary stat 435 // rather than a stat for each variation. 436 $summarystat = $this->make_new_calculated_question_summary_stat($variantquestioncalculated, $variantstats); 437 438 return [$summarystat]; 439 } else { 440 return $variantstats; 441 } 442 } 443 } 444 445 /** 446 * We need a new object for display. Sub-question stats can appear more than once in different slots. 447 * So we create a clone of the object and then we can set properties on the object that are per slot. 448 * 449 * @param int $displaynumber The display number for this sub question. 450 * @param int $slot The slot number. 451 * @param int $subqid The sub question id. 452 * @param null|int $variant The variant no. 453 * @return calculated_for_subquestion The object for display. 454 */ 455 protected function make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant = null) { 456 $slotstat = fullclone($this->for_subq($subqid, $variant)); 457 $slotstat->question->number = $this->for_slot($slot)->question->number; 458 $slotstat->subqdisplayorder = $displaynumber; 459 return $slotstat; 460 } 461 462 /** 463 * Create a summary calculated object for a calculated question. This is used as a placeholder 464 * to indicate that a calculated question has sub questions or variations to show rather than listing each 465 * subquestion or variation directly. 466 * 467 * @param calculated $randomquestioncalculated The calculated instance for the random question slot. 468 * @param calculated[] $subquestionstats The instances of the calculated stats of the questions that are being summarised. 469 * @return calculated_question_summary 470 */ 471 protected function make_new_calculated_question_summary_stat($randomquestioncalculated, $subquestionstats) { 472 $question = $randomquestioncalculated->question; 473 $slot = $randomquestioncalculated->slot; 474 $calculatedsummary = new calculated_question_summary($question, $slot, $subquestionstats); 475 476 return $calculatedsummary; 477 } 478 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body