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