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