Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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   * This file defines the quiz grades table.
  19   *
  20   * @package   quiz_overview
  21   * @copyright 2008 Jamie Pratt
  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_table.php');
  29  
  30  
  31  /**
  32   * This is a table subclass for displaying the quiz grades report.
  33   *
  34   * @copyright 2008 Jamie Pratt
  35   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class quiz_overview_table extends quiz_attempts_report_table {
  38  
  39      protected $regradedqs = array();
  40  
  41      /**
  42       * Constructor
  43       * @param object $quiz
  44       * @param context $context
  45       * @param string $qmsubselect
  46       * @param quiz_overview_options $options
  47       * @param \core\dml\sql_join $groupstudentsjoins
  48       * @param \core\dml\sql_join $studentsjoins
  49       * @param array $questions
  50       * @param moodle_url $reporturl
  51       */
  52      public function __construct($quiz, $context, $qmsubselect,
  53              quiz_overview_options $options, \core\dml\sql_join $groupstudentsjoins,
  54              \core\dml\sql_join $studentsjoins, $questions, $reporturl) {
  55          parent::__construct('mod-quiz-report-overview-report', $quiz , $context,
  56                  $qmsubselect, $options, $groupstudentsjoins, $studentsjoins, $questions, $reporturl);
  57      }
  58  
  59      public function build_table() {
  60          global $DB;
  61  
  62          if (!$this->rawdata) {
  63              return;
  64          }
  65  
  66          $this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetime'));
  67          parent::build_table();
  68  
  69          // End of adding the data from attempts. Now add averages at bottom.
  70          $this->add_separator();
  71  
  72          if (!empty($this->groupstudentsjoins->joins)) {
  73              $sql = "SELECT DISTINCT u.id
  74                        FROM {user} u
  75                      {$this->groupstudentsjoins->joins}
  76                       WHERE {$this->groupstudentsjoins->wheres}";
  77              $groupstudents = $DB->get_records_sql($sql, $this->groupstudentsjoins->params);
  78              if ($groupstudents) {
  79                  $this->add_average_row(get_string('groupavg', 'grades'), $this->groupstudentsjoins);
  80              }
  81          }
  82  
  83          if (!empty($this->studentsjoins->joins)) {
  84              $sql = "SELECT DISTINCT u.id
  85                        FROM {user} u
  86                      {$this->studentsjoins->joins}
  87                       WHERE {$this->studentsjoins->wheres}";
  88              $students = $DB->get_records_sql($sql, $this->studentsjoins->params);
  89              if ($students) {
  90                  $this->add_average_row(get_string('overallaverage', 'grades'), $this->studentsjoins);
  91              }
  92          }
  93      }
  94  
  95      /**
  96       * Calculate the average overall and question scores for a set of attempts at the quiz.
  97       *
  98       * @param string $label the title ot use for this row.
  99       * @param \core\dml\sql_join $usersjoins to indicate a set of users.
 100       * @return array of table cells that make up the average row.
 101       */
 102      public function compute_average_row($label, \core\dml\sql_join $usersjoins) {
 103          global $DB;
 104  
 105          list($fields, $from, $where, $params) = $this->base_sql($usersjoins);
 106          $record = $DB->get_record_sql("
 107                  SELECT AVG(quizaouter.sumgrades) AS grade, COUNT(quizaouter.sumgrades) AS numaveraged
 108                    FROM {quiz_attempts} quizaouter
 109                    JOIN (
 110                         SELECT DISTINCT quiza.id
 111                           FROM $from
 112                          WHERE $where
 113                         ) relevant_attempt_ids ON quizaouter.id = relevant_attempt_ids.id
 114                  ", $params);
 115          $record->grade = quiz_rescale_grade($record->grade, $this->quiz, false);
 116          if ($this->is_downloading()) {
 117              $namekey = 'lastname';
 118          } else {
 119              $namekey = 'fullname';
 120          }
 121          $averagerow = array(
 122              $namekey       => $label,
 123              'sumgrades'    => $this->format_average($record),
 124              'feedbacktext' => strip_tags(quiz_report_feedback_for_grade(
 125                                           $record->grade, $this->quiz->id, $this->context))
 126          );
 127  
 128          if ($this->options->slotmarks) {
 129              $dm = new question_engine_data_mapper();
 130              $qubaids = new qubaid_join("{quiz_attempts} quizaouter
 131                    JOIN (
 132                         SELECT DISTINCT quiza.id
 133                           FROM $from
 134                          WHERE $where
 135                         ) relevant_attempt_ids ON quizaouter.id = relevant_attempt_ids.id",
 136                      'quizaouter.uniqueid', '1 = 1', $params);
 137              $avggradebyq = $dm->load_average_marks($qubaids, array_keys($this->questions));
 138  
 139              $averagerow += $this->format_average_grade_for_questions($avggradebyq);
 140          }
 141  
 142          return $averagerow;
 143      }
 144  
 145      /**
 146       * Add an average grade row for a set of users.
 147       *
 148       * @param string $label the title ot use for this row.
 149       * @param \core\dml\sql_join $usersjoins (joins, wheres, params) for the users to average over.
 150       */
 151      protected function add_average_row($label, \core\dml\sql_join $usersjoins) {
 152          $averagerow = $this->compute_average_row($label, $usersjoins);
 153          $this->add_data_keyed($averagerow);
 154      }
 155  
 156      /**
 157       * Helper userd by {@link add_average_row()}.
 158       * @param array $gradeaverages the raw grades.
 159       * @return array the (partial) row of data.
 160       */
 161      protected function format_average_grade_for_questions($gradeaverages) {
 162          $row = array();
 163  
 164          if (!$gradeaverages) {
 165              $gradeaverages = array();
 166          }
 167  
 168          foreach ($this->questions as $question) {
 169              if (isset($gradeaverages[$question->slot]) && $question->maxmark > 0) {
 170                  $record = $gradeaverages[$question->slot];
 171                  $record->grade = quiz_rescale_grade(
 172                          $record->averagefraction * $question->maxmark, $this->quiz, false);
 173  
 174              } else {
 175                  $record = new stdClass();
 176                  $record->grade = null;
 177                  $record->numaveraged = 0;
 178              }
 179  
 180              $row['qsgrade' . $question->slot] = $this->format_average($record, true);
 181          }
 182  
 183          return $row;
 184      }
 185  
 186      /**
 187       * Format an entry in an average row.
 188       * @param object $record with fields grade and numaveraged.
 189       * @param bool $question true if this is a question score, false if it is an overall score.
 190       * @return string HTML fragment for an average score (with number of things included in the average).
 191       */
 192      protected function format_average($record, $question = false) {
 193          if (is_null($record->grade)) {
 194              $average = '-';
 195          } else if ($question) {
 196              $average = quiz_format_question_grade($this->quiz, $record->grade);
 197          } else {
 198              $average = quiz_format_grade($this->quiz, $record->grade);
 199          }
 200  
 201          if ($this->download) {
 202              return $average;
 203          } else if (is_null($record->numaveraged) || $record->numaveraged == 0) {
 204              return html_writer::tag('span', html_writer::tag('span',
 205                      $average, array('class' => 'average')), array('class' => 'avgcell'));
 206          } else {
 207              return html_writer::tag('span', html_writer::tag('span',
 208                      $average, array('class' => 'average')) . ' ' . html_writer::tag('span',
 209                      '(' . $record->numaveraged . ')', array('class' => 'count')),
 210                      array('class' => 'avgcell'));
 211          }
 212      }
 213  
 214      protected function submit_buttons() {
 215          if (has_capability('mod/quiz:regrade', $this->context)) {
 216              $regradebuttonparams = [
 217                  'type'  => 'submit',
 218                  'class' => 'btn btn-secondary mr-1',
 219                  'name'  => 'regrade',
 220                  'value' => get_string('regradeselected', 'quiz_overview'),
 221                  'data-action' => 'toggle',
 222                  'data-togglegroup' => $this->togglegroup,
 223                  'data-toggle' => 'action',
 224                  'disabled' => true
 225              ];
 226              echo html_writer::empty_tag('input', $regradebuttonparams);
 227          }
 228          parent::submit_buttons();
 229      }
 230  
 231      public function col_sumgrades($attempt) {
 232          if ($attempt->state != quiz_attempt::FINISHED) {
 233              return '-';
 234          }
 235  
 236          $grade = quiz_rescale_grade($attempt->sumgrades, $this->quiz);
 237          if ($this->is_downloading()) {
 238              return $grade;
 239          }
 240  
 241          if (isset($this->regradedqs[$attempt->usageid])) {
 242              $newsumgrade = 0;
 243              $oldsumgrade = 0;
 244              foreach ($this->questions as $question) {
 245                  if (isset($this->regradedqs[$attempt->usageid][$question->slot])) {
 246                      $newsumgrade += $this->regradedqs[$attempt->usageid]
 247                              [$question->slot]->newfraction * $question->maxmark;
 248                      $oldsumgrade += $this->regradedqs[$attempt->usageid]
 249                              [$question->slot]->oldfraction * $question->maxmark;
 250                  } else {
 251                      $newsumgrade += $this->lateststeps[$attempt->usageid]
 252                              [$question->slot]->fraction * $question->maxmark;
 253                      $oldsumgrade += $this->lateststeps[$attempt->usageid]
 254                              [$question->slot]->fraction * $question->maxmark;
 255                  }
 256              }
 257              $newsumgrade = quiz_rescale_grade($newsumgrade, $this->quiz);
 258              $oldsumgrade = quiz_rescale_grade($oldsumgrade, $this->quiz);
 259              $grade = html_writer::tag('del', $oldsumgrade) . '/' .
 260                      html_writer::empty_tag('br') . $newsumgrade;
 261          }
 262          return html_writer::link(new moodle_url('/mod/quiz/review.php',
 263                  array('attempt' => $attempt->attempt)), $grade,
 264                  array('title' => get_string('reviewattempt', 'quiz')));
 265      }
 266  
 267      /**
 268       * @param string $colname the name of the column.
 269       * @param object $attempt the row of data - see the SQL in display() in
 270       * mod/quiz/report/overview/report.php to see what fields are present,
 271       * and what they are called.
 272       * @return string the contents of the cell.
 273       */
 274      public function other_cols($colname, $attempt) {
 275          if (!preg_match('/^qsgrade(\d+)$/', $colname, $matches)) {
 276              return parent::other_cols($colname, $attempt);
 277          }
 278          $slot = $matches[1];
 279  
 280          $question = $this->questions[$slot];
 281          if (!isset($this->lateststeps[$attempt->usageid][$slot])) {
 282              return '-';
 283          }
 284  
 285          $stepdata = $this->lateststeps[$attempt->usageid][$slot];
 286          $state = question_state::get($stepdata->state);
 287  
 288          if ($question->maxmark == 0) {
 289              $grade = '-';
 290          } else if (is_null($stepdata->fraction)) {
 291              if ($state == question_state::$needsgrading) {
 292                  $grade = get_string('requiresgrading', 'question');
 293              } else {
 294                  $grade = '-';
 295              }
 296          } else {
 297              $grade = quiz_rescale_grade(
 298                      $stepdata->fraction * $question->maxmark, $this->quiz, 'question');
 299          }
 300  
 301          if ($this->is_downloading()) {
 302              return $grade;
 303          }
 304  
 305          if (isset($this->regradedqs[$attempt->usageid][$slot])) {
 306              $gradefromdb = $grade;
 307              $newgrade = quiz_rescale_grade(
 308                      $this->regradedqs[$attempt->usageid][$slot]->newfraction * $question->maxmark,
 309                      $this->quiz, 'question');
 310              $oldgrade = quiz_rescale_grade(
 311                      $this->regradedqs[$attempt->usageid][$slot]->oldfraction * $question->maxmark,
 312                      $this->quiz, 'question');
 313  
 314              $grade = html_writer::tag('del', $oldgrade) . '/' .
 315                      html_writer::empty_tag('br') . $newgrade;
 316          }
 317  
 318          return $this->make_review_link($grade, $attempt, $slot);
 319      }
 320  
 321      public function col_regraded($attempt) {
 322          if ($attempt->regraded == '') {
 323              return '';
 324          } else if ($attempt->regraded == 0) {
 325              return get_string('needed', 'quiz_overview');
 326          } else if ($attempt->regraded == 1) {
 327              return get_string('done', 'quiz_overview');
 328          }
 329      }
 330  
 331      protected function update_sql_after_count($fields, $from, $where, $params) {
 332          $fields .= ", COALESCE((
 333                                  SELECT MAX(qqr.regraded)
 334                                    FROM {quiz_overview_regrades} qqr
 335                                   WHERE qqr.questionusageid = quiza.uniqueid
 336                            ), -1) AS regraded";
 337          if ($this->options->onlyregraded) {
 338              $where .= " AND COALESCE((
 339                                      SELECT MAX(qqr.regraded)
 340                                        FROM {quiz_overview_regrades} qqr
 341                                       WHERE qqr.questionusageid = quiza.uniqueid
 342                                  ), -1) <> -1";
 343          }
 344          return [$fields, $from, $where, $params];
 345      }
 346  
 347      protected function requires_latest_steps_loaded() {
 348          return $this->options->slotmarks;
 349      }
 350  
 351      protected function is_latest_step_column($column) {
 352          if (preg_match('/^qsgrade([0-9]+)/', $column, $matches)) {
 353              return $matches[1];
 354          }
 355          return false;
 356      }
 357  
 358      protected function get_required_latest_state_fields($slot, $alias) {
 359          return "$alias.fraction * $alias.maxmark AS qsgrade$slot";
 360      }
 361  
 362      public function query_db($pagesize, $useinitialsbar = true) {
 363          parent::query_db($pagesize, $useinitialsbar);
 364  
 365          if ($this->options->slotmarks && has_capability('mod/quiz:regrade', $this->context)) {
 366              $this->regradedqs = $this->get_regraded_questions();
 367          }
 368      }
 369  
 370      /**
 371       * Get all the questions in all the attempts being displayed that need regrading.
 372       * @return array A two dimensional array $questionusageid => $slot => $regradeinfo.
 373       */
 374      protected function get_regraded_questions() {
 375          global $DB;
 376  
 377          $qubaids = $this->get_qubaids_condition();
 378          $regradedqs = $DB->get_records_select('quiz_overview_regrades',
 379                  'questionusageid ' . $qubaids->usage_id_in(), $qubaids->usage_id_in_params());
 380          return quiz_report_index_by_keys($regradedqs, array('questionusageid', 'slot'));
 381      }
 382  }