Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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