See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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 * Quiz statistics report, table for showing statistics of each question in the quiz. 19 * 20 * @package quiz_statistics 21 * @copyright 2008 Jamie Pratt 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 require_once($CFG->libdir.'/tablelib.php'); 28 29 use \core_question\statistics\questions\calculated_question_summary; 30 31 /** 32 * This table has one row for each question in the quiz, with sub-rows when 33 * random questions and variants appear. 34 * 35 * There are columns for the various item and position statistics. 36 * 37 * @copyright 2008 Jamie Pratt 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class quiz_statistics_table extends flexible_table { 41 /** @var object the quiz settings. */ 42 protected $quiz; 43 44 /** @var integer the quiz course_module id. */ 45 protected $cmid; 46 47 /** 48 * Constructor. 49 */ 50 public function __construct() { 51 parent::__construct('mod-quiz-report-statistics-report'); 52 } 53 54 /** 55 * Set up the columns and headers and other properties of the table and then 56 * call flexible_table::setup() method. 57 * 58 * @param object $quiz the quiz settings 59 * @param int $cmid the quiz course_module id 60 * @param moodle_url $reporturl the URL to redisplay this report. 61 * @param int $s number of attempts included in the statistics. 62 */ 63 public function statistics_setup($quiz, $cmid, $reporturl, $s) { 64 $this->quiz = $quiz; 65 $this->cmid = $cmid; 66 67 // Define the table columns. 68 $columns = array(); 69 $headers = array(); 70 71 $columns[] = 'number'; 72 $headers[] = get_string('questionnumber', 'quiz_statistics'); 73 74 if (!$this->is_downloading()) { 75 $columns[] = 'icon'; 76 $headers[] = ''; 77 $columns[] = 'actions'; 78 $headers[] = ''; 79 } else { 80 $columns[] = 'qtype'; 81 $headers[] = get_string('questiontype', 'quiz_statistics'); 82 } 83 84 $columns[] = 'name'; 85 $headers[] = get_string('questionname', 'quiz'); 86 87 $columns[] = 's'; 88 $headers[] = get_string('attempts', 'quiz_statistics'); 89 90 if ($s > 1) { 91 $columns[] = 'facility'; 92 $headers[] = get_string('facility', 'quiz_statistics'); 93 94 $columns[] = 'sd'; 95 $headers[] = get_string('standarddeviationq', 'quiz_statistics'); 96 } 97 98 $columns[] = 'random_guess_score'; 99 $headers[] = get_string('random_guess_score', 'quiz_statistics'); 100 101 $columns[] = 'intended_weight'; 102 $headers[] = get_string('intended_weight', 'quiz_statistics'); 103 104 $columns[] = 'effective_weight'; 105 $headers[] = get_string('effective_weight', 'quiz_statistics'); 106 107 $columns[] = 'discrimination_index'; 108 $headers[] = get_string('discrimination_index', 'quiz_statistics'); 109 110 $columns[] = 'discriminative_efficiency'; 111 $headers[] = get_string('discriminative_efficiency', 'quiz_statistics'); 112 113 $this->define_columns($columns); 114 $this->define_headers($headers); 115 $this->sortable(false); 116 117 $this->column_class('s', 'numcol'); 118 $this->column_class('facility', 'numcol'); 119 $this->column_class('sd', 'numcol'); 120 $this->column_class('random_guess_score', 'numcol'); 121 $this->column_class('intended_weight', 'numcol'); 122 $this->column_class('effective_weight', 'numcol'); 123 $this->column_class('discrimination_index', 'numcol'); 124 $this->column_class('discriminative_efficiency', 'numcol'); 125 126 // Set up the table. 127 $this->define_baseurl($reporturl->out()); 128 129 $this->collapsible(true); 130 131 $this->set_attribute('id', 'questionstatistics'); 132 $this->set_attribute('class', 'generaltable generalbox boxaligncenter'); 133 134 parent::setup(); 135 } 136 137 /** 138 * Open a div tag to wrap statistics table. 139 */ 140 public function wrap_html_start() { 141 // Horrible Moodle 2.0 wide-content work-around. 142 if (!$this->is_downloading()) { 143 echo html_writer::start_tag('div', array('id' => 'tablecontainer', 144 'class' => 'statistics-tablecontainer')); 145 } 146 } 147 148 /** 149 * Close a statistics table div. 150 */ 151 public function wrap_html_finish() { 152 if (!$this->is_downloading()) { 153 echo html_writer::end_tag('div'); 154 } 155 } 156 157 /** 158 * The question number. 159 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 160 * @return string contents of this table cell. 161 */ 162 protected function col_number($questionstat) { 163 if ($this->is_calculated_question_summary($questionstat)) { 164 return ''; 165 } 166 if (!isset($questionstat->question->number)) { 167 return ''; 168 } 169 $number = $questionstat->question->number; 170 171 if (isset($questionstat->subqdisplayorder)) { 172 $number = $number . '.'.$questionstat->subqdisplayorder; 173 } 174 175 if ($questionstat->question->qtype != 'random' && !is_null($questionstat->variant)) { 176 $number = $number . '.'.$questionstat->variant; 177 } 178 179 return $number; 180 } 181 182 /** 183 * The question type icon. 184 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 185 * @return string contents of this table cell. 186 */ 187 protected function col_icon($questionstat) { 188 if ($this->is_calculated_question_summary($questionstat)) { 189 return ''; 190 } else { 191 return print_question_icon($questionstat->question, true); 192 } 193 } 194 195 /** 196 * Actions that can be performed on the question by this user (e.g. edit or preview). 197 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 198 * @return string contents of this table cell. 199 */ 200 protected function col_actions($questionstat) { 201 if ($this->is_calculated_question_summary($questionstat)) { 202 return ''; 203 } else { 204 return quiz_question_action_icons($this->quiz, $this->cmid, 205 $questionstat->question, $this->baseurl, $questionstat->variant); 206 } 207 } 208 209 /** 210 * The question type name. 211 * 212 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 213 * @return string contents of this table cell. 214 */ 215 protected function col_qtype($questionstat) { 216 return question_bank::get_qtype_name($questionstat->question->qtype); 217 } 218 219 /** 220 * The question name. 221 * 222 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 223 * @return string contents of this table cell. 224 */ 225 protected function col_name($questionstat) { 226 $name = $questionstat->question->name; 227 228 if (!is_null($questionstat->variant)) { 229 $a = new stdClass(); 230 $a->name = $name; 231 $a->variant = $questionstat->variant; 232 $name = get_string('nameforvariant', 'quiz_statistics', $a); 233 } 234 235 if ($this->is_downloading()) { 236 return $name; 237 } 238 239 $baseurl = new moodle_url($this->baseurl); 240 if (!is_null($questionstat->variant)) { 241 if ($questionstat->subquestion) { 242 // Variant of a sub-question. 243 $url = new moodle_url($baseurl, array('qid' => $questionstat->questionid, 'variant' => $questionstat->variant)); 244 $name = html_writer::link($url, $name, array('title' => get_string('detailedanalysisforvariant', 245 'quiz_statistics', 246 $questionstat->variant))); 247 } else if ($questionstat->slot) { 248 // Variant of a question in a slot. 249 $url = new moodle_url($baseurl, array('slot' => $questionstat->slot, 'variant' => $questionstat->variant)); 250 $name = html_writer::link($url, $name, array('title' => get_string('detailedanalysisforvariant', 251 'quiz_statistics', 252 $questionstat->variant))); 253 } 254 } else { 255 if ($questionstat->subquestion && !$questionstat->get_variants()) { 256 // Sub question without variants. 257 $url = new moodle_url($baseurl, array('qid' => $questionstat->questionid)); 258 $name = html_writer::link($url, $name, array('title' => get_string('detailedanalysis', 'quiz_statistics'))); 259 } else if ($baseurl->param('slot') === null && $questionstat->slot) { 260 // Question in a slot, we are not on a page showing structural analysis of one slot, 261 // we don't want linking on those pages. 262 $number = $questionstat->question->number; 263 $israndomquestion = $questionstat->question->qtype == 'random'; 264 $url = new moodle_url($baseurl, array('slot' => $questionstat->slot)); 265 266 if ($this->is_calculated_question_summary($questionstat)) { 267 // Only make the random question summary row name link to the slot structure 268 // analysis page with specific text to clearly indicate the link to the user. 269 // Random and variant question rows will render the name without a link to improve clarity 270 // in the UI. 271 $name = html_writer::div(get_string('rangeofvalues', 'quiz_statistics')); 272 } else if (!$israndomquestion && !$questionstat->get_variants() && !$questionstat->get_sub_question_ids()) { 273 // Question cannot be broken down into sub-questions or variants. Link will show response analysis page. 274 $name = html_writer::link($url, 275 $name, 276 array('title' => get_string('detailedanalysis', 'quiz_statistics'))); 277 } 278 } 279 } 280 281 282 if ($this->is_dubious_question($questionstat)) { 283 $name = html_writer::tag('div', $name, array('class' => 'dubious')); 284 } 285 286 if ($this->is_calculated_question_summary($questionstat)) { 287 $name .= html_writer::link($url, get_string('viewanalysis', 'quiz_statistics')); 288 } else if (!empty($questionstat->minmedianmaxnotice)) { 289 $name = get_string($questionstat->minmedianmaxnotice, 'quiz_statistics') . '<br />' . $name; 290 } 291 292 return $name; 293 } 294 295 /** 296 * The number of attempts at this question. 297 * 298 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 299 * @return string contents of this table cell. 300 */ 301 protected function col_s($questionstat) { 302 if ($this->is_calculated_question_summary($questionstat)) { 303 list($min, $max) = $questionstat->get_min_max_of('s'); 304 $min = $min ?: 0; 305 $max = $max ?: 0; 306 return $this->format_range($min, $max); 307 } else if (!isset($questionstat->s)) { 308 return 0; 309 } else { 310 return $questionstat->s; 311 } 312 } 313 314 /** 315 * The facility index (average fraction). 316 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 317 * @return string contents of this table cell. 318 */ 319 protected function col_facility($questionstat) { 320 if ($this->is_calculated_question_summary($questionstat)) { 321 list($min, $max) = $questionstat->get_min_max_of('facility'); 322 return $this->format_percentage_range($min, $max); 323 } else if (is_null($questionstat->facility)) { 324 return ''; 325 } else { 326 return $this->format_percentage($questionstat->facility); 327 } 328 } 329 330 /** 331 * The standard deviation of the fractions. 332 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 333 * @return string contents of this table cell. 334 */ 335 protected function col_sd($questionstat) { 336 if ($this->is_calculated_question_summary($questionstat)) { 337 list($min, $max) = $questionstat->get_min_max_of('sd'); 338 return $this->format_percentage_range($min, $max); 339 } else if (is_null($questionstat->sd) || $questionstat->maxmark == 0) { 340 return ''; 341 } else { 342 return $this->format_percentage($questionstat->sd / $questionstat->maxmark); 343 } 344 } 345 346 /** 347 * An estimate of the fraction a student would get by guessing randomly. 348 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 349 * @return string contents of this table cell. 350 */ 351 protected function col_random_guess_score($questionstat) { 352 if ($this->is_calculated_question_summary($questionstat)) { 353 list($min, $max) = $questionstat->get_min_max_of('randomguessscore'); 354 return $this->format_percentage_range($min, $max); 355 } else if (is_null($questionstat->randomguessscore)) { 356 return ''; 357 } else { 358 return $this->format_percentage($questionstat->randomguessscore); 359 } 360 } 361 362 /** 363 * The intended question weight. Maximum mark for the question as a percentage 364 * of maximum mark for the quiz. That is, the indended influence this question 365 * on the student's overall mark. 366 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 367 * @return string contents of this table cell. 368 */ 369 protected function col_intended_weight($questionstat) { 370 if ($this->is_calculated_question_summary($questionstat)) { 371 list($min, $max) = $questionstat->get_min_max_of('maxmark'); 372 373 if (is_null($min) && is_null($max)) { 374 return ''; 375 } else { 376 $min = quiz_report_scale_summarks_as_percentage($min, $this->quiz); 377 $max = quiz_report_scale_summarks_as_percentage($max, $this->quiz); 378 return $this->format_range($min, $max); 379 } 380 } else { 381 return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz); 382 } 383 } 384 385 /** 386 * The effective question weight. That is, an estimate of the actual 387 * influence this question has on the student's overall mark. 388 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 389 * @return string contents of this table cell. 390 */ 391 protected function col_effective_weight($questionstat) { 392 global $OUTPUT; 393 394 if ($this->is_calculated_question_summary($questionstat)) { 395 list($min, $max) = $questionstat->get_min_max_of('effectiveweight'); 396 397 if (is_null($min) && is_null($max)) { 398 return ''; 399 } else { 400 list( , $negcovar) = $questionstat->get_min_max_of('negcovar'); 401 if ($negcovar) { 402 $min = get_string('negcovar', 'quiz_statistics'); 403 } 404 405 return $this->format_range($min, $max); 406 } 407 } else if (is_null($questionstat->effectiveweight)) { 408 return ''; 409 } else if ($questionstat->negcovar) { 410 $negcovar = get_string('negcovar', 'quiz_statistics'); 411 412 if (!$this->is_downloading()) { 413 $negcovar = html_writer::tag('div', 414 $negcovar . $OUTPUT->help_icon('negcovar', 'quiz_statistics'), 415 array('class' => 'negcovar')); 416 } 417 418 return $negcovar; 419 } else { 420 return $this->format_percentage($questionstat->effectiveweight, false); 421 } 422 } 423 424 /** 425 * Discrimination index. This is the product moment correlation coefficient 426 * between the fraction for this question, and the average fraction for the 427 * other questions in this quiz. 428 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 429 * @return string contents of this table cell. 430 */ 431 protected function col_discrimination_index($questionstat) { 432 if ($this->is_calculated_question_summary($questionstat)) { 433 list($min, $max) = $questionstat->get_min_max_of('discriminationindex'); 434 435 if (isset($max)) { 436 $min = $min ?: 0; 437 } 438 439 if (is_numeric($min)) { 440 $min = $this->format_percentage($min, false); 441 } 442 if (is_numeric($max)) { 443 $max = $this->format_percentage($max, false); 444 } 445 446 return $this->format_range($min, $max); 447 } else if (!is_numeric($questionstat->discriminationindex)) { 448 return $questionstat->discriminationindex; 449 } else { 450 return $this->format_percentage($questionstat->discriminationindex, false); 451 } 452 } 453 454 /** 455 * Discrimination efficiency, similar to, but different from, the Discrimination index. 456 * 457 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 458 * @return string contents of this table cell. 459 */ 460 protected function col_discriminative_efficiency($questionstat) { 461 if ($this->is_calculated_question_summary($questionstat)) { 462 list($min, $max) = $questionstat->get_min_max_of('discriminativeefficiency'); 463 464 if (!is_numeric($min) && !is_numeric($max)) { 465 return ''; 466 } else { 467 return $this->format_percentage_range($min, $max, false); 468 } 469 } else if (!is_numeric($questionstat->discriminativeefficiency)) { 470 return ''; 471 } else { 472 return $this->format_percentage($questionstat->discriminativeefficiency, false); 473 } 474 } 475 476 /** 477 * This method encapsulates the test for wheter a question should be considered dubious. 478 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 479 * @return bool is this question possibly not pulling it's weight? 480 */ 481 protected function is_dubious_question($questionstat) { 482 if ($this->is_calculated_question_summary($questionstat)) { 483 // We only care about the minimum value here. 484 // If the minimum value is less than the threshold, then we know that there is at least one value below the threshold. 485 list($discriminativeefficiency) = $questionstat->get_min_max_of('discriminativeefficiency'); 486 } else { 487 $discriminativeefficiency = $questionstat->discriminativeefficiency; 488 } 489 490 if (!is_numeric($discriminativeefficiency)) { 491 return false; 492 } 493 494 return $discriminativeefficiency < 15; 495 } 496 497 /** 498 * Check if the given stats object is an instance of calculated_question_summary. 499 * 500 * @param \core_question\statistics\questions\calculated $questionstat Stats object 501 * @return bool 502 */ 503 protected function is_calculated_question_summary($questionstat) { 504 return $questionstat instanceof calculated_question_summary; 505 } 506 507 /** 508 * Format inputs to represent a range between $min and $max. 509 * This function does not check if $min is less than $max or not. 510 * If both $min and $max are equal to null, this function returns an empty string. 511 * 512 * @param string|null $min The minimum value in the range 513 * @param string|null $max The maximum value in the range 514 * @return string 515 */ 516 protected function format_range(string $min = null, string $max = null) { 517 if (is_null($min) && is_null($max)) { 518 return ''; 519 } else { 520 $a = new stdClass(); 521 $a->min = $min; 522 $a->max = $max; 523 524 return get_string('rangebetween', 'quiz_statistics', $a); 525 } 526 } 527 528 /** 529 * Format a number to a localised percentage with specified decimal points. 530 * 531 * @param float $number The number being formatted 532 * @param bool $fraction An indicator for whether the number is a fraction or is already multiplied by 100 533 * @param int $decimals Sets the number of decimal points 534 * @return string 535 */ 536 protected function format_percentage(float $number, bool $fraction = true, int $decimals = 2) { 537 $coefficient = $fraction ? 100 : 1; 538 return get_string('percents', 'moodle', format_float($number * $coefficient, $decimals)); 539 } 540 541 /** 542 * Format $min and $max to localised percentages and form a string that represents a range between them. 543 * This function does not check if $min is less than $max or not. 544 * If both $min and $max are equal to null, this function returns an empty string. 545 * 546 * @param float|null $min The minimum value of the range 547 * @param float|null $max The maximum value of the range 548 * @param bool $fraction An indicator for whether min and max are a fractions or are already multiplied by 100 549 * @param int $decimals Sets the number of decimal points 550 * @return string A formatted string that represents a range between $min to $max. 551 */ 552 protected function format_percentage_range(float $min = null, float $max = null, bool $fraction = true, int $decimals = 2) { 553 if (is_null($min) && is_null($max)) { 554 return ''; 555 } else { 556 $min = $min ?: 0; 557 $max = $max ?: 0; 558 return $this->format_range( 559 $this->format_percentage($min, $fraction, $decimals), 560 $this->format_percentage($max, $fraction, $decimals) 561 ); 562 } 563 } 564 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body