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 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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   * This file defines the quiz overview report class.
  19   *
  20   * @package   quiz_overview
  21   * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport.php');
  29  require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_options.php');
  30  require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_form.php');
  31  require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_table.php');
  32  
  33  
  34  /**
  35   * Quiz report subclass for the overview (grades) report.
  36   *
  37   * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
  38   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class quiz_overview_report extends quiz_attempts_report {
  41  
  42      /**
  43       * @var bool whether there are actually students to show, given the options.
  44       */
  45      protected $hasgroupstudents;
  46  
  47      public function display($quiz, $cm, $course) {
  48          global $DB, $OUTPUT, $PAGE;
  49  
  50          list($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins) = $this->init(
  51                  'overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
  52  
  53          $options = new quiz_overview_options('overview', $quiz, $cm, $course);
  54  
  55          if ($fromform = $this->form->get_data()) {
  56              $options->process_settings_from_form($fromform);
  57  
  58          } else {
  59              $options->process_settings_from_params();
  60          }
  61  
  62          $this->form->set_data($options->get_initial_form_data());
  63  
  64          // Load the required questions.
  65          $questions = quiz_report_get_significant_questions($quiz);
  66  
  67          // Prepare for downloading, if applicable.
  68          $courseshortname = format_string($course->shortname, true,
  69                  array('context' => context_course::instance($course->id)));
  70          $table = new quiz_overview_table($quiz, $this->context, $this->qmsubselect,
  71                  $options, $groupstudentsjoins, $studentsjoins, $questions, $options->get_url());
  72          $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
  73                  $courseshortname, $quiz->name);
  74          $table->is_downloading($options->download, $filename,
  75                  $courseshortname . ' ' . format_string($quiz->name, true));
  76          if ($table->is_downloading()) {
  77              raise_memory_limit(MEMORY_EXTRA);
  78          }
  79  
  80          $this->hasgroupstudents = false;
  81          if (!empty($groupstudentsjoins->joins)) {
  82              $sql = "SELECT DISTINCT u.id
  83                        FROM {user} u
  84                      $groupstudentsjoins->joins
  85                       WHERE $groupstudentsjoins->wheres";
  86              $this->hasgroupstudents = $DB->record_exists_sql($sql, $groupstudentsjoins->params);
  87          }
  88          $hasstudents = false;
  89          if (!empty($studentsjoins->joins)) {
  90              $sql = "SELECT DISTINCT u.id
  91                      FROM {user} u
  92                      $studentsjoins->joins
  93                      WHERE $studentsjoins->wheres";
  94              $hasstudents = $DB->record_exists_sql($sql, $studentsjoins->params);
  95          }
  96          if ($options->attempts == self::ALL_WITH) {
  97              // This option is only available to users who can access all groups in
  98              // groups mode, so setting allowed to empty (which means all quiz attempts
  99              // are accessible, is not a security porblem.
 100              $allowedjoins = new \core\dml\sql_join();
 101          }
 102  
 103          $this->course = $course; // Hack to make this available in process_actions.
 104          $this->process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $options->get_url());
 105  
 106          $hasquestions = quiz_has_questions($quiz->id);
 107  
 108          // Start output.
 109          if (!$table->is_downloading()) {
 110              // Only print headers if not asked to download data.
 111              $this->print_standard_header_and_messages($cm, $course, $quiz,
 112                      $options, $currentgroup, $hasquestions, $hasstudents);
 113  
 114              // Print the display options.
 115              $this->form->display();
 116          }
 117  
 118          $hasstudents = $hasstudents && (!$currentgroup || $this->hasgroupstudents);
 119          if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
 120              // Construct the SQL.
 121              $table->setup_sql_queries($allowedjoins);
 122  
 123              if (!$table->is_downloading()) {
 124                  // Output the regrade buttons.
 125                  if (has_capability('mod/quiz:regrade', $this->context)) {
 126                      $regradesneeded = $this->count_question_attempts_needing_regrade(
 127                              $quiz, $groupstudentsjoins);
 128                      if ($currentgroup) {
 129                          $a= new stdClass();
 130                          $a->groupname = groups_get_group_name($currentgroup);
 131                          $a->coursestudents = get_string('participants');
 132                          $a->countregradeneeded = $regradesneeded;
 133                          $regradealldrydolabel =
 134                                  get_string('regradealldrydogroup', 'quiz_overview', $a);
 135                          $regradealldrylabel =
 136                                  get_string('regradealldrygroup', 'quiz_overview', $a);
 137                          $regradealllabel =
 138                                  get_string('regradeallgroup', 'quiz_overview', $a);
 139                      } else {
 140                          $regradealldrydolabel =
 141                                  get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
 142                          $regradealldrylabel =
 143                                  get_string('regradealldry', 'quiz_overview');
 144                          $regradealllabel =
 145                                  get_string('regradeall', 'quiz_overview');
 146                      }
 147                      $displayurl = new moodle_url($options->get_url(), array('sesskey' => sesskey()));
 148                      echo '<div class="mdl-align">';
 149                      echo '<form action="'.$displayurl->out_omit_querystring().'">';
 150                      echo '<div>';
 151                      echo html_writer::input_hidden_params($displayurl);
 152                      echo '<input type="submit" class="btn btn-secondary" name="regradeall" value="'.$regradealllabel.'"/>';
 153                      echo '<input type="submit" class="btn btn-secondary ml-1" name="regradealldry" value="' .
 154                              $regradealldrylabel . '"/>';
 155                      if ($regradesneeded) {
 156                          echo '<input type="submit" class="btn btn-secondary ml-1" name="regradealldrydo" value="' .
 157                                  $regradealldrydolabel . '"/>';
 158                      }
 159                      echo '</div>';
 160                      echo '</form>';
 161                      echo '</div>';
 162                  }
 163                  // Print information on the grading method.
 164                  if ($strattempthighlight = quiz_report_highlighting_grading_method(
 165                          $quiz, $this->qmsubselect, $options->onlygraded)) {
 166                      echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
 167                  }
 168              }
 169  
 170              // Define table columns.
 171              $columns = array();
 172              $headers = array();
 173  
 174              if (!$table->is_downloading() && $options->checkboxcolumn) {
 175                  $columnname = 'checkbox';
 176                  $columns[] = $columnname;
 177                  $headers[] = $table->checkbox_col_header($columnname);
 178              }
 179  
 180              $this->add_user_columns($table, $columns, $headers);
 181              $this->add_state_column($columns, $headers);
 182              $this->add_time_columns($columns, $headers);
 183  
 184              $this->add_grade_columns($quiz, $options->usercanseegrades, $columns, $headers, false);
 185  
 186              if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) &&
 187                      $this->has_regraded_questions($table->sql->from, $table->sql->where, $table->sql->params)) {
 188                  $columns[] = 'regraded';
 189                  $headers[] = get_string('regrade', 'quiz_overview');
 190              }
 191  
 192              if ($options->slotmarks) {
 193                  foreach ($questions as $slot => $question) {
 194                      // Ignore questions of zero length.
 195                      $columns[] = 'qsgrade' . $slot;
 196                      $header = get_string('qbrief', 'quiz', $question->number);
 197                      if (!$table->is_downloading()) {
 198                          $header .= '<br />';
 199                      } else {
 200                          $header .= ' ';
 201                      }
 202                      $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
 203                      $headers[] = $header;
 204                  }
 205              }
 206  
 207              $this->set_up_table_columns($table, $columns, $headers, $this->get_base_url(), $options, false);
 208              $table->set_attribute('class', 'generaltable generalbox grades');
 209  
 210              $table->out($options->pagesize, true);
 211          }
 212  
 213          if (!$table->is_downloading() && $options->usercanseegrades) {
 214              $output = $PAGE->get_renderer('mod_quiz');
 215              list($bands, $bandwidth) = self::get_bands_count_and_width($quiz);
 216              $labels = self::get_bands_labels($bands, $bandwidth, $quiz);
 217  
 218              if ($currentgroup && $this->hasgroupstudents) {
 219                  $sql = "SELECT qg.id
 220                            FROM {quiz_grades} qg
 221                            JOIN {user} u on u.id = qg.userid
 222                          {$groupstudentsjoins->joins}
 223                            WHERE qg.quiz = $quiz->id AND {$groupstudentsjoins->wheres}";
 224                  if ($DB->record_exists_sql($sql, $groupstudentsjoins->params)) {
 225                      $data = quiz_report_grade_bands($bandwidth, $bands, $quiz->id, $groupstudentsjoins);
 226                      $chart = self::get_chart($labels, $data);
 227                      $graphname = get_string('overviewreportgraphgroup', 'quiz_overview', groups_get_group_name($currentgroup));
 228                      // Numerical range data should display in LTR even for RTL languages.
 229                      echo $output->chart($chart, $graphname, ['dir' => 'ltr']);
 230                  }
 231              }
 232  
 233              if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) {
 234                  $data = quiz_report_grade_bands($bandwidth, $bands, $quiz->id, new \core\dml\sql_join());
 235                  $chart = self::get_chart($labels, $data);
 236                  $graphname = get_string('overviewreportgraph', 'quiz_overview');
 237                  // Numerical range data should display in LTR even for RTL languages.
 238                  echo $output->chart($chart, $graphname, ['dir' => 'ltr']);
 239              }
 240          }
 241          return true;
 242      }
 243  
 244      /**
 245       * Extends parent function processing any submitted actions.
 246       *
 247       * @param object $quiz
 248       * @param object $cm
 249       * @param int $currentgroup
 250       * @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params)
 251       * @param \core\dml\sql_join $allowedjoins (joins, wheres, params)
 252       * @param moodle_url $redirecturl
 253       */
 254      protected function process_actions($quiz, $cm, $currentgroup, \core\dml\sql_join $groupstudentsjoins,
 255              \core\dml\sql_join $allowedjoins, $redirecturl) {
 256          parent::process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $redirecturl);
 257  
 258          if (empty($currentgroup) || $this->hasgroupstudents) {
 259              if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
 260                  if ($attemptids = optional_param_array('attemptid', array(), PARAM_INT)) {
 261                      $this->start_regrade($quiz, $cm);
 262                      $this->regrade_attempts($quiz, false, $groupstudentsjoins, $attemptids);
 263                      $this->finish_regrade($redirecturl);
 264                  }
 265              }
 266          }
 267  
 268          if (optional_param('regradeall', 0, PARAM_BOOL) && confirm_sesskey()) {
 269              $this->start_regrade($quiz, $cm);
 270              $this->regrade_attempts($quiz, false, $groupstudentsjoins);
 271              $this->finish_regrade($redirecturl);
 272  
 273          } else if (optional_param('regradealldry', 0, PARAM_BOOL) && confirm_sesskey()) {
 274              $this->start_regrade($quiz, $cm);
 275              $this->regrade_attempts($quiz, true, $groupstudentsjoins);
 276              $this->finish_regrade($redirecturl);
 277  
 278          } else if (optional_param('regradealldrydo', 0, PARAM_BOOL) && confirm_sesskey()) {
 279              $this->start_regrade($quiz, $cm);
 280              $this->regrade_attempts_needing_it($quiz, $groupstudentsjoins);
 281              $this->finish_regrade($redirecturl);
 282          }
 283      }
 284  
 285      /**
 286       * Check necessary capabilities, and start the display of the regrade progress page.
 287       * @param object $quiz the quiz settings.
 288       * @param object $cm the cm object for the quiz.
 289       */
 290      protected function start_regrade($quiz, $cm) {
 291          require_capability('mod/quiz:regrade', $this->context);
 292          $this->print_header_and_tabs($cm, $this->course, $quiz, $this->mode);
 293      }
 294  
 295      /**
 296       * Finish displaying the regrade progress page.
 297       * @param moodle_url $nexturl where to send the user after the regrade.
 298       * @uses exit. This method never returns.
 299       */
 300      protected function finish_regrade($nexturl) {
 301          global $OUTPUT;
 302          \core\notification::success(get_string('regradecomplete', 'quiz_overview'));
 303          echo $OUTPUT->continue_button($nexturl);
 304          echo $OUTPUT->footer();
 305          die();
 306      }
 307  
 308      /**
 309       * Unlock the session and allow the regrading process to run in the background.
 310       */
 311      protected function unlock_session() {
 312          \core\session\manager::write_close();
 313          ignore_user_abort(true);
 314      }
 315  
 316      /**
 317       * Regrade a particular quiz attempt. Either for real ($dryrun = false), or
 318       * as a pretend regrade to see which fractions would change. The outcome is
 319       * stored in the quiz_overview_regrades table.
 320       *
 321       * Note, $attempt is not upgraded in the database. The caller needs to do that.
 322       * However, $attempt->sumgrades is updated, if this is not a dry run.
 323       *
 324       * @param object $attempt the quiz attempt to regrade.
 325       * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
 326       * @param array $slots if null, regrade all questions, otherwise, just regrade
 327       *      the quetsions with those slots.
 328       */
 329      protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
 330          global $DB;
 331          // Need more time for a quiz with many questions.
 332          core_php_time_limit::raise(300);
 333  
 334          $transaction = $DB->start_delegated_transaction();
 335  
 336          $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
 337  
 338          if (is_null($slots)) {
 339              $slots = $quba->get_slots();
 340          }
 341  
 342          $finished = $attempt->state == quiz_attempt::FINISHED;
 343          foreach ($slots as $slot) {
 344              $qqr = new stdClass();
 345              $qqr->oldfraction = $quba->get_question_fraction($slot);
 346  
 347              $quba->regrade_question($slot, $finished);
 348  
 349              $qqr->newfraction = $quba->get_question_fraction($slot);
 350  
 351              if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) {
 352                  $qqr->questionusageid = $quba->get_id();
 353                  $qqr->slot = $slot;
 354                  $qqr->regraded = empty($dryrun);
 355                  $qqr->timemodified = time();
 356                  $DB->insert_record('quiz_overview_regrades', $qqr, false);
 357              }
 358          }
 359  
 360          if (!$dryrun) {
 361              question_engine::save_questions_usage_by_activity($quba);
 362  
 363              $params = array(
 364                'objectid' => $attempt->id,
 365                'relateduserid' => $attempt->userid,
 366                'context' => $this->context,
 367                'other' => array(
 368                  'quizid' => $attempt->quiz
 369                )
 370              );
 371              $event = \mod_quiz\event\attempt_regraded::create($params);
 372              $event->trigger();
 373          }
 374  
 375          $transaction->allow_commit();
 376  
 377          // Really, PHP should not need this hint, but without this, we just run out of memory.
 378          $quba = null;
 379          $transaction = null;
 380          gc_collect_cycles();
 381      }
 382  
 383      /**
 384       * Regrade attempts for this quiz, exactly which attempts are regraded is
 385       * controlled by the parameters.
 386       * @param object $quiz the quiz settings.
 387       * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
 388       * @param \core\dml\sql_join|array $groupstudentsjoins empty for all attempts, otherwise regrade attempts
 389       * for these users.
 390       * @param array $attemptids blank for all attempts, otherwise only regrade
 391       * attempts whose id is in this list.
 392       */
 393      protected function regrade_attempts($quiz, $dryrun = false,
 394              \core\dml\sql_join$groupstudentsjoins = null, $attemptids = array()) {
 395          global $DB;
 396          $this->unlock_session();
 397  
 398          $userfieldsapi = \core_user\fields::for_name();
 399          $sql = "SELECT quiza.*, " . $userfieldsapi->get_sql('u', false, '', '', false)->selects . "
 400                    FROM {quiz_attempts} quiza
 401                    JOIN {user} u ON u.id = quiza.userid";
 402          $where = "quiz = :qid AND preview = 0";
 403          $params = array('qid' => $quiz->id);
 404  
 405          if ($this->hasgroupstudents && !empty($groupstudentsjoins->joins)) {
 406              $sql .= "\n{$groupstudentsjoins->joins}";
 407              $where .= " AND {$groupstudentsjoins->wheres}";
 408              $params += $groupstudentsjoins->params;
 409          }
 410  
 411          if ($attemptids) {
 412              list($attemptidcondition, $attemptidparams) = $DB->get_in_or_equal($attemptids, SQL_PARAMS_NAMED);
 413              $where .= " AND quiza.id $attemptidcondition";
 414              $params += $attemptidparams;
 415          }
 416  
 417          $sql .= "\nWHERE {$where}";
 418          $attempts = $DB->get_records_sql($sql, $params);
 419          if (!$attempts) {
 420              return;
 421          }
 422  
 423          $this->regrade_batch_of_attempts($quiz, $attempts, $dryrun, $groupstudentsjoins);
 424      }
 425  
 426      /**
 427       * Regrade those questions in those attempts that are marked as needing regrading
 428       * in the quiz_overview_regrades table.
 429       * @param object $quiz the quiz settings.
 430       * @param \core\dml\sql_join $groupstudentsjoins empty for all attempts, otherwise regrade attempts
 431       * for these users.
 432       */
 433      protected function regrade_attempts_needing_it($quiz, \core\dml\sql_join $groupstudentsjoins) {
 434          global $DB;
 435          $this->unlock_session();
 436  
 437          $join = '{quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid';
 438          $where = "quiza.quiz = :qid AND quiza.preview = 0 AND qqr.regraded = 0";
 439          $params = array('qid' => $quiz->id);
 440  
 441          // Fetch all attempts that need regrading.
 442          if ($this->hasgroupstudents && !empty($groupstudentsjoins->joins)) {
 443              $join .= "\nJOIN {user} u ON u.id = quiza.userid
 444                      {$groupstudentsjoins->joins}";
 445              $where .= " AND {$groupstudentsjoins->wheres}";
 446              $params += $groupstudentsjoins->params;
 447          }
 448  
 449          $toregrade = $DB->get_recordset_sql("
 450                  SELECT quiza.uniqueid, qqr.slot
 451                    FROM {quiz_attempts} quiza
 452                    JOIN $join
 453                   WHERE $where", $params);
 454  
 455          $attemptquestions = array();
 456          foreach ($toregrade as $row) {
 457              $attemptquestions[$row->uniqueid][] = $row->slot;
 458          }
 459          $toregrade->close();
 460  
 461          if (!$attemptquestions) {
 462              return;
 463          }
 464  
 465          list($uniqueidcondition, $params) = $DB->get_in_or_equal(array_keys($attemptquestions));
 466          $userfieldsapi = \core_user\fields::for_name();
 467          $attempts = $DB->get_records_sql("
 468                  SELECT quiza.*, " . $userfieldsapi->get_sql('u', false, '', '', false)->selects . "
 469                    FROM {quiz_attempts} quiza
 470                    JOIN {user} u ON u.id = quiza.userid
 471                   WHERE quiza.uniqueid $uniqueidcondition
 472                  ", $params);
 473  
 474          foreach ($attempts as $attempt) {
 475              $attempt->regradeonlyslots = $attemptquestions[$attempt->uniqueid];
 476          }
 477  
 478          $this->regrade_batch_of_attempts($quiz, $attempts, false, $groupstudentsjoins);
 479      }
 480  
 481      /**
 482       * This is a helper used by {@link regrade_attempts()} and
 483       * {@link regrade_attempts_needing_it()}.
 484       *
 485       * Given an array of attempts, it regrades them all, or does a dry run.
 486       * Each object in the attempts array must be a row from the quiz_attempts
 487       * table, with the \core_user\fields::for_name() fields from the user table joined in.
 488       * In addition, if $attempt->regradeonlyslots is set, then only those slots
 489       * are regraded, otherwise all slots are regraded.
 490       *
 491       * @param object $quiz the quiz settings.
 492       * @param array $attempts of data from the quiz_attempts table, with extra data as above.
 493       * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
 494       * @param \core\dml\sql_join $groupstudentsjoins empty for all attempts, otherwise regrade attempts
 495       */
 496      protected function regrade_batch_of_attempts($quiz, array $attempts,
 497              bool $dryrun, \core\dml\sql_join $groupstudentsjoins) {
 498          $this->clear_regrade_table($quiz, $groupstudentsjoins);
 499  
 500          $progressbar = new progress_bar('quiz_overview_regrade', 500, true);
 501          $a = array(
 502              'count' => count($attempts),
 503              'done'  => 0,
 504          );
 505          foreach ($attempts as $attempt) {
 506              $a['done']++;
 507              $a['attemptnum'] = $attempt->attempt;
 508              $a['name'] = fullname($attempt);
 509              $a['attemptid'] = $attempt->id;
 510              if (!isset($attempt->regradeonlyslots)) {
 511                  $attempt->regradeonlyslots = null;
 512              }
 513              $progressbar->update($a['done'], $a['count'],
 514                      get_string('regradingattemptxofywithdetails', 'quiz_overview', $a));
 515              $this->regrade_attempt($attempt, $dryrun, $attempt->regradeonlyslots);
 516          }
 517          $progressbar->update($a['done'], $a['count'],
 518                  get_string('regradedsuccessfullyxofy', 'quiz_overview', $a));
 519  
 520          if (!$dryrun) {
 521              $this->update_overall_grades($quiz);
 522          }
 523      }
 524  
 525      /**
 526       * Count the number of attempts in need of a regrade.
 527       *
 528       * @param object $quiz the quiz settings.
 529       * @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params) If this is given, only data relating
 530       * to these users is cleared.
 531       * @return int the number of attempts.
 532       */
 533      protected function count_question_attempts_needing_regrade($quiz, \core\dml\sql_join $groupstudentsjoins) {
 534          global $DB;
 535  
 536          $userjoin = '';
 537          $usertest = '';
 538          $params = array();
 539          if ($this->hasgroupstudents) {
 540              $userjoin = "JOIN {user} u ON u.id = quiza.userid
 541                      {$groupstudentsjoins->joins}";
 542              $usertest = "{$groupstudentsjoins->wheres} AND u.id = quiza.userid AND ";
 543              $params = $groupstudentsjoins->params;
 544          }
 545  
 546          $params['cquiz'] = $quiz->id;
 547          $sql = "SELECT COUNT(DISTINCT quiza.id)
 548                    FROM {quiz_attempts} quiza
 549                    JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid
 550                  $userjoin
 551                   WHERE
 552                        $usertest
 553                        quiza.quiz = :cquiz AND
 554                        quiza.preview = 0 AND
 555                        qqr.regraded = 0";
 556          return $DB->count_records_sql($sql, $params);
 557      }
 558  
 559      /**
 560       * Are there any pending regrades in the table we are going to show?
 561       * @param string $from tables used by the main query.
 562       * @param string $where where clause used by the main query.
 563       * @param array $params required by the SQL.
 564       * @return bool whether there are pending regrades.
 565       */
 566      protected function has_regraded_questions($from, $where, $params) {
 567          global $DB;
 568          return $DB->record_exists_sql("
 569                  SELECT 1
 570                    FROM {$from}
 571                    JOIN {quiz_overview_regrades} qor ON qor.questionusageid = quiza.uniqueid
 572                   WHERE {$where}", $params);
 573      }
 574  
 575      /**
 576       * Remove all information about pending/complete regrades from the database.
 577       * @param object $quiz the quiz settings.
 578       * @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params). If this is given, only data relating
 579       * to these users is cleared.
 580       */
 581      protected function clear_regrade_table($quiz, \core\dml\sql_join $groupstudentsjoins) {
 582          global $DB;
 583  
 584          // Fetch all attempts that need regrading.
 585          $select = "questionusageid IN (
 586                      SELECT uniqueid
 587                        FROM {quiz_attempts} quiza";
 588          $where = "WHERE quiza.quiz = :qid";
 589          $params = array('qid' => $quiz->id);
 590          if ($this->hasgroupstudents && !empty($groupstudentsjoins->joins)) {
 591              $select .= "\nJOIN {user} u ON u.id = quiza.userid
 592                      {$groupstudentsjoins->joins}";
 593              $where .= " AND {$groupstudentsjoins->wheres}";
 594              $params += $groupstudentsjoins->params;
 595          }
 596          $select .= "\n$where)";
 597  
 598          $DB->delete_records_select('quiz_overview_regrades', $select, $params);
 599      }
 600  
 601      /**
 602       * Update the final grades for all attempts. This method is used following
 603       * a regrade.
 604       * @param object $quiz the quiz settings.
 605       * @param array $userids only update scores for these userids.
 606       * @param array $attemptids attemptids only update scores for these attempt ids.
 607       */
 608      protected function update_overall_grades($quiz) {
 609          quiz_update_all_attempt_sumgrades($quiz);
 610          quiz_update_all_final_grades($quiz);
 611          quiz_update_grades($quiz);
 612      }
 613  
 614      /**
 615       * Get the bands configuration for the quiz.
 616       *
 617       * This returns the configuration for having between 11 and 20 bars in
 618       * a chart based on the maximum grade to be given on a quiz. The width of
 619       * a band is the number of grade points it encapsulates.
 620       *
 621       * @param object $quiz The quiz object.
 622       * @return array Contains the number of bands, and their width.
 623       */
 624      public static function get_bands_count_and_width($quiz) {
 625          $bands = $quiz->grade;
 626          while ($bands > 20 || $bands <= 10) {
 627              if ($bands > 50) {
 628                  $bands /= 5;
 629              } else if ($bands > 20) {
 630                  $bands /= 2;
 631              }
 632              if ($bands < 4) {
 633                  $bands *= 5;
 634              } else if ($bands <= 10) {
 635                  $bands *= 2;
 636              }
 637          }
 638          // See MDL-34589. Using doubles as array keys causes problems in PHP 5.4, hence the explicit cast to int.
 639          $bands = (int) ceil($bands);
 640          return [$bands, $quiz->grade / $bands];
 641      }
 642  
 643      /**
 644       * Get the bands labels.
 645       *
 646       * @param int $bands The number of bands.
 647       * @param int $bandwidth The band width.
 648       * @param object $quiz The quiz object.
 649       * @return string[] The labels.
 650       */
 651      public static function get_bands_labels($bands, $bandwidth, $quiz) {
 652          $bandlabels = [];
 653          for ($i = 1; $i <= $bands; $i++) {
 654              $bandlabels[] = quiz_format_grade($quiz, ($i - 1) * $bandwidth) . ' - ' . quiz_format_grade($quiz, $i * $bandwidth);
 655          }
 656          return $bandlabels;
 657      }
 658  
 659      /**
 660       * Get a chart.
 661       *
 662       * @param string[] $labels Chart labels.
 663       * @param int[] $data The data.
 664       * @return \core\chart_base
 665       */
 666      protected static function get_chart($labels, $data) {
 667          $chart = new \core\chart_bar();
 668          $chart->set_labels($labels);
 669          $chart->get_xaxis(0, true)->set_label(get_string('gradenoun'));
 670  
 671          $yaxis = $chart->get_yaxis(0, true);
 672          $yaxis->set_label(get_string('participants'));
 673          $yaxis->set_stepsize(max(1, round(max($data) / 10)));
 674  
 675          $series = new \core\chart_series(get_string('participants'), $data);
 676          $chart->add_series($series);
 677          return $chart;
 678      }
 679  }