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