Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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.'&nbsp;'.$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'] . '&nbsp;' .
 309                  question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
 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  }