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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body