Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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