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 * Quiz statistics report class. 19 * 20 * @package quiz_statistics 21 * @copyright 2014 Open University 22 * @author James Pratt <me@jamiep.org> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php'); 29 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php'); 30 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php'); 31 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php'); 32 /** 33 * The quiz statistics report provides summary information about each question in 34 * a quiz, compared to the whole quiz. It also provides a drill-down to more 35 * detailed information about each question. 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_report extends quiz_default_report { 41 42 /** @var context_module context of this quiz.*/ 43 protected $context; 44 45 /** @var quiz_statistics_table instance of table class used for main questions stats table. */ 46 protected $table; 47 48 /** @var \core\progress\base|null $progress Handles progress reporting or not. */ 49 protected $progress = null; 50 51 /** 52 * Display the report. 53 */ 54 public function display($quiz, $cm, $course) { 55 global $OUTPUT, $DB; 56 57 raise_memory_limit(MEMORY_HUGE); 58 59 $this->context = context_module::instance($cm->id); 60 61 if (!quiz_has_questions($quiz->id)) { 62 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics'); 63 echo quiz_no_questions_message($quiz, $cm, $this->context); 64 return true; 65 } 66 67 // Work out the display options. 68 $download = optional_param('download', '', PARAM_ALPHA); 69 $everything = optional_param('everything', 0, PARAM_BOOL); 70 $recalculate = optional_param('recalculate', 0, PARAM_BOOL); 71 // A qid paramter indicates we should display the detailed analysis of a sub question. 72 $qid = optional_param('qid', 0, PARAM_INT); 73 $slot = optional_param('slot', 0, PARAM_INT); 74 $variantno = optional_param('variant', null, PARAM_INT); 75 $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT); 76 $whichtries = optional_param('whichtries', question_attempt::LAST_TRY, PARAM_ALPHA); 77 78 $pageoptions = array(); 79 $pageoptions['id'] = $cm->id; 80 $pageoptions['mode'] = 'statistics'; 81 82 $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions); 83 84 $mform = new quiz_statistics_settings_form($reporturl, compact('quiz')); 85 86 $mform->set_data(array('whichattempts' => $whichattempts, 'whichtries' => $whichtries)); 87 88 if ($whichattempts != $quiz->grademethod) { 89 $reporturl->param('whichattempts', $whichattempts); 90 } 91 92 if ($whichtries != question_attempt::LAST_TRY) { 93 $reporturl->param('whichtries', $whichtries); 94 } 95 96 // Find out current groups mode. 97 $currentgroup = $this->get_current_group($cm, $course, $this->context); 98 $nostudentsingroup = false; // True if a group is selected and there is no one in it. 99 if (empty($currentgroup)) { 100 $currentgroup = 0; 101 $groupstudentsjoins = new \core\dml\sql_join(); 102 103 } else if ($currentgroup == self::NO_GROUPS_ALLOWED) { 104 $groupstudentsjoins = new \core\dml\sql_join(); 105 $nostudentsingroup = true; 106 107 } else { 108 // All users who can attempt quizzes and who are in the currently selected group. 109 $groupstudentsjoins = get_enrolled_with_capabilities_join($this->context, '', 110 array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $currentgroup); 111 if (!empty($groupstudentsjoins->joins)) { 112 $sql = "SELECT DISTINCT u.id 113 FROM {user} u 114 {$groupstudentsjoins->joins} 115 WHERE {$groupstudentsjoins->wheres}"; 116 if (!$DB->record_exists_sql($sql, $groupstudentsjoins->params)) { 117 $nostudentsingroup = true; 118 } 119 } 120 } 121 122 $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts); 123 124 // If recalculate was requested, handle that. 125 if ($recalculate && confirm_sesskey()) { 126 $this->clear_cached_data($qubaids); 127 redirect($reporturl); 128 } 129 130 // Set up the main table. 131 $this->table = new quiz_statistics_table(); 132 if ($everything) { 133 $report = get_string('completestatsfilename', 'quiz_statistics'); 134 } else { 135 $report = get_string('questionstatsfilename', 'quiz_statistics'); 136 } 137 $courseshortname = format_string($course->shortname, true, 138 array('context' => context_course::instance($course->id))); 139 $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name); 140 $this->table->is_downloading($download, $filename, 141 get_string('quizstructureanalysis', 'quiz_statistics')); 142 $questions = $this->load_and_initialise_questions_for_calculations($quiz); 143 144 // Print the page header stuff (if not downloading. 145 if (!$this->table->is_downloading()) { 146 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics'); 147 } 148 149 if (!$nostudentsingroup) { 150 // Get the data to be displayed. 151 $progress = $this->get_progress_trace_instance(); 152 list($quizstats, $questionstats) = 153 $this->get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudentsjoins, $questions, $progress); 154 } else { 155 // Or create empty stats containers. 156 $quizstats = new \quiz_statistics\calculated($whichattempts); 157 $questionstats = new \core_question\statistics\questions\all_calculated_for_qubaid_condition(); 158 } 159 160 // Set up the table. 161 $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s()); 162 163 // Print the rest of the page header stuff (if not downloading. 164 if (!$this->table->is_downloading()) { 165 166 if (groups_get_activity_groupmode($cm)) { 167 groups_print_activity_menu($cm, $reporturl->out()); 168 if ($currentgroup && $nostudentsingroup) { 169 $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics')); 170 } 171 } 172 173 if (!$this->table->is_downloading() && $quizstats->s() == 0) { 174 echo $OUTPUT->notification(get_string('nogradedattempts', 'quiz_statistics')); 175 } 176 177 foreach ($questionstats->any_error_messages() as $errormessage) { 178 echo $OUTPUT->notification($errormessage); 179 } 180 181 // Print display options form. 182 $mform->display(); 183 } 184 185 if ($everything) { // Implies is downloading. 186 // Overall report, then the analysis of each question. 187 $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz); 188 $this->download_quiz_info_table($quizinfo); 189 190 if ($quizstats->s()) { 191 $this->output_quiz_structure_analysis_table($questionstats); 192 193 if ($this->table->is_downloading() == 'html' && $quizstats->s() != 0) { 194 $this->output_statistics_graph($quiz->id, $qubaids); 195 } 196 197 $this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl, $whichtries); 198 } 199 200 $this->table->export_class_instance()->finish_document(); 201 202 } else if ($qid) { 203 // Report on an individual sub-question indexed questionid. 204 if (!$questionstats->has_subq($qid, $variantno)) { 205 print_error('questiondoesnotexist', 'question'); 206 } 207 208 $this->output_individual_question_data($quiz, $questionstats->for_subq($qid, $variantno)); 209 $this->output_individual_question_response_analysis($questionstats->for_subq($qid, $variantno)->question, 210 $variantno, 211 $questionstats->for_subq($qid, $variantno)->s, 212 $reporturl, 213 $qubaids, 214 $whichtries); 215 // Back to overview link. 216 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' . 217 get_string('backtoquizreport', 'quiz_statistics') . '</a>', 218 'boxaligncenter generalbox boxwidthnormal mdl-align'); 219 } else if ($slot) { 220 // Report on an individual question indexed by position. 221 if (!isset($questions[$slot])) { 222 print_error('questiondoesnotexist', 'question'); 223 } 224 225 if ($variantno === null && 226 ($questionstats->for_slot($slot)->get_sub_question_ids() 227 || $questionstats->for_slot($slot)->get_variants())) { 228 if (!$this->table->is_downloading()) { 229 $number = $questionstats->for_slot($slot)->question->number; 230 echo $OUTPUT->heading(get_string('slotstructureanalysis', 'quiz_statistics', $number), 3); 231 } 232 $this->table->define_baseurl(new moodle_url($reporturl, array('slot' => $slot))); 233 $this->table->format_and_add_array_of_rows($questionstats->structure_analysis_for_one_slot($slot)); 234 } else { 235 $this->output_individual_question_data($quiz, $questionstats->for_slot($slot, $variantno)); 236 $this->output_individual_question_response_analysis($questions[$slot], 237 $variantno, 238 $questionstats->for_slot($slot, $variantno)->s, 239 $reporturl, 240 $qubaids, 241 $whichtries); 242 } 243 if (!$this->table->is_downloading()) { 244 // Back to overview link. 245 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' . 246 get_string('backtoquizreport', 'quiz_statistics') . '</a>', 247 'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align'); 248 } else { 249 $this->table->finish_output(); 250 } 251 252 } else if ($this->table->is_downloading()) { 253 // Downloading overview report. 254 $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz); 255 $this->download_quiz_info_table($quizinfo); 256 if ($quizstats->s()) { 257 $this->output_quiz_structure_analysis_table($questionstats); 258 } 259 $this->table->export_class_instance()->finish_document(); 260 261 } else { 262 // On-screen display of overview report. 263 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3); 264 echo $this->output_caching_info($quizstats->timemodified, $quiz->id, $groupstudentsjoins, $whichattempts, $reporturl); 265 echo $this->everything_download_options($reporturl); 266 $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz); 267 echo $this->output_quiz_info_table($quizinfo); 268 if ($quizstats->s()) { 269 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3); 270 $this->output_quiz_structure_analysis_table($questionstats); 271 $this->output_statistics_graph($quiz, $qubaids); 272 } 273 } 274 275 return true; 276 } 277 278 /** 279 * Display the statistical and introductory information about a question. 280 * Only called when not downloading. 281 * 282 * @param object $quiz the quiz settings. 283 * @param \core_question\statistics\questions\calculated $questionstat the question to report on. 284 */ 285 protected function output_individual_question_data($quiz, $questionstat) { 286 global $OUTPUT; 287 288 // On-screen display. Show a summary of the question's place in the quiz, 289 // and the question statistics. 290 $datumfromtable = $this->table->format_row($questionstat); 291 292 // Set up the question info table. 293 $questioninfotable = new html_table(); 294 $questioninfotable->align = array('center', 'center'); 295 $questioninfotable->width = '60%'; 296 $questioninfotable->attributes['class'] = 'generaltable titlesleft'; 297 298 $questioninfotable->data = array(); 299 $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name); 300 $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'), 301 $questionstat->question->name.' '.$datumfromtable['actions']); 302 303 if ($questionstat->variant !== null) { 304 $questioninfotable->data[] = array(get_string('variant', 'quiz_statistics'), $questionstat->variant); 305 306 } 307 $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), 308 $datumfromtable['icon'] . ' ' . 309 question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . ' ' . 310 $datumfromtable['icon']); 311 $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'), 312 $questionstat->positions); 313 314 // Set up the question statistics table. 315 $questionstatstable = new html_table(); 316 $questionstatstable->align = array('center', 'center'); 317 $questionstatstable->width = '60%'; 318 $questionstatstable->attributes['class'] = 'generaltable titlesleft'; 319 320 unset($datumfromtable['number']); 321 unset($datumfromtable['icon']); 322 $actions = $datumfromtable['actions']; 323 unset($datumfromtable['actions']); 324 unset($datumfromtable['name']); 325 $labels = array( 326 's' => get_string('attempts', 'quiz_statistics'), 327 'facility' => get_string('facility', 'quiz_statistics'), 328 'sd' => get_string('standarddeviationq', 'quiz_statistics'), 329 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'), 330 'intended_weight' => get_string('intended_weight', 'quiz_statistics'), 331 'effective_weight' => get_string('effective_weight', 'quiz_statistics'), 332 'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'), 333 'discriminative_efficiency' => 334 get_string('discriminative_efficiency', 'quiz_statistics') 335 ); 336 foreach ($datumfromtable as $item => $value) { 337 $questionstatstable->data[] = array($labels[$item], $value); 338 } 339 340 // Display the various bits. 341 echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3); 342 echo html_writer::table($questioninfotable); 343 echo $this->render_question_text($questionstat->question); 344 echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3); 345 echo html_writer::table($questionstatstable); 346 } 347 348 /** 349 * Output question text in a box with urls appropriate for a preview of the question. 350 * 351 * @param object $question question data. 352 * @return string HTML of question text, ready for display. 353 */ 354 protected function render_question_text($question) { 355 global $OUTPUT; 356 357 $text = question_rewrite_question_preview_urls($question->questiontext, $question->id, 358 $question->contextid, 'question', 'questiontext', $question->id, 359 $this->context->id, 'quiz_statistics'); 360 361 return $OUTPUT->box(format_text($text, $question->questiontextformat, 362 array('noclean' => true, 'para' => false, 'overflowdiv' => true)), 363 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align'); 364 } 365 366 /** 367 * Display the response analysis for a question. 368 * 369 * @param object $question the question to report on. 370 * @param int|null $variantno the variant 371 * @param int $s 372 * @param moodle_url $reporturl the URL to redisplay this report. 373 * @param qubaid_condition $qubaids 374 * @param string $whichtries 375 */ 376 protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids, 377 $whichtries = question_attempt::LAST_TRY) { 378 global $OUTPUT; 379 380 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) { 381 return; 382 } 383 384 $qtable = new quiz_statistics_question_table($question->id); 385 $exportclass = $this->table->export_class_instance(); 386 $qtable->export_class_instance($exportclass); 387 if (!$this->table->is_downloading()) { 388 // Output an appropriate title. 389 echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3); 390 391 } else { 392 // Work out an appropriate title. 393 $a = clone($question); 394 $a->variant = $variantno; 395 396 if (!empty($question->number) && !is_null($variantno)) { 397 $questiontabletitle = get_string('analysisnovariant', 'quiz_statistics', $a); 398 } else if (!empty($question->number)) { 399 $questiontabletitle = get_string('analysisno', 'quiz_statistics', $a); 400 } else if (!is_null($variantno)) { 401 $questiontabletitle = get_string('analysisvariant', 'quiz_statistics', $a); 402 } else { 403 $questiontabletitle = get_string('analysisnameonly', 'quiz_statistics', $a); 404 } 405 406 if ($this->table->is_downloading() == 'html') { 407 $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle); 408 } 409 410 // Set up the table. 411 $exportclass->start_table($questiontabletitle); 412 413 if ($this->table->is_downloading() == 'html') { 414 echo $this->render_question_text($question); 415 } 416 } 417 418 $responesanalyser = new \core_question\statistics\responses\analyser($question, $whichtries); 419 $responseanalysis = $responesanalyser->load_cached($qubaids, $whichtries); 420 421 $qtable->question_setup($reporturl, $question, $s, $responseanalysis); 422 if ($this->table->is_downloading()) { 423 $exportclass->output_headers($qtable->headers); 424 } 425 426 // Where no variant no is specified the variant no is actually one. 427 if ($variantno === null) { 428 $variantno = 1; 429 } 430 foreach ($responseanalysis->get_subpart_ids($variantno) as $partid) { 431 $subpart = $responseanalysis->get_analysis_for_subpart($variantno, $partid); 432 foreach ($subpart->get_response_class_ids() as $responseclassid) { 433 $responseclass = $subpart->get_response_class($responseclassid); 434 $tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid); 435 foreach ($tabledata as $row) { 436 $qtable->add_data_keyed($qtable->format_row($row)); 437 } 438 } 439 } 440 441 $qtable->finish_output(!$this->table->is_downloading()); 442 } 443 444 /** 445 * Output the table that lists all the questions in the quiz with their statistics. 446 * 447 * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in 448 * the quiz including subqs and 449 * variants. 450 */ 451 protected function output_quiz_structure_analysis_table($questionstats) { 452 $limitvariants = !$this->table->is_downloading(); 453 foreach ($questionstats->get_all_slots() as $slot) { 454 // Output the data for these question statistics. 455 $structureanalysis = $questionstats->structure_analysis_for_one_slot($slot, $limitvariants); 456 if (is_null($structureanalysis)) { 457 $this->table->add_separator(); 458 } else { 459 foreach ($structureanalysis as $row) { 460 $bgcssclass = ''; 461 // The only way to identify in this point of the report if a row is a summary row 462 // is checking if it's a instance of calculated_question_summary class. 463 if ($row instanceof \core_question\statistics\questions\calculated_question_summary) { 464 // Apply a custom css class to summary row to remove border and reduce paddings. 465 $bgcssclass = 'quiz_statistics-summaryrow'; 466 467 // For question that contain a summary row, we add a "hidden" row in between so the report 468 // display both rows with same background color. 469 $this->table->add_data_keyed([], 'd-none hidden'); 470 } 471 472 $this->table->add_data_keyed($this->table->format_row($row), $bgcssclass); 473 } 474 } 475 } 476 477 $this->table->finish_output(!$this->table->is_downloading()); 478 } 479 480 /** 481 * Return HTML for table of overall quiz statistics. 482 * 483 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}. 484 * @return string the HTML. 485 */ 486 protected function output_quiz_info_table($quizinfo) { 487 488 $quizinfotable = new html_table(); 489 $quizinfotable->align = array('center', 'center'); 490 $quizinfotable->width = '60%'; 491 $quizinfotable->attributes['class'] = 'generaltable titlesleft'; 492 $quizinfotable->data = array(); 493 494 foreach ($quizinfo as $heading => $value) { 495 $quizinfotable->data[] = array($heading, $value); 496 } 497 498 return html_writer::table($quizinfotable); 499 } 500 501 /** 502 * Download the table of overall quiz statistics. 503 * 504 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}. 505 */ 506 protected function download_quiz_info_table($quizinfo) { 507 global $OUTPUT; 508 509 // HTML download is a special case. 510 if ($this->table->is_downloading() == 'html') { 511 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3); 512 echo $this->output_quiz_info_table($quizinfo); 513 return; 514 } 515 516 // Reformat the data ready for output. 517 $headers = array(); 518 $row = array(); 519 foreach ($quizinfo as $heading => $value) { 520 $headers[] = $heading; 521 $row[] = $value; 522 } 523 524 // Do the output. 525 $exportclass = $this->table->export_class_instance(); 526 $exportclass->start_table(get_string('quizinformation', 'quiz_statistics')); 527 $exportclass->output_headers($headers); 528 $exportclass->add_data($row); 529 $exportclass->finish_table(); 530 } 531 532 /** 533 * Output the HTML needed to show the statistics graph. 534 * 535 * @param int|object $quizorid The quiz, or its ID. 536 * @param qubaid_condition $qubaids the question usages whose responses to analyse. 537 * @param string $whichattempts Which attempts constant. 538 */ 539 protected function output_statistics_graph($quizorid, $qubaids) { 540 global $DB, $PAGE; 541 542 $quiz = $quizorid; 543 if (!is_object($quiz)) { 544 $quiz = $DB->get_record('quiz', array('id' => $quizorid), '*', MUST_EXIST); 545 } 546 547 // Load the rest of the required data. 548 $questions = quiz_report_get_significant_questions($quiz); 549 550 // Only load main question not sub questions. 551 $questionstatistics = $DB->get_records_select('question_statistics', 552 'hashcode = ? AND slot IS NOT NULL AND variant IS NULL', 553 [$qubaids->get_hash_code()]); 554 555 // Configure what to display. 556 $fieldstoplot = [ 557 'facility' => get_string('facility', 'quiz_statistics'), 558 'discriminativeefficiency' => get_string('discriminative_efficiency', 'quiz_statistics') 559 ]; 560 $fieldstoplotfactor = ['facility' => 100, 'discriminativeefficiency' => 1]; 561 562 // Prepare the arrays to hold the data. 563 $xdata = []; 564 foreach (array_keys($fieldstoplot) as $fieldtoplot) { 565 $ydata[$fieldtoplot] = []; 566 } 567 568 // Fill in the data for each question. 569 foreach ($questionstatistics as $questionstatistic) { 570 $number = $questions[$questionstatistic->slot]->number; 571 $xdata[$number] = $number; 572 573 foreach ($fieldstoplot as $fieldtoplot => $notused) { 574 $value = $questionstatistic->$fieldtoplot; 575 if (is_null($value)) { 576 $value = 0; 577 } 578 $value *= $fieldstoplotfactor[$fieldtoplot]; 579 $ydata[$fieldtoplot][$number] = number_format($value, 2); 580 } 581 } 582 583 // Create the chart. 584 sort($xdata); 585 $chart = new \core\chart_bar(); 586 $chart->get_xaxis(0, true)->set_label(get_string('position', 'quiz_statistics')); 587 $chart->set_labels(array_values($xdata)); 588 589 foreach ($fieldstoplot as $fieldtoplot => $notused) { 590 ksort($ydata[$fieldtoplot]); 591 $series = new \core\chart_series($fieldstoplot[$fieldtoplot], array_values($ydata[$fieldtoplot])); 592 $chart->add_series($series); 593 } 594 595 // Find max. 596 $max = 0; 597 foreach ($fieldstoplot as $fieldtoplot => $notused) { 598 $max = max($max, max($ydata[$fieldtoplot])); 599 } 600 601 // Set Y properties. 602 $yaxis = $chart->get_yaxis(0, true); 603 $yaxis->set_stepsize(10); 604 $yaxis->set_label('%'); 605 606 $output = $PAGE->get_renderer('mod_quiz'); 607 $graphname = get_string('statisticsreportgraph', 'quiz_statistics'); 608 echo $output->chart($chart, $graphname); 609 } 610 611 /** 612 * Get the quiz and question statistics, either by loading the cached results, 613 * or by recomputing them. 614 * 615 * @param object $quiz the quiz settings. 616 * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in 617 * $quiz->grademethod ie. 618 * QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST 619 * we calculate stats based on which attempts would affect the grade for each student. 620 * @param string $whichtries which tries to analyse for response analysis. Will be one of 621 * question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES. 622 * @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params for students in this group. 623 * @param array $questions full question data. 624 * @param \core\progress\base|null $progress 625 * @return array with 2 elements: - $quizstats The statistics for overall attempt scores. 626 * - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition 627 */ 628 public function get_all_stats_and_analysis( 629 $quiz, $whichattempts, $whichtries, \core\dml\sql_join $groupstudentsjoins, $questions, $progress = null) { 630 631 if ($progress === null) { 632 $progress = new \core\progress\none(); 633 } 634 635 $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts); 636 637 $qcalc = new \core_question\statistics\questions\calculator($questions, $progress); 638 639 $quizcalc = new \quiz_statistics\calculator($progress); 640 641 $progress->start_progress('', 3); 642 if ($quizcalc->get_last_calculated_time($qubaids) === false) { 643 644 // Recalculate now. 645 $questionstats = $qcalc->calculate($qubaids); 646 $progress->progress(1); 647 648 $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudentsjoins, count($questions), 649 $qcalc->get_sum_of_mark_variance()); 650 $progress->progress(2); 651 } else { 652 $quizstats = $quizcalc->get_cached($qubaids); 653 $progress->progress(1); 654 $questionstats = $qcalc->get_cached($qubaids); 655 $progress->progress(2); 656 } 657 658 if ($quizstats->s()) { 659 $subquestions = $questionstats->get_sub_questions(); 660 $this->analyse_responses_for_all_questions_and_subquestions($questions, 661 $subquestions, 662 $qubaids, 663 $whichtries, 664 $progress); 665 } 666 $progress->progress(3); 667 $progress->end_progress(); 668 669 return array($quizstats, $questionstats); 670 } 671 672 /** 673 * Appropriate instance depending if we want html output for the user or not. 674 * 675 * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress. 676 */ 677 protected function get_progress_trace_instance() { 678 if ($this->progress === null) { 679 if (!$this->table->is_downloading()) { 680 $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics')); 681 $this->progress->set_display_names(); 682 } else { 683 $this->progress = new \core\progress\none(); 684 } 685 } 686 return $this->progress; 687 } 688 689 /** 690 * Analyse responses for all questions and sub questions in this quiz. 691 * 692 * @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations 693 * @param object[] $subquestions full question objects. 694 * @param qubaid_condition $qubaids the question usages whose responses to analyse. 695 * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES. 696 * @param null|\core\progress\base $progress Used to indicate progress of task. 697 */ 698 protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids, 699 $whichtries, $progress = null) { 700 if ($progress === null) { 701 $progress = new \core\progress\none(); 702 } 703 704 // Starting response analysis tasks. 705 $progress->start_progress('', count($questions) + count($subquestions)); 706 707 $done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress); 708 709 $this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done); 710 711 // Finished all response analysis tasks. 712 $progress->end_progress(); 713 } 714 715 /** 716 * Analyse responses for an array of questions or sub questions. 717 * 718 * @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations. 719 * @param qubaid_condition $qubaids the question usages whose responses to analyse. 720 * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES. 721 * @param null|\core\progress\base $progress Used to indicate progress of task. 722 * @param int[] $done array keys are ids of questions that have been analysed before calling method. 723 * @return array array keys are ids of questions that were analysed after this method call. 724 */ 725 protected function analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress = null, $done = array()) { 726 $countquestions = count($questions); 727 if (!$countquestions) { 728 return array(); 729 } 730 if ($progress === null) { 731 $progress = new \core\progress\none(); 732 } 733 $progress->start_progress('', $countquestions, $countquestions); 734 foreach ($questions as $question) { 735 $progress->increment_progress(); 736 if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses() && !isset($done[$question->id])) { 737 $responesstats = new \core_question\statistics\responses\analyser($question, $whichtries); 738 if ($responesstats->get_last_analysed_time($qubaids, $whichtries) === false) { 739 $responesstats->calculate($qubaids, $whichtries); 740 } 741 } 742 $done[$question->id] = 1; 743 } 744 $progress->end_progress(); 745 return $done; 746 } 747 748 /** 749 * Return a little form for the user to request to download the full report, including quiz stats and response analysis for 750 * all questions and sub-questions. 751 * 752 * @param moodle_url $reporturl the base URL of the report. 753 * @return string HTML. 754 */ 755 protected function everything_download_options(moodle_url $reporturl) { 756 global $OUTPUT; 757 return $OUTPUT->download_dataformat_selector(get_string('downloadeverything', 'quiz_statistics'), 758 $reporturl->out_omit_querystring(), 'download', $reporturl->params() + array('everything' => 1)); 759 } 760 761 /** 762 * Return HTML for a message that says when the stats were last calculated and a 'recalculate now' button. 763 * 764 * @param int $lastcachetime the time the stats were last cached. 765 * @param int $quizid the quiz id. 766 * @param array $groupstudentsjoins (joins, wheres, params) for students in the group or empty array if groups not used. 767 * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in 768 * $quiz->grademethod ie. 769 * QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST 770 * we calculate stats based on which attempts would affect the grade for each student. 771 * @param moodle_url $reporturl url for this report 772 * @return string HTML. 773 */ 774 protected function output_caching_info($lastcachetime, $quizid, $groupstudentsjoins, $whichattempts, $reporturl) { 775 global $DB, $OUTPUT; 776 777 if (empty($lastcachetime)) { 778 return ''; 779 } 780 781 // Find the number of attempts since the cached statistics were computed. 782 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudentsjoins, $whichattempts, true); 783 $count = $DB->count_records_sql(" 784 SELECT COUNT(1) 785 FROM $fromqa 786 WHERE $whereqa 787 AND quiza.timefinish > {$lastcachetime}", $qaparams); 788 789 if (!$count) { 790 $count = 0; 791 } 792 793 // Generate the output. 794 $a = new stdClass(); 795 $a->lastcalculated = format_time(time() - $lastcachetime); 796 $a->count = $count; 797 798 $recalcualteurl = new moodle_url($reporturl, 799 array('recalculate' => 1, 'sesskey' => sesskey())); 800 $output = ''; 801 $output .= $OUTPUT->box_start( 802 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice'); 803 $output .= get_string('lastcalculated', 'quiz_statistics', $a); 804 $output .= $OUTPUT->single_button($recalcualteurl, 805 get_string('recalculatenow', 'quiz_statistics')); 806 $output .= $OUTPUT->box_end(true); 807 808 return $output; 809 } 810 811 /** 812 * Clear the cached data for a particular report configuration. This will trigger a re-computation the next time the report 813 * is displayed. 814 * 815 * @param $qubaids qubaid_condition 816 */ 817 protected function clear_cached_data($qubaids) { 818 global $DB; 819 $DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code())); 820 $DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code())); 821 $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code())); 822 } 823 824 /** 825 * Load the questions in this quiz and add some properties to the objects needed in the reports. 826 * 827 * @param object $quiz the quiz. 828 * @return array of questions for this quiz. 829 */ 830 public function load_and_initialise_questions_for_calculations($quiz) { 831 // Load the questions. 832 $questions = quiz_report_get_significant_questions($quiz); 833 $questionids = array(); 834 foreach ($questions as $question) { 835 $questionids[] = $question->id; 836 } 837 $fullquestions = question_load_questions($questionids); 838 foreach ($questions as $qno => $question) { 839 $q = $fullquestions[$question->id]; 840 $q->maxmark = $question->maxmark; 841 $q->slot = $qno; 842 $q->number = $question->number; 843 $questions[$qno] = $q; 844 } 845 return $questions; 846 } 847 848 /** 849 * Output all response analysis for all questions, sub-questions and variants. For download in a number of formats. 850 * 851 * @param $qubaids 852 * @param $questions 853 * @param $questionstats 854 * @param $reporturl 855 * @param $whichtries string 856 */ 857 protected function output_all_question_response_analysis($qubaids, 858 $questions, 859 $questionstats, 860 $reporturl, 861 $whichtries = question_attempt::LAST_TRY) { 862 foreach ($questions as $slot => $question) { 863 if (question_bank::get_qtype( 864 $question->qtype, false)->can_analyse_responses() 865 ) { 866 if ($questionstats->for_slot($slot)->get_variants()) { 867 foreach ($questionstats->for_slot($slot)->get_variants() as $variantno) { 868 $this->output_individual_question_response_analysis($question, 869 $variantno, 870 $questionstats->for_slot($slot, $variantno)->s, 871 $reporturl, 872 $qubaids, 873 $whichtries); 874 } 875 } else { 876 $this->output_individual_question_response_analysis($question, 877 null, 878 $questionstats->for_slot($slot)->s, 879 $reporturl, 880 $qubaids, 881 $whichtries); 882 } 883 } else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) { 884 foreach ($subqids as $subqid) { 885 if ($variants = $questionstats->for_subq($subqid)->get_variants()) { 886 foreach ($variants as $variantno) { 887 $this->output_individual_question_response_analysis( 888 $questionstats->for_subq($subqid, $variantno)->question, 889 $variantno, 890 $questionstats->for_subq($subqid, $variantno)->s, 891 $reporturl, 892 $qubaids, 893 $whichtries); 894 } 895 } else { 896 $this->output_individual_question_response_analysis( 897 $questionstats->for_subq($subqid)->question, 898 null, 899 $questionstats->for_subq($subqid)->s, 900 $reporturl, 901 $qubaids, 902 $whichtries); 903 904 } 905 } 906 } 907 } 908 } 909 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body