Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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