Search moodle.org's
Developer Documentation

   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->id, $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.'&nbsp;'.$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'] . '&nbsp;' .
 306                  question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
 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 $quizid
 514       * @param $currentgroup
 515       * @param $whichattempts
 516       */
 517      protected function output_statistics_graph($quizid, $currentgroup, $whichattempts) {
 518          global $PAGE;
 519  
 520          $output = $PAGE->get_renderer('mod_quiz');
 521          $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
 522                                      compact('quizid', 'currentgroup', 'whichattempts'));
 523          $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
 524          echo $output->graph($imageurl, $graphname);
 525      }
 526  
 527      /**
 528       * Get the quiz and question statistics, either by loading the cached results,
 529       * or by recomputing them.
 530       *
 531       * @param object $quiz               the quiz settings.
 532       * @param string $whichattempts      which attempts to use, represented internally as one of the constants as used in
 533       *                                   $quiz->grademethod ie.
 534       *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
 535       *                                   we calculate stats based on which attempts would affect the grade for each student.
 536       * @param string $whichtries         which tries to analyse for response analysis. Will be one of
 537       *                                   question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
 538       * @param array  $groupstudents      students in this group.
 539       * @param array  $questions          full question data.
 540       * @param \core\progress\base|null   $progress
 541       * @return array with 2 elements:    - $quizstats The statistics for overall attempt scores.
 542       *                                   - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition
 543       */
 544      public function get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudents, $questions, $progress = null) {
 545  
 546          if ($progress === null) {
 547              $progress = new \core\progress\null();
 548          }
 549  
 550          $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
 551  
 552          $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
 553  
 554          $quizcalc = new \quiz_statistics\calculator($progress);
 555  
 556          $progress->start_progress('', 3);
 557          if ($quizcalc->get_last_calculated_time($qubaids) === false) {
 558  
 559              // Recalculate now.
 560              $questionstats = $qcalc->calculate($qubaids);
 561              $progress->progress(1);
 562  
 563              $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
 564                                                $qcalc->get_sum_of_mark_variance());
 565              $progress->progress(2);
 566          } else {
 567              $quizstats = $quizcalc->get_cached($qubaids);
 568              $progress->progress(1);
 569              $questionstats = $qcalc->get_cached($qubaids);
 570              $progress->progress(2);
 571          }
 572  
 573          if ($quizstats->s()) {
 574              $subquestions = $questionstats->get_sub_questions();
 575              $this->analyse_responses_for_all_questions_and_subquestions($questions,
 576                                                                          $subquestions,
 577                                                                          $qubaids,
 578                                                                          $whichtries,
 579                                                                          $progress);
 580          }
 581          $progress->progress(3);
 582          $progress->end_progress();
 583  
 584          return array($quizstats, $questionstats);
 585      }
 586  
 587      /**
 588       * Appropriate instance depending if we want html output for the user or not.
 589       *
 590       * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.
 591       */
 592      protected function get_progress_trace_instance() {
 593          if ($this->progress === null) {
 594              if (!$this->table->is_downloading()) {
 595                  $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
 596                  $this->progress->set_display_names();
 597              } else {
 598                  $this->progress = new \core\progress\null();
 599              }
 600          }
 601          return $this->progress;
 602      }
 603  
 604      /**
 605       * Analyse responses for all questions and sub questions in this quiz.
 606       *
 607       * @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations
 608       * @param object[] $subquestions full question objects.
 609       * @param qubaid_condition $qubaids the question usages whose responses to analyse.
 610       * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
 611       * @param null|\core\progress\base $progress Used to indicate progress of task.
 612       */
 613      protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids,
 614                                                                              $whichtries, $progress = null) {
 615          if ($progress === null) {
 616              $progress = new \core\progress\null();
 617          }
 618  
 619          // Starting response analysis tasks.
 620          $progress->start_progress('', count($questions) + count($subquestions));
 621  
 622          $done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress);
 623  
 624          $this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done);
 625  
 626          // Finished all response analysis tasks.
 627          $progress->end_progress();
 628      }
 629  
 630      /**
 631       * Analyse responses for an array of questions or sub questions.
 632       *
 633       * @param object[] $questions  as returned by self::load_and_initialise_questions_for_calculations.
 634       * @param qubaid_condition $qubaids the question usages whose responses to analyse.
 635       * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
 636       * @param null|\core\progress\base $progress Used to indicate progress of task.
 637       * @param int[] $done array keys are ids of questions that have been analysed before calling method.
 638       * @return array array keys are ids of questions that were analysed after this method call.
 639       */
 640      protected function analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress = null, $done = array()) {
 641          $countquestions = count($questions);
 642          if (!$countquestions) {
 643              return array();
 644          }
 645          if ($progress === null) {
 646              $progress = new \core\progress\null();
 647          }
 648          $progress->start_progress('', $countquestions, $countquestions);
 649          foreach ($questions as $question) {
 650              $progress->increment_progress();
 651              if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses()  && !isset($done[$question->id])) {
 652                  $responesstats = new \core_question\statistics\responses\analyser($question, $whichtries);
 653                  if ($responesstats->get_last_analysed_time($qubaids, $whichtries) === false) {
 654                      $responesstats->calculate($qubaids, $whichtries);
 655                  }
 656              }
 657              $done[$question->id] = 1;
 658          }
 659          $progress->end_progress();
 660          return $done;
 661      }
 662  
 663      /**
 664       * Return a little form for the user to request to download the full report, including quiz stats and response analysis for
 665       * all questions and sub-questions.
 666       *
 667       * @return string HTML.
 668       */
 669      protected function everything_download_options() {
 670          $downloadoptions = $this->table->get_download_menu();
 671  
 672          $downloadelements = new stdClass();
 673          $downloadelements->formatsmenu = html_writer::select($downloadoptions, 'download',
 674                  $this->table->defaultdownloadformat, false);
 675          $downloadelements->downloadbutton = '<input type="submit" value="' .
 676                  get_string('download') . '"/>';
 677  
 678          $output = '<form action="'. $this->table->baseurl .'" method="post">';
 679          $output .= '<div class="mdl-align">';
 680          $output .= '<input type="hidden" name="everything" value="1"/>';
 681          $output .= html_writer::tag('label', get_string('downloadeverything', 'quiz_statistics', $downloadelements));
 682          $output .= '</div></form>';
 683  
 684          return $output;
 685      }
 686  
 687      /**
 688       * Return HTML for a message that says when the stats were last calculated and a 'recalculate now' button.
 689       *
 690       * @param int    $lastcachetime  the time the stats were last cached.
 691       * @param int    $quizid         the quiz id.
 692       * @param array  $groupstudents  ids of students in the group or empty array if groups not used.
 693       * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
 694       *                                   $quiz->grademethod ie.
 695       *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
 696       *                                   we calculate stats based on which attempts would affect the grade for each student.
 697       * @param moodle_url $reporturl url for this report
 698       * @return string HTML.
 699       */
 700      protected function output_caching_info($lastcachetime, $quizid, $groupstudents, $whichattempts, $reporturl) {
 701          global $DB, $OUTPUT;
 702  
 703          if (empty($lastcachetime)) {
 704              return '';
 705          }
 706  
 707          // Find the number of attempts since the cached statistics were computed.
 708          list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, true);
 709          $count = $DB->count_records_sql("
 710                  SELECT COUNT(1)
 711                  FROM $fromqa
 712                  WHERE $whereqa
 713                  AND quiza.timefinish > {$lastcachetime}", $qaparams);
 714  
 715          if (!$count) {
 716              $count = 0;
 717          }
 718  
 719          // Generate the output.
 720          $a = new stdClass();
 721          $a->lastcalculated = format_time(time() - $lastcachetime);
 722          $a->count = $count;
 723  
 724          $recalcualteurl = new moodle_url($reporturl,
 725                  array('recalculate' => 1, 'sesskey' => sesskey()));
 726          $output = '';
 727          $output .= $OUTPUT->box_start(
 728                  'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
 729          $output .= get_string('lastcalculated', 'quiz_statistics', $a);
 730          $output .= $OUTPUT->single_button($recalcualteurl,
 731                  get_string('recalculatenow', 'quiz_statistics'));
 732          $output .= $OUTPUT->box_end(true);
 733  
 734          return $output;
 735      }
 736  
 737      /**
 738       * Clear the cached data for a particular report configuration. This will trigger a re-computation the next time the report
 739       * is displayed.
 740       *
 741       * @param $qubaids qubaid_condition
 742       */
 743      protected function clear_cached_data($qubaids) {
 744          global $DB;
 745          $DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
 746          $DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
 747          $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
 748      }
 749  
 750      /**
 751       * Load the questions in this quiz and add some properties to the objects needed in the reports.
 752       *
 753       * @param object $quiz the quiz.
 754       * @return array of questions for this quiz.
 755       */
 756      public function load_and_initialise_questions_for_calculations($quiz) {
 757          // Load the questions.
 758          $questions = quiz_report_get_significant_questions($quiz);
 759          $questionids = array();
 760          foreach ($questions as $question) {
 761              $questionids[] = $question->id;
 762          }
 763          $fullquestions = question_load_questions($questionids);
 764          foreach ($questions as $qno => $question) {
 765              $q = $fullquestions[$question->id];
 766              $q->maxmark = $question->maxmark;
 767              $q->slot = $qno;
 768              $q->number = $question->number;
 769              $questions[$qno] = $q;
 770          }
 771          return $questions;
 772      }
 773  
 774      /**
 775       * Output all response analysis for all questions, sub-questions and variants. For download in a number of formats.
 776       *
 777       * @param $qubaids
 778       * @param $questions
 779       * @param $questionstats
 780       * @param $reporturl
 781       * @param $whichtries string
 782       */
 783      protected function output_all_question_response_analysis($qubaids,
 784                                                               $questions,
 785                                                               $questionstats,
 786                                                               $reporturl,
 787                                                               $whichtries = question_attempt::LAST_TRY) {
 788          foreach ($questions as $slot => $question) {
 789              if (question_bank::get_qtype(
 790                  $question->qtype, false)->can_analyse_responses()
 791              ) {
 792                  if ($questionstats->for_slot($slot)->get_variants()) {
 793                      foreach ($questionstats->for_slot($slot)->get_variants() as $variantno) {
 794                          $this->output_individual_question_response_analysis($question,
 795                                                                              $variantno,
 796                                                                              $questionstats->for_slot($slot, $variantno)->s,
 797                                                                              $reporturl,
 798                                                                              $qubaids,
 799                                                                              $whichtries);
 800                      }
 801                  } else {
 802                      $this->output_individual_question_response_analysis($question,
 803                                                                          null,
 804                                                                          $questionstats->for_slot($slot)->s,
 805                                                                          $reporturl,
 806                                                                          $qubaids,
 807                                                                          $whichtries);
 808                  }
 809              } else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
 810                  foreach ($subqids as $subqid) {
 811                      if ($variants = $questionstats->for_subq($subqid)->get_variants()) {
 812                          foreach ($variants as $variantno) {
 813                              $this->output_individual_question_response_analysis(
 814                                  $questionstats->for_subq($subqid, $variantno)->question,
 815                                  $variantno,
 816                                  $questionstats->for_subq($subqid, $variantno)->s,
 817                                  $reporturl,
 818                                  $qubaids,
 819                                  $whichtries);
 820                          }
 821                      } else {
 822                          $this->output_individual_question_response_analysis(
 823                              $questionstats->for_subq($subqid)->question,
 824                              null,
 825                              $questionstats->for_subq($subqid)->s,
 826                              $reporturl,
 827                              $qubaids,
 828                              $whichtries);
 829  
 830                      }
 831                  }
 832              }
 833          }
 834      }
 835  }

Search This Site: