Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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 stdClass 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 stdClass $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 = []; 69 $headers = []; 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', ['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 $questionobject = $questionstat->question; 192 return print_question_icon($questionobject); 193 } 194 } 195 196 /** 197 * Actions that can be performed on the question by this user (e.g. edit or preview). 198 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 199 * @return string contents of this table cell. 200 */ 201 protected function col_actions($questionstat) { 202 if ($this->is_calculated_question_summary($questionstat)) { 203 return ''; 204 } else if ($questionstat->question->qtype === 'missingtype') { 205 return ''; 206 } else { 207 return quiz_question_action_icons($this->quiz, $this->cmid, 208 $questionstat->question, $this->baseurl, $questionstat->variant); 209 } 210 } 211 212 /** 213 * The question type name. 214 * 215 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 216 * @return string contents of this table cell. 217 */ 218 protected function col_qtype($questionstat) { 219 return question_bank::get_qtype_name($questionstat->question->qtype); 220 } 221 222 /** 223 * The question name. 224 * 225 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 226 * @return string contents of this table cell. 227 */ 228 protected function col_name($questionstat) { 229 $name = $questionstat->question->name; 230 231 if (!is_null($questionstat->variant)) { 232 $a = new stdClass(); 233 $a->name = $name; 234 $a->variant = $questionstat->variant; 235 $name = get_string('nameforvariant', 'quiz_statistics', $a); 236 } 237 238 if ($this->is_downloading()) { 239 return $name; 240 } 241 242 $baseurl = new moodle_url($this->baseurl); 243 if (!is_null($questionstat->variant)) { 244 if ($questionstat->subquestion) { 245 // Variant of a sub-question. 246 $url = new moodle_url($baseurl, ['qid' => $questionstat->questionid, 'variant' => $questionstat->variant]); 247 $name = html_writer::link($url, $name, ['title' => get_string('detailedanalysisforvariant', 248 'quiz_statistics', 249 $questionstat->variant)]); 250 } else if ($questionstat->slot) { 251 // Variant of a question in a slot. 252 $url = new moodle_url($baseurl, ['slot' => $questionstat->slot, 'variant' => $questionstat->variant]); 253 $name = html_writer::link($url, $name, ['title' => get_string('detailedanalysisforvariant', 254 'quiz_statistics', 255 $questionstat->variant)]); 256 } 257 } else { 258 if ($questionstat->subquestion && !$questionstat->get_variants()) { 259 // Sub question without variants. 260 $url = new moodle_url($baseurl, ['qid' => $questionstat->questionid]); 261 $name = html_writer::link($url, $name, ['title' => get_string('detailedanalysis', 'quiz_statistics')]); 262 } else if ($baseurl->param('slot') === null && $questionstat->slot) { 263 // Question in a slot, we are not on a page showing structural analysis of one slot, 264 // we don't want linking on those pages. 265 $number = $questionstat->question->number; 266 $israndomquestion = $questionstat->question->qtype == 'random'; 267 $url = new moodle_url($baseurl, ['slot' => $questionstat->slot]); 268 269 if ($this->is_calculated_question_summary($questionstat)) { 270 // Only make the random question summary row name link to the slot structure 271 // analysis page with specific text to clearly indicate the link to the user. 272 // Random and variant question rows will render the name without a link to improve clarity 273 // in the UI. 274 $name = html_writer::div(get_string('rangeofvalues', 'quiz_statistics')); 275 } else if (!$israndomquestion && !$questionstat->get_variants() && !$questionstat->get_sub_question_ids()) { 276 // Question cannot be broken down into sub-questions or variants. Link will show response analysis page. 277 $name = html_writer::link($url, 278 $name, 279 ['title' => get_string('detailedanalysis', 'quiz_statistics')]); 280 } 281 } 282 } 283 284 285 if ($this->is_dubious_question($questionstat)) { 286 $name = html_writer::tag('div', $name, ['class' => 'dubious']); 287 } 288 289 if ($this->is_calculated_question_summary($questionstat)) { 290 $name .= html_writer::link($url, get_string('viewanalysis', 'quiz_statistics')); 291 } else if (!empty($questionstat->minmedianmaxnotice)) { 292 $name = get_string($questionstat->minmedianmaxnotice, 'quiz_statistics') . '<br />' . $name; 293 } 294 295 return $name; 296 } 297 298 /** 299 * The number of attempts at this question. 300 * 301 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 302 * @return string contents of this table cell. 303 */ 304 protected function col_s($questionstat) { 305 if ($this->is_calculated_question_summary($questionstat)) { 306 list($min, $max) = $questionstat->get_min_max_of('s'); 307 $min = $min ?: 0; 308 $max = $max ?: 0; 309 return $this->format_range($min, $max); 310 } else if (!isset($questionstat->s)) { 311 return 0; 312 } else { 313 return $questionstat->s; 314 } 315 } 316 317 /** 318 * The facility index (average fraction). 319 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 320 * @return string contents of this table cell. 321 */ 322 protected function col_facility($questionstat) { 323 if ($this->is_calculated_question_summary($questionstat)) { 324 list($min, $max) = $questionstat->get_min_max_of('facility'); 325 return $this->format_percentage_range($min, $max); 326 } else if (is_null($questionstat->facility)) { 327 return ''; 328 } else { 329 return $this->format_percentage($questionstat->facility); 330 } 331 } 332 333 /** 334 * The standard deviation of the fractions. 335 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 336 * @return string contents of this table cell. 337 */ 338 protected function col_sd($questionstat) { 339 if ($this->is_calculated_question_summary($questionstat)) { 340 list($min, $max) = $questionstat->get_min_max_of('sd'); 341 return $this->format_percentage_range($min, $max); 342 } else if (is_null($questionstat->sd) || $questionstat->maxmark == 0) { 343 return ''; 344 } else { 345 return $this->format_percentage($questionstat->sd / $questionstat->maxmark); 346 } 347 } 348 349 /** 350 * An estimate of the fraction a student would get by guessing randomly. 351 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 352 * @return string contents of this table cell. 353 */ 354 protected function col_random_guess_score($questionstat) { 355 if ($this->is_calculated_question_summary($questionstat)) { 356 list($min, $max) = $questionstat->get_min_max_of('randomguessscore'); 357 return $this->format_percentage_range($min, $max); 358 } else if (is_null($questionstat->randomguessscore)) { 359 return ''; 360 } else { 361 return $this->format_percentage($questionstat->randomguessscore); 362 } 363 } 364 365 /** 366 * The intended question weight. Maximum mark for the question as a percentage 367 * of maximum mark for the quiz. That is, the indended influence this question 368 * on the student's overall mark. 369 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 370 * @return string contents of this table cell. 371 */ 372 protected function col_intended_weight($questionstat) { 373 if ($this->is_calculated_question_summary($questionstat)) { 374 list($min, $max) = $questionstat->get_min_max_of('maxmark'); 375 376 if (is_null($min) && is_null($max)) { 377 return ''; 378 } else { 379 $min = quiz_report_scale_summarks_as_percentage($min, $this->quiz); 380 $max = quiz_report_scale_summarks_as_percentage($max, $this->quiz); 381 return $this->format_range($min, $max); 382 } 383 } else { 384 return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz); 385 } 386 } 387 388 /** 389 * The effective question weight. That is, an estimate of the actual 390 * influence this question has on the student's overall mark. 391 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 392 * @return string contents of this table cell. 393 */ 394 protected function col_effective_weight($questionstat) { 395 global $OUTPUT; 396 397 if ($this->is_calculated_question_summary($questionstat)) { 398 list($min, $max) = $questionstat->get_min_max_of('effectiveweight'); 399 400 if (is_null($min) && is_null($max)) { 401 return ''; 402 } else { 403 list( , $negcovar) = $questionstat->get_min_max_of('negcovar'); 404 if ($negcovar) { 405 $min = get_string('negcovar', 'quiz_statistics'); 406 } 407 408 return $this->format_range($min, $max); 409 } 410 } else if (is_null($questionstat->effectiveweight)) { 411 return ''; 412 } else if ($questionstat->negcovar) { 413 $negcovar = get_string('negcovar', 'quiz_statistics'); 414 415 if (!$this->is_downloading()) { 416 $negcovar = html_writer::tag('div', 417 $negcovar . $OUTPUT->help_icon('negcovar', 'quiz_statistics'), 418 ['class' => 'negcovar']); 419 } 420 421 return $negcovar; 422 } else { 423 return $this->format_percentage($questionstat->effectiveweight, false); 424 } 425 } 426 427 /** 428 * Discrimination index. This is the product moment correlation coefficient 429 * between the fraction for this question, and the average fraction for the 430 * other questions in this quiz. 431 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 432 * @return string contents of this table cell. 433 */ 434 protected function col_discrimination_index($questionstat) { 435 if ($this->is_calculated_question_summary($questionstat)) { 436 list($min, $max) = $questionstat->get_min_max_of('discriminationindex'); 437 438 if (isset($max)) { 439 $min = $min ?: 0; 440 } 441 442 if (is_numeric($min)) { 443 $min = $this->format_percentage($min, false); 444 } 445 if (is_numeric($max)) { 446 $max = $this->format_percentage($max, false); 447 } 448 449 return $this->format_range($min, $max); 450 } else if (!is_numeric($questionstat->discriminationindex)) { 451 return $questionstat->discriminationindex; 452 } else { 453 return $this->format_percentage($questionstat->discriminationindex, false); 454 } 455 } 456 457 /** 458 * Discrimination efficiency, similar to, but different from, the Discrimination index. 459 * 460 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 461 * @return string contents of this table cell. 462 */ 463 protected function col_discriminative_efficiency($questionstat) { 464 if ($this->is_calculated_question_summary($questionstat)) { 465 list($min, $max) = $questionstat->get_min_max_of('discriminativeefficiency'); 466 467 if (!is_numeric($min) && !is_numeric($max)) { 468 return ''; 469 } else { 470 return $this->format_percentage_range($min, $max, false); 471 } 472 } else if (!is_numeric($questionstat->discriminativeefficiency)) { 473 return ''; 474 } else { 475 return $this->format_percentage($questionstat->discriminativeefficiency, false); 476 } 477 } 478 479 /** 480 * This method encapsulates the test for wheter a question should be considered dubious. 481 * @param \core_question\statistics\questions\calculated $questionstat stats for the question. 482 * @return bool is this question possibly not pulling it's weight? 483 */ 484 protected function is_dubious_question($questionstat) { 485 if ($this->is_calculated_question_summary($questionstat)) { 486 // We only care about the minimum value here. 487 // If the minimum value is less than the threshold, then we know that there is at least one value below the threshold. 488 list($discriminativeefficiency) = $questionstat->get_min_max_of('discriminativeefficiency'); 489 } else { 490 $discriminativeefficiency = $questionstat->discriminativeefficiency; 491 } 492 493 if (!is_numeric($discriminativeefficiency)) { 494 return false; 495 } 496 497 return $discriminativeefficiency < 15; 498 } 499 500 /** 501 * Check if the given stats object is an instance of calculated_question_summary. 502 * 503 * @param \core_question\statistics\questions\calculated $questionstat Stats object 504 * @return bool 505 */ 506 protected function is_calculated_question_summary($questionstat) { 507 return $questionstat instanceof calculated_question_summary; 508 } 509 510 /** 511 * Format inputs to represent a range between $min and $max. 512 * This function does not check if $min is less than $max or not. 513 * If both $min and $max are equal to null, this function returns an empty string. 514 * 515 * @param string|null $min The minimum value in the range 516 * @param string|null $max The maximum value in the range 517 * @return string 518 */ 519 protected function format_range(string $min = null, string $max = null) { 520 if (is_null($min) && is_null($max)) { 521 return ''; 522 } else { 523 $a = new stdClass(); 524 $a->min = $min; 525 $a->max = $max; 526 527 return get_string('rangebetween', 'quiz_statistics', $a); 528 } 529 } 530 531 /** 532 * Format a number to a localised percentage with specified decimal points. 533 * 534 * @param float $number The number being formatted 535 * @param bool $fraction An indicator for whether the number is a fraction or is already multiplied by 100 536 * @param int $decimals Sets the number of decimal points 537 * @return string 538 */ 539 protected function format_percentage(float $number, bool $fraction = true, int $decimals = 2) { 540 $coefficient = $fraction ? 100 : 1; 541 return get_string('percents', 'moodle', format_float($number * $coefficient, $decimals)); 542 } 543 544 /** 545 * Format $min and $max to localised percentages and form a string that represents a range between them. 546 * This function does not check if $min is less than $max or not. 547 * If both $min and $max are equal to null, this function returns an empty string. 548 * 549 * @param float|null $min The minimum value of the range 550 * @param float|null $max The maximum value of the range 551 * @param bool $fraction An indicator for whether min and max are a fractions or are already multiplied by 100 552 * @param int $decimals Sets the number of decimal points 553 * @return string A formatted string that represents a range between $min to $max. 554 */ 555 protected function format_percentage_range(float $min = null, float $max = null, bool $fraction = true, int $decimals = 2) { 556 if (is_null($min) && is_null($max)) { 557 return ''; 558 } else { 559 $min = $min ?: 0; 560 $max = $max ?: 0; 561 return $this->format_range( 562 $this->format_percentage($min, $fraction, $decimals), 563 $this->format_percentage($max, $fraction, $decimals) 564 ); 565 } 566 } 567 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body