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