MDL-59931 mod_quiz: Fixing incorrect pagination count.
[moodle.git] / mod / quiz / report / overview / report.php
CommitLineData
2c3968c4 1<?php
e24ee794
TH
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
2c3968c4 17/**
e24ee794 18 * This file defines the quiz overview report class.
2c3968c4 19 *
8d76124c
TH
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
98f38217 23 */
2c3968c4 24
e24ee794 25
a17b297d
TH
26defined('MOODLE_INTERNAL') || die();
27
6b4e2d76 28require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport.php');
dcd65f1b
TH
29require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_options.php');
30require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_form.php');
6b4e2d76 31require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_table.php');
7bbe08a2 32
7bbe08a2 33
e24ee794
TH
34/**
35 * Quiz report subclass for the overview (grades) report.
36 *
8d76124c
TH
37 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
e24ee794 39 */
ac4d9157 40class quiz_overview_report extends quiz_attempts_report {
e24ee794 41
c7df5006 42 public function display($quiz, $cm, $course) {
10c4fce5
JB
43 global $DB, $OUTPUT, $PAGE;
44
45 list($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins) = $this->init(
46 'overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
7bbe08a2 47
e97d60ad 48 $options = new quiz_overview_options('overview', $quiz, $cm, $course);
e24ee794 49
303aa3b8 50 if ($fromform = $this->form->get_data()) {
dcd65f1b 51 $options->process_settings_from_form($fromform);
e24ee794 52
0217f932 53 } else {
dcd65f1b 54 $options->process_settings_from_params();
0217f932 55 }
e24ee794 56
dcd65f1b 57 $this->form->set_data($options->get_initial_form_data());
e24ee794 58
e24ee794 59 // Load the required questions.
88b3eb8b 60 $questions = quiz_report_get_significant_questions($quiz);
e24ee794 61
26aded55
TH
62 // Prepare for downloading, if applicable.
63 $courseshortname = format_string($course->shortname, true,
64 array('context' => context_course::instance($course->id)));
303aa3b8 65 $table = new quiz_overview_table($quiz, $this->context, $this->qmsubselect,
10c4fce5 66 $options, $groupstudentsjoins, $studentsjoins, $questions, $options->get_url());
e24ee794 67 $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
8ebbb06a 68 $courseshortname, $quiz->name);
dcd65f1b 69 $table->is_downloading($options->download, $filename,
26aded55 70 $courseshortname . ' ' . format_string($quiz->name, true));
e24ee794
TH
71 if ($table->is_downloading()) {
72 raise_memory_limit(MEMORY_EXTRA);
73 }
74
10c4fce5
JB
75 $this->hasgroupstudents = false;
76 if (!empty($groupstudentsjoins->joins)) {
77 $sql = "SELECT DISTINCT u.id
78 FROM {user} u
79 $groupstudentsjoins->joins
80 WHERE $groupstudentsjoins->wheres";
81 $this->hasgroupstudents = $DB->record_exists_sql($sql, $groupstudentsjoins->params);
82 }
83 $hasstudents = false;
84 if (!empty($studentsjoins->joins)) {
85 $sql = "SELECT DISTINCT u.id
86 FROM {user} u
87 $studentsjoins->joins
88 WHERE $studentsjoins->wheres";
89 $hasstudents = $DB->record_exists_sql($sql, $studentsjoins->params);
90 }
91 if ($options->attempts == self::ALL_WITH) {
92 // This option is only available to users who can access all groups in
93 // groups mode, so setting allowed to empty (which means all quiz attempts
94 // are accessible, is not a security porblem.
95 $allowedjoins = new \core\dml\sql_join();
96 }
97
b0ac8539 98 $this->course = $course; // Hack to make this available in process_actions.
10c4fce5 99 $this->process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $options->get_url());
aeb15530 100
e24ee794
TH
101 // Start output.
102 if (!$table->is_downloading()) {
768a7588 103 // Only print headers if not asked to download data.
361cf27d 104 $this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
e24ee794
TH
105 }
106
768a7588 107 if ($groupmode = groups_get_activity_groupmode($cm)) {
4747c788 108 // Groups are being used, so output the group selector if we are not downloading.
c35f3afc 109 if (!$table->is_downloading()) {
e97d60ad 110 groups_print_activity_menu($cm, $options->get_url());
970d0fe0 111 }
2d7617c6 112 }
98f38217 113
768a7588 114 // Print information on the number of existing attempts.
4747c788
TH
115 if (!$table->is_downloading()) {
116 // Do not print notices when downloading.
e24ee794
TH
117 if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
118 echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
119 }
120 }
121
ccba5b88 122 $hasquestions = quiz_has_questions($quiz->id);
3c6185e9
TH
123 if (!$table->is_downloading()) {
124 if (!$hasquestions) {
125 echo quiz_no_questions_message($quiz, $cm, $this->context);
10c4fce5 126 } else if (!$hasstudents) {
414e7276 127 echo $OUTPUT->notification(get_string('nostudentsyet'));
10c4fce5 128 } else if ($currentgroup && !$this->hasgroupstudents) {
414e7276
TH
129 echo $OUTPUT->notification(get_string('nostudentsingroup'));
130 }
e24ee794 131
4747c788 132 // Print the display options.
303aa3b8 133 $this->form->display();
f6c7f158 134 }
39790bd8 135
10c4fce5 136 $hasstudents = $hasstudents && (!$currentgroup || $this->hasgroupstudents);
863872e3 137 if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
768a7588 138 // Construct the SQL.
10c4fce5 139 list($fields, $from, $where, $params) = $table->base_sql($allowedjoins);
aeb15530 140
43f122d3 141 $table->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp", $params);
98f38217 142
e24ee794 143 // Test to see if there are any regraded attempts to be listed.
25a03faa
TH
144 $fields .= ", COALESCE((
145 SELECT MAX(qqr.regraded)
146 FROM {quiz_overview_regrades} qqr
147 WHERE qqr.questionusageid = quiza.uniqueid
148 ), -1) AS regraded";
dcd65f1b 149 if ($options->onlyregraded) {
25a03faa
TH
150 $where .= " AND COALESCE((
151 SELECT MAX(qqr.regraded)
152 FROM {quiz_overview_regrades} qqr
153 WHERE qqr.questionusageid = quiza.uniqueid
154 ), -1) <> -1";
98f38217 155 }
abe67b24 156 $table->set_sql($fields, $from, $where, $params);
aeb15530 157
e24ee794 158 if (!$table->is_downloading()) {
768a7588 159 // Output the regrade buttons.
07a2b2f0 160 if (has_capability('mod/quiz:regrade', $this->context)) {
e24ee794 161 $regradesneeded = $this->count_question_attempts_needing_regrade(
10c4fce5 162 $quiz, $groupstudentsjoins);
07a2b2f0 163 if ($currentgroup) {
39790bd8 164 $a= new stdClass();
f29e6691 165 $a->groupname = groups_get_group_name($currentgroup);
9101efd3 166 $a->coursestudents = get_string('participants');
e24ee794 167 $a->countregradeneeded = $regradesneeded;
25a03faa
TH
168 $regradealldrydolabel =
169 get_string('regradealldrydogroup', 'quiz_overview', $a);
170 $regradealldrylabel =
171 get_string('regradealldrygroup', 'quiz_overview', $a);
172 $regradealllabel =
173 get_string('regradeallgroup', 'quiz_overview', $a);
f29e6691 174 } else {
25a03faa
TH
175 $regradealldrydolabel =
176 get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
177 $regradealldrylabel =
178 get_string('regradealldry', 'quiz_overview');
179 $regradealllabel =
180 get_string('regradeall', 'quiz_overview');
f29e6691 181 }
e97d60ad 182 $displayurl = new moodle_url($options->get_url(), array('sesskey' => sesskey()));
f29e6691 183 echo '<div class="mdl-align">';
eb788065 184 echo '<form action="'.$displayurl->out_omit_querystring().'">';
f29e6691 185 echo '<div>';
6ea66ff3 186 echo html_writer::input_hidden_params($displayurl);
12d6dfc1
DW
187 echo '<input type="submit" class="btn btn-secondary" name="regradeall" value="'.$regradealllabel.'"/>';
188 echo '<input type="submit" class="btn btn-secondary m-l-1" name="regradealldry" value="' .
25a03faa 189 $regradealldrylabel . '"/>';
e24ee794 190 if ($regradesneeded) {
12d6dfc1 191 echo '<input type="submit" class="btn btn-secondary m-l-1" name="regradealldrydo" value="' .
25a03faa 192 $regradealldrydolabel . '"/>';
f29e6691 193 }
194 echo '</div>';
195 echo '</form>';
196 echo '</div>';
197 }
768a7588 198 // Print information on the grading method.
25a03faa 199 if ($strattempthighlight = quiz_report_highlighting_grading_method(
dcd65f1b 200 $quiz, $this->qmsubselect, $options->onlygraded)) {
f29e6691 201 echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
202 }
203 }
aeb15530 204
768a7588 205 // Define table columns.
e24ee794
TH
206 $columns = array();
207 $headers = array();
aeb15530 208
9e67e357 209 if (!$table->is_downloading() && $options->checkboxcolumn) {
e24ee794 210 $columns[] = 'checkbox';
25a03faa 211 $headers[] = null;
abe67b24 212 }
aeb15530 213
e24ee794 214 $this->add_user_columns($table, $columns, $headers);
c547514a 215 $this->add_state_column($columns, $headers);
e24ee794 216 $this->add_time_columns($columns, $headers);
aeb15530 217
26d9ee4e
TH
218 $this->add_grade_columns($quiz, $options->usercanseegrades, $columns, $headers, false);
219
220 if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) &&
221 $this->has_regraded_questions($from, $where, $params)) {
222 $columns[] = 'regraded';
223 $headers[] = get_string('regrade', 'quiz_overview');
224 }
225
dcd65f1b 226 if ($options->slotmarks) {
e24ee794 227 foreach ($questions as $slot => $question) {
768a7588 228 // Ignore questions of zero length.
e24ee794
TH
229 $columns[] = 'qsgrade' . $slot;
230 $header = get_string('qbrief', 'quiz', $question->number);
98f38217 231 if (!$table->is_downloading()) {
e24ee794 232 $header .= '<br />';
98f38217 233 } else {
e24ee794 234 $header .= ' ';
98f38217 235 }
e24ee794 236 $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
98f38217 237 $headers[] = $header;
eacb462e 238 }
abe67b24 239 }
e24ee794 240
e97d60ad 241 $this->set_up_table_columns($table, $columns, $headers, $this->get_base_url(), $options, false);
e24ee794 242 $table->set_attribute('class', 'generaltable generalbox grades');
aeb15530 243
dcd65f1b 244 $table->out($options->pagesize, true);
7bbe08a2 245 }
e24ee794 246
9e67e357 247 if (!$table->is_downloading() && $options->usercanseegrades) {
2cdcb905 248 $output = $PAGE->get_renderer('mod_quiz');
ec882623
FM
249 list($bands, $bandwidth) = self::get_bands_count_and_width($quiz);
250 $labels = self::get_bands_labels($bands, $bandwidth, $quiz);
251
10c4fce5
JB
252 if ($currentgroup && $this->hasgroupstudents) {
253 $sql = "SELECT qg.id
254 FROM {quiz_grades} qg
255 JOIN {user} u on u.id = qg.userid
256 {$groupstudentsjoins->joins}
257 WHERE qg.quiz = $quiz->id AND {$groupstudentsjoins->wheres}";
258 if ($DB->record_exists_sql($sql, $groupstudentsjoins->params)) {
259 $data = quiz_report_grade_bands($bandwidth, $bands, $quiz->id, $groupstudentsjoins);
ec882623
FM
260 $chart = self::get_chart($labels, $data);
261 $graphname = get_string('overviewreportgraphgroup', 'quiz_overview', groups_get_group_name($currentgroup));
262 echo $output->chart($chart, $graphname);
f29e6691 263 }
264 }
e24ee794 265
07a2b2f0 266 if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) {
10c4fce5 267 $data = quiz_report_grade_bands($bandwidth, $bands, $quiz->id, new \core\dml\sql_join());
ec882623 268 $chart = self::get_chart($labels, $data);
2cdcb905 269 $graphname = get_string('overviewreportgraph', 'quiz_overview');
ec882623 270 echo $output->chart($chart, $graphname);
aad5b0fc 271 }
78517b5a 272 }
7bbe08a2 273 return true;
274 }
e24ee794 275
10c4fce5
JB
276 /**
277 * Extends parent function processing any submitted actions.
278 *
279 * @param object $quiz
280 * @param object $cm
281 * @param int $currentgroup
282 * @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params)
283 * @param \core\dml\sql_join $allowedjoins (joins, wheres, params)
284 * @param moodle_url $redirecturl
285 */
286 protected function process_actions($quiz, $cm, $currentgroup, \core\dml\sql_join $groupstudentsjoins,
287 \core\dml\sql_join $allowedjoins, $redirecturl) {
288 parent::process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $redirecturl);
361cf27d 289
10c4fce5 290 if (empty($currentgroup) || $this->hasgroupstudents) {
361cf27d
TH
291 if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
292 if ($attemptids = optional_param_array('attemptid', array(), PARAM_INT)) {
b0ac8539 293 $this->start_regrade($quiz, $cm);
10c4fce5 294 $this->regrade_attempts($quiz, false, $groupstudentsjoins, $attemptids);
b0ac8539 295 $this->finish_regrade($redirecturl);
361cf27d
TH
296 }
297 }
298 }
299
300 if (optional_param('regradeall', 0, PARAM_BOOL) && confirm_sesskey()) {
b0ac8539 301 $this->start_regrade($quiz, $cm);
10c4fce5 302 $this->regrade_attempts($quiz, false, $groupstudentsjoins);
b0ac8539 303 $this->finish_regrade($redirecturl);
361cf27d
TH
304
305 } else if (optional_param('regradealldry', 0, PARAM_BOOL) && confirm_sesskey()) {
b0ac8539 306 $this->start_regrade($quiz, $cm);
10c4fce5 307 $this->regrade_attempts($quiz, true, $groupstudentsjoins);
b0ac8539 308 $this->finish_regrade($redirecturl);
361cf27d
TH
309
310 } else if (optional_param('regradealldrydo', 0, PARAM_BOOL) && confirm_sesskey()) {
b0ac8539 311 $this->start_regrade($quiz, $cm);
10c4fce5 312 $this->regrade_attempts_needing_it($quiz, $groupstudentsjoins);
b0ac8539 313 $this->finish_regrade($redirecturl);
361cf27d
TH
314 }
315 }
316
b0ac8539
TH
317 /**
318 * Check necessary capabilities, and start the display of the regrade progress page.
319 * @param object $quiz the quiz settings.
320 * @param object $cm the cm object for the quiz.
321 */
322 protected function start_regrade($quiz, $cm) {
b0ac8539
TH
323 require_capability('mod/quiz:regrade', $this->context);
324 $this->print_header_and_tabs($cm, $this->course, $quiz, $this->mode);
325 }
326
327 /**
328 * Finish displaying the regrade progress page.
329 * @param moodle_url $nexturl where to send the user after the regrade.
330 * @uses exit. This method never returns.
331 */
332 protected function finish_regrade($nexturl) {
cd6dfd68
JD
333 global $OUTPUT;
334 \core\notification::success(get_string('regradecomplete', 'quiz_overview'));
335 echo $OUTPUT->continue_button($nexturl);
336 echo $OUTPUT->footer();
337 die();
b0ac8539
TH
338 }
339
62af3fe4
TL
340 /**
341 * Unlock the session and allow the regrading process to run in the background.
342 */
343 protected function unlock_session() {
d79d5ac2 344 \core\session\manager::write_close();
62af3fe4
TL
345 ignore_user_abort(true);
346 }
347
98f38217 348 /**
e24ee794
TH
349 * Regrade a particular quiz attempt. Either for real ($dryrun = false), or
350 * as a pretend regrade to see which fractions would change. The outcome is
351 * stored in the quiz_overview_regrades table.
352 *
353 * Note, $attempt is not upgraded in the database. The caller needs to do that.
354 * However, $attempt->sumgrades is updated, if this is not a dry run.
355 *
356 * @param object $attempt the quiz attempt to regrade.
f7970e3c 357 * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
e24ee794
TH
358 * @param array $slots if null, regrade all questions, otherwise, just regrade
359 * the quetsions with those slots.
98f38217 360 */
e24ee794
TH
361 protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
362 global $DB;
62af3fe4 363 // Need more time for a quiz with many questions.
3ef7279f 364 core_php_time_limit::raise(300);
98f38217 365
e24ee794 366 $transaction = $DB->start_delegated_transaction();
98f38217 367
e24ee794
TH
368 $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
369
370 if (is_null($slots)) {
371 $slots = $quba->get_slots();
98f38217 372 }
e24ee794 373
be18f589 374 $finished = $attempt->state == quiz_attempt::FINISHED;
e24ee794 375 foreach ($slots as $slot) {
0ff4bd08 376 $qqr = new stdClass();
e24ee794
TH
377 $qqr->oldfraction = $quba->get_question_fraction($slot);
378
379 $quba->regrade_question($slot, $finished);
380
381 $qqr->newfraction = $quba->get_question_fraction($slot);
382
383 if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) {
384 $qqr->questionusageid = $quba->get_id();
385 $qqr->slot = $slot;
386 $qqr->regraded = empty($dryrun);
387 $qqr->timemodified = time();
388 $DB->insert_record('quiz_overview_regrades', $qqr, false);
98f38217 389 }
98f38217 390 }
391
e24ee794
TH
392 if (!$dryrun) {
393 question_engine::save_questions_usage_by_activity($quba);
98f38217 394 }
e24ee794
TH
395
396 $transaction->allow_commit();
01118710
K
397
398 // Really, PHP should not need this hint, but without this, we just run out of memory.
399 $quba = null;
400 $transaction = null;
401 gc_collect_cycles();
98f38217 402 }
e24ee794
TH
403
404 /**
405 * Regrade attempts for this quiz, exactly which attempts are regraded is
406 * controlled by the parameters.
407 * @param object $quiz the quiz settings.
f7970e3c 408 * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
10c4fce5 409 * @param \core\dml\sql_join|array $groupstudentsjoins empty for all attempts, otherwise regrade attempts
e24ee794
TH
410 * for these users.
411 * @param array $attemptids blank for all attempts, otherwise only regrade
412 * attempts whose id is in this list.
413 */
414 protected function regrade_attempts($quiz, $dryrun = false,
10c4fce5 415 \core\dml\sql_join$groupstudentsjoins = null, $attemptids = array()) {
98f38217 416 global $DB;
62af3fe4 417 $this->unlock_session();
e24ee794 418
10c4fce5
JB
419 $sql = "SELECT quiza.*
420 FROM {quiz_attempts} quiza";
421 $where = "quiz = :qid AND preview = 0";
422 $params = array('qid' => $quiz->id);
e24ee794 423
10c4fce5
JB
424 if ($this->hasgroupstudents && !empty($groupstudentsjoins->joins)) {
425 $sql .= "\nJOIN {user} u ON u.id = quiza.userid
426 {$groupstudentsjoins->joins}";
427 $where .= " AND {$groupstudentsjoins->wheres}";
428 $params += $groupstudentsjoins->params;
98f38217 429 }
e24ee794
TH
430
431 if ($attemptids) {
10c4fce5
JB
432 $aids = join(',', $attemptids);
433 $where .= " AND quiza.id IN ({$aids})";
98f38217 434 }
98f38217 435
10c4fce5
JB
436 $sql .= "\nWHERE {$where}";
437 $attempts = $DB->get_records_sql($sql, $params);
e24ee794
TH
438 if (!$attempts) {
439 return;
440 }
98f38217 441
10c4fce5 442 $this->clear_regrade_table($quiz, $groupstudentsjoins);
98f38217 443
b0ac8539
TH
444 $progressbar = new progress_bar('quiz_overview_regrade', 500, true);
445 $a = array(
446 'count' => count($attempts),
447 'done' => 0,
448 );
98f38217 449 foreach ($attempts as $attempt) {
e24ee794 450 $this->regrade_attempt($attempt, $dryrun);
b0ac8539
TH
451 $a['done']++;
452 $progressbar->update($a['done'], $a['count'],
453 get_string('regradingattemptxofy', 'quiz_overview', $a));
e24ee794
TH
454 }
455
456 if (!$dryrun) {
457 $this->update_overall_grades($quiz);
98f38217 458 }
98f38217 459 }
460
e24ee794
TH
461 /**
462 * Regrade those questions in those attempts that are marked as needing regrading
463 * in the quiz_overview_regrades table.
464 * @param object $quiz the quiz settings.
10c4fce5 465 * @param \core\dml\sql_join $groupstudentsjoins empty for all attempts, otherwise regrade attempts
e24ee794
TH
466 * for these users.
467 */
10c4fce5 468 protected function regrade_attempts_needing_it($quiz, \core\dml\sql_join $groupstudentsjoins) {
98f38217 469 global $DB;
62af3fe4 470 $this->unlock_session();
e24ee794 471
10c4fce5
JB
472 $join = '{quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid';
473 $where = "quiza.quiz = :qid AND quiza.preview = 0 AND qqr.regraded = 0";
474 $params = array('qid' => $quiz->id);
e24ee794 475
768a7588 476 // Fetch all attempts that need regrading.
10c4fce5
JB
477 if ($this->hasgroupstudents && !empty($groupstudentsjoins->joins)) {
478 $join .= "\nJOIN {user} u ON u.id = quiza.userid
479 {$groupstudentsjoins->joins}";
480 $where .= " AND {$groupstudentsjoins->wheres}";
481 $params += $groupstudentsjoins->params;
98f38217 482 }
23277af8 483
f027922e 484 $toregrade = $DB->get_recordset_sql("
e24ee794 485 SELECT quiza.uniqueid, qqr.slot
10c4fce5
JB
486 FROM {quiz_attempts} quiza
487 JOIN $join
488 WHERE $where", $params);
e24ee794 489
e24ee794
TH
490 $attemptquestions = array();
491 foreach ($toregrade as $row) {
492 $attemptquestions[$row->uniqueid][] = $row->slot;
98f38217 493 }
f027922e
TH
494 $toregrade->close();
495
496 if (!$attemptquestions) {
497 return;
498 }
499
25a03faa
TH
500 $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
501 array_keys($attemptquestions));
e24ee794 502
10c4fce5 503 $this->clear_regrade_table($quiz, $groupstudentsjoins);
e24ee794 504
b0ac8539
TH
505 $progressbar = new progress_bar('quiz_overview_regrade', 500, true);
506 $a = array(
507 'count' => count($attempts),
508 'done' => 0,
509 );
e24ee794 510 foreach ($attempts as $attempt) {
e24ee794 511 $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]);
b0ac8539
TH
512 $a['done']++;
513 $progressbar->update($a['done'], $a['count'],
514 get_string('regradingattemptxofy', 'quiz_overview', $a));
98f38217 515 }
e24ee794
TH
516
517 $this->update_overall_grades($quiz);
98f38217 518 }
f05fedc8 519
e24ee794
TH
520 /**
521 * Count the number of attempts in need of a regrade.
522 * @param object $quiz the quiz settings.
10c4fce5 523 * @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params) If this is given, only data relating
e24ee794
TH
524 * to these users is cleared.
525 */
10c4fce5 526 protected function count_question_attempts_needing_regrade($quiz, \core\dml\sql_join $groupstudentsjoins) {
e24ee794
TH
527 global $DB;
528
10c4fce5 529 $userjoin = '';
e24ee794
TH
530 $usertest = '';
531 $params = array();
10c4fce5
JB
532 if ($this->hasgroupstudents) {
533 $userjoin = "JOIN {user} u ON u.id = quiza.userid
534 {$groupstudentsjoins->joins}";
535 $usertest = "{$groupstudentsjoins->wheres} AND u.id = quiza.userid AND ";
536 $params = $groupstudentsjoins->params;
98f38217 537 }
e24ee794 538
10c4fce5 539 $params['cquiz'] = $quiz->id;
e24ee794 540 $sql = "SELECT COUNT(DISTINCT quiza.id)
10c4fce5
JB
541 FROM {quiz_attempts} quiza
542 JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid
543 $userjoin
544 WHERE
545 $usertest
546 quiza.quiz = :cquiz AND
547 quiza.preview = 0 AND
548 qqr.regraded = 0";
e24ee794
TH
549 return $DB->count_records_sql($sql, $params);
550 }
551
552 /**
553 * Are there any pending regrades in the table we are going to show?
554 * @param string $from tables used by the main query.
555 * @param string $where where clause used by the main query.
556 * @param array $params required by the SQL.
557 * @return bool whether there are pending regrades.
558 */
559 protected function has_regraded_questions($from, $where, $params) {
560 global $DB;
2ce9a94d
TH
561 return $DB->record_exists_sql("
562 SELECT 1
563 FROM {$from}
564 JOIN {quiz_overview_regrades} qor ON qor.questionusageid = quiza.uniqueid
565 WHERE {$where}", $params);
98f38217 566 }
f05fedc8 567
e24ee794
TH
568 /**
569 * Remove all information about pending/complete regrades from the database.
570 * @param object $quiz the quiz settings.
10c4fce5 571 * @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params). If this is given, only data relating
e24ee794
TH
572 * to these users is cleared.
573 */
10c4fce5 574 protected function clear_regrade_table($quiz, \core\dml\sql_join $groupstudentsjoins) {
98f38217 575 global $DB;
e24ee794 576
768a7588 577 // Fetch all attempts that need regrading.
10c4fce5
JB
578 $select = "questionusageid IN (
579 SELECT uniqueid
580 FROM {quiz_attempts} quiza";
581 $where = "WHERE quiza.quiz = :qid";
582 $params = array('qid' => $quiz->id);
583 if ($this->hasgroupstudents && !empty($groupstudentsjoins->joins)) {
584 $select .= "\nJOIN {user} u ON u.id = quiza.userid
585 {$groupstudentsjoins->joins}";
586 $where .= " AND {$groupstudentsjoins->wheres}";
587 $params += $groupstudentsjoins->params;
98f38217 588 }
10c4fce5 589 $select .= "\n$where)";
aeb15530 590
10c4fce5 591 $DB->delete_records_select('quiz_overview_regrades', $select, $params);
e24ee794 592 }
98f38217 593
e24ee794
TH
594 /**
595 * Update the final grades for all attempts. This method is used following
596 * a regrade.
597 * @param object $quiz the quiz settings.
598 * @param array $userids only update scores for these userids.
599 * @param array $attemptids attemptids only update scores for these attempt ids.
600 */
601 protected function update_overall_grades($quiz) {
602 quiz_update_all_attempt_sumgrades($quiz);
603 quiz_update_all_final_grades($quiz);
604 quiz_update_grades($quiz);
98f38217 605 }
ec882623
FM
606
607 /**
608 * Get the bands configuration for the quiz.
609 *
610 * This returns the configuration for having between 11 and 20 bars in
611 * a chart based on the maximum grade to be given on a quiz. The width of
612 * a band is the number of grade points it encapsulates.
613 *
614 * @param object $quiz The quiz object.
615 * @return array Contains the number of bands, and their width.
616 */
617 public static function get_bands_count_and_width($quiz) {
618 $bands = $quiz->grade;
619 while ($bands > 20 || $bands <= 10) {
620 if ($bands > 50) {
621 $bands /= 5;
622 } else if ($bands > 20) {
623 $bands /= 2;
624 }
625 if ($bands < 4) {
626 $bands *= 5;
627 } else if ($bands <= 10) {
628 $bands *= 2;
629 }
630 }
631 // See MDL-34589. Using doubles as array keys causes problems in PHP 5.4, hence the explicit cast to int.
632 $bands = (int) ceil($bands);
633 return [$bands, $quiz->grade / $bands];
634 }
635
636 /**
637 * Get the bands labels.
638 *
639 * @param int $bands The number of bands.
640 * @param int $bandwidth The band width.
641 * @param object $quiz The quiz object.
642 * @return string[] The labels.
643 */
644 public static function get_bands_labels($bands, $bandwidth, $quiz) {
645 $bandlabels = [];
646 for ($i = 1; $i <= $bands; $i++) {
647 $bandlabels[] = quiz_format_grade($quiz, ($i - 1) * $bandwidth) . ' - ' . quiz_format_grade($quiz, $i * $bandwidth);
648 }
649 return $bandlabels;
650 }
651
652 /**
653 * Get a chart.
654 *
655 * @param string[] $labels Chart labels.
656 * @param int[] $data The data.
657 * @return \core\chart_base
658 */
659 protected static function get_chart($labels, $data) {
660 $chart = new \core\chart_bar();
661 $chart->set_labels($labels);
662 $chart->get_xaxis(0, true)->set_label(get_string('grade'));
663
664 $yaxis = $chart->get_yaxis(0, true);
665 $yaxis->set_label(get_string('participants'));
666 $yaxis->set_stepsize(max(1, round(max($data) / 10)));
667
668 $series = new \core\chart_series(get_string('participants'), $data);
669 $chart->add_series($series);
670 return $chart;
671 }
7bbe08a2 672}