71cb1b02c2959d152d0c24f8d97f414dbf1cb21e
[moodle.git] / mod / quiz / report / overview / report.php
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/>.
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  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport.php');
29 require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_options.php');
30 require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_form.php');
31 require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_table.php');
34 /**
35  * Quiz report subclass for the overview (grades) report.
36  *
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
39  */
40 class quiz_overview_report extends quiz_attempts_report {
42     public function display($quiz, $cm, $course) {
43         global $DB, $OUTPUT, $PAGE;
45         list($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins) = $this->init(
46                 'overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
48         $options = new quiz_overview_options('overview', $quiz, $cm, $course);
50         if ($fromform = $this->form->get_data()) {
51             $options->process_settings_from_form($fromform);
53         } else {
54             $options->process_settings_from_params();
55         }
57         $this->form->set_data($options->get_initial_form_data());
59         // Load the required questions.
60         $questions = quiz_report_get_significant_questions($quiz);
62         // Prepare for downloading, if applicable.
63         $courseshortname = format_string($course->shortname, true,
64                 array('context' => context_course::instance($course->id)));
65         $table = new quiz_overview_table($quiz, $this->context, $this->qmsubselect,
66                 $options, $groupstudentsjoins, $studentsjoins, $questions, $options->get_url());
67         $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
68                 $courseshortname, $quiz->name);
69         $table->is_downloading($options->download, $filename,
70                 $courseshortname . ' ' . format_string($quiz->name, true));
71         if ($table->is_downloading()) {
72             raise_memory_limit(MEMORY_EXTRA);
73         }
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         }
98         $this->course = $course; // Hack to make this available in process_actions.
99         $this->process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $options->get_url());
101         // Start output.
102         if (!$table->is_downloading()) {
103             // Only print headers if not asked to download data.
104             $this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
105         }
107         if ($groupmode = groups_get_activity_groupmode($cm)) {
108             // Groups are being used, so output the group selector if we are not downloading.
109             if (!$table->is_downloading()) {
110                 groups_print_activity_menu($cm, $options->get_url());
111             }
112         }
114         // Print information on the number of existing attempts.
115         if (!$table->is_downloading()) {
116             // Do not print notices when downloading.
117             if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
118                 echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
119             }
120         }
122         $hasquestions = quiz_has_questions($quiz->id);
123         if (!$table->is_downloading()) {
124             if (!$hasquestions) {
125                 echo quiz_no_questions_message($quiz, $cm, $this->context);
126             } else if (!$hasstudents) {
127                 echo $OUTPUT->notification(get_string('nostudentsyet'));
128             } else if ($currentgroup && !$this->hasgroupstudents) {
129                 echo $OUTPUT->notification(get_string('nostudentsingroup'));
130             }
132             // Print the display options.
133             $this->form->display();
134         }
136         $hasstudents = $hasstudents && (!$currentgroup || $this->hasgroupstudents);
137         if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
138             // Construct the SQL.
139             list($fields, $from, $where, $params) = $table->base_sql($allowedjoins);
141             $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params);
143             // Test to see if there are any regraded attempts to be listed.
144             $fields .= ", COALESCE((
145                                 SELECT MAX(qqr.regraded)
146                                   FROM {quiz_overview_regrades} qqr
147                                  WHERE qqr.questionusageid = quiza.uniqueid
148                           ), -1) AS regraded";
149             if ($options->onlyregraded) {
150                 $where .= " AND COALESCE((
151                                     SELECT MAX(qqr.regraded)
152                                       FROM {quiz_overview_regrades} qqr
153                                      WHERE qqr.questionusageid = quiza.uniqueid
154                                 ), -1) <> -1";
155             }
156             $table->set_sql($fields, $from, $where, $params);
158             if (!$table->is_downloading()) {
159                 // Output the regrade buttons.
160                 if (has_capability('mod/quiz:regrade', $this->context)) {
161                     $regradesneeded = $this->count_question_attempts_needing_regrade(
162                             $quiz, $groupstudentsjoins);
163                     if ($currentgroup) {
164                         $a= new stdClass();
165                         $a->groupname = groups_get_group_name($currentgroup);
166                         $a->coursestudents = get_string('participants');
167                         $a->countregradeneeded = $regradesneeded;
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);
174                     } else {
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');
181                     }
182                     $displayurl = new moodle_url($options->get_url(), array('sesskey' => sesskey()));
183                     echo '<div class="mdl-align">';
184                     echo '<form action="'.$displayurl->out_omit_querystring().'">';
185                     echo '<div>';
186                     echo html_writer::input_hidden_params($displayurl);
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="' .
189                             $regradealldrylabel . '"/>';
190                     if ($regradesneeded) {
191                         echo '<input type="submit" class="btn btn-secondary m-l-1" name="regradealldrydo" value="' .
192                                 $regradealldrydolabel . '"/>';
193                     }
194                     echo '</div>';
195                     echo '</form>';
196                     echo '</div>';
197                 }
198                 // Print information on the grading method.
199                 if ($strattempthighlight = quiz_report_highlighting_grading_method(
200                         $quiz, $this->qmsubselect, $options->onlygraded)) {
201                     echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
202                 }
203             }
205             // Define table columns.
206             $columns = array();
207             $headers = array();
209             if (!$table->is_downloading() && $options->checkboxcolumn) {
210                 $columns[] = 'checkbox';
211                 $headers[] = null;
212             }
214             $this->add_user_columns($table, $columns, $headers);
215             $this->add_state_column($columns, $headers);
216             $this->add_time_columns($columns, $headers);
218             $this->add_grade_columns($quiz, $options->usercanseegrades, $columns, $headers, false);
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             }
226             if ($options->slotmarks) {
227                 foreach ($questions as $slot => $question) {
228                     // Ignore questions of zero length.
229                     $columns[] = 'qsgrade' . $slot;
230                     $header = get_string('qbrief', 'quiz', $question->number);
231                     if (!$table->is_downloading()) {
232                         $header .= '<br />';
233                     } else {
234                         $header .= ' ';
235                     }
236                     $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
237                     $headers[] = $header;
238                 }
239             }
241             $this->set_up_table_columns($table, $columns, $headers, $this->get_base_url(), $options, false);
242             $table->set_attribute('class', 'generaltable generalbox grades');
244             $table->out($options->pagesize, true);
245         }
247         if (!$table->is_downloading() && $options->usercanseegrades) {
248             $output = $PAGE->get_renderer('mod_quiz');
249             list($bands, $bandwidth) = self::get_bands_count_and_width($quiz);
250             $labels = self::get_bands_labels($bands, $bandwidth, $quiz);
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);
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);
263                 }
264             }
266             if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) {
267                 $data = quiz_report_grade_bands($bandwidth, $bands, $quiz->id, new \core\dml\sql_join());
268                 $chart = self::get_chart($labels, $data);
269                 $graphname = get_string('overviewreportgraph', 'quiz_overview');
270                 echo $output->chart($chart, $graphname);
271             }
272         }
273         return true;
274     }
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);
290         if (empty($currentgroup) || $this->hasgroupstudents) {
291             if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
292                 if ($attemptids = optional_param_array('attemptid', array(), PARAM_INT)) {
293                     $this->start_regrade($quiz, $cm);
294                     $this->regrade_attempts($quiz, false, $groupstudentsjoins, $attemptids);
295                     $this->finish_regrade($redirecturl);
296                 }
297             }
298         }
300         if (optional_param('regradeall', 0, PARAM_BOOL) && confirm_sesskey()) {
301             $this->start_regrade($quiz, $cm);
302             $this->regrade_attempts($quiz, false, $groupstudentsjoins);
303             $this->finish_regrade($redirecturl);
305         } else if (optional_param('regradealldry', 0, PARAM_BOOL) && confirm_sesskey()) {
306             $this->start_regrade($quiz, $cm);
307             $this->regrade_attempts($quiz, true, $groupstudentsjoins);
308             $this->finish_regrade($redirecturl);
310         } else if (optional_param('regradealldrydo', 0, PARAM_BOOL) && confirm_sesskey()) {
311             $this->start_regrade($quiz, $cm);
312             $this->regrade_attempts_needing_it($quiz, $groupstudentsjoins);
313             $this->finish_regrade($redirecturl);
314         }
315     }
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) {
323         require_capability('mod/quiz:regrade', $this->context);
324         $this->print_header_and_tabs($cm, $this->course, $quiz, $this->mode);
325     }
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) {
333         global $OUTPUT;
334         \core\notification::success(get_string('regradecomplete', 'quiz_overview'));
335         echo $OUTPUT->continue_button($nexturl);
336         echo $OUTPUT->footer();
337         die();
338     }
340     /**
341      * Unlock the session and allow the regrading process to run in the background.
342      */
343     protected function unlock_session() {
344         \core\session\manager::write_close();
345         ignore_user_abort(true);
346     }
348     /**
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.
357      * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
358      * @param array $slots if null, regrade all questions, otherwise, just regrade
359      *      the quetsions with those slots.
360      */
361     protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
362         global $DB;
363         // Need more time for a quiz with many questions.
364         core_php_time_limit::raise(300);
366         $transaction = $DB->start_delegated_transaction();
368         $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
370         if (is_null($slots)) {
371             $slots = $quba->get_slots();
372         }
374         $finished = $attempt->state == quiz_attempt::FINISHED;
375         foreach ($slots as $slot) {
376             $qqr = new stdClass();
377             $qqr->oldfraction = $quba->get_question_fraction($slot);
379             $quba->regrade_question($slot, $finished);
381             $qqr->newfraction = $quba->get_question_fraction($slot);
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);
389             }
390         }
392         if (!$dryrun) {
393             question_engine::save_questions_usage_by_activity($quba);
394         }
396         $transaction->allow_commit();
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();
402     }
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.
408      * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
409      * @param \core\dml\sql_join|array $groupstudentsjoins empty for all attempts, otherwise regrade attempts
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,
415             \core\dml\sql_join$groupstudentsjoins = null, $attemptids = array()) {
416         global $DB;
417         $this->unlock_session();
419         $sql = "SELECT quiza.*
420                   FROM {quiz_attempts} quiza";
421         $where = "quiz = :qid AND preview = 0";
422         $params = array('qid' => $quiz->id);
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;
429         }
431         if ($attemptids) {
432             $aids = join(',', $attemptids);
433             $where .= " AND quiza.id IN ({$aids})";
434         }
436         $sql .= "\nWHERE {$where}";
437         $attempts = $DB->get_records_sql($sql, $params);
438         if (!$attempts) {
439             return;
440         }
442         $this->clear_regrade_table($quiz, $groupstudentsjoins);
444         $progressbar = new progress_bar('quiz_overview_regrade', 500, true);
445         $a = array(
446             'count' => count($attempts),
447             'done'  => 0,
448         );
449         foreach ($attempts as $attempt) {
450             $this->regrade_attempt($attempt, $dryrun);
451             $a['done']++;
452             $progressbar->update($a['done'], $a['count'],
453                     get_string('regradingattemptxofy', 'quiz_overview', $a));
454         }
456         if (!$dryrun) {
457             $this->update_overall_grades($quiz);
458         }
459     }
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.
465      * @param \core\dml\sql_join $groupstudentsjoins empty for all attempts, otherwise regrade attempts
466      * for these users.
467      */
468     protected function regrade_attempts_needing_it($quiz, \core\dml\sql_join $groupstudentsjoins) {
469         global $DB;
470         $this->unlock_session();
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);
476         // Fetch all attempts that need regrading.
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;
482         }
484         $toregrade = $DB->get_recordset_sql("
485                 SELECT quiza.uniqueid, qqr.slot
486                   FROM {quiz_attempts} quiza
487                   JOIN $join
488                  WHERE $where", $params);
490         $attemptquestions = array();
491         foreach ($toregrade as $row) {
492             $attemptquestions[$row->uniqueid][] = $row->slot;
493         }
494         $toregrade->close();
496         if (!$attemptquestions) {
497             return;
498         }
500         $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
501                 array_keys($attemptquestions));
503         $this->clear_regrade_table($quiz, $groupstudentsjoins);
505         $progressbar = new progress_bar('quiz_overview_regrade', 500, true);
506         $a = array(
507             'count' => count($attempts),
508             'done'  => 0,
509         );
510         foreach ($attempts as $attempt) {
511             $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]);
512             $a['done']++;
513             $progressbar->update($a['done'], $a['count'],
514                     get_string('regradingattemptxofy', 'quiz_overview', $a));
515         }
517         $this->update_overall_grades($quiz);
518     }
520     /**
521      * Count the number of attempts in need of a regrade.
522      * @param object $quiz the quiz settings.
523      * @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params) If this is given, only data relating
524      * to these users is cleared.
525      */
526     protected function count_question_attempts_needing_regrade($quiz, \core\dml\sql_join $groupstudentsjoins) {
527         global $DB;
529         $userjoin = '';
530         $usertest = '';
531         $params = array();
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;
537         }
539         $params['cquiz'] = $quiz->id;
540         $sql = "SELECT COUNT(DISTINCT quiza.id)
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";
549         return $DB->count_records_sql($sql, $params);
550     }
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;
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);
566     }
568     /**
569      * Remove all information about pending/complete regrades from the database.
570      * @param object $quiz the quiz settings.
571      * @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params). If this is given, only data relating
572      * to these users is cleared.
573      */
574     protected function clear_regrade_table($quiz, \core\dml\sql_join $groupstudentsjoins) {
575         global $DB;
577         // Fetch all attempts that need regrading.
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;
588         }
589         $select .= "\n$where)";
591         $DB->delete_records_select('quiz_overview_regrades', $select, $params);
592     }
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);
605     }
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     }
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     }
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'));
664         $yaxis = $chart->get_yaxis(0, true);
665         $yaxis->set_label(get_string('participants'));
666         $yaxis->set_stepsize(max(1, round(max($data) / 10)));
668         $series = new \core\chart_series(get_string('participants'), $data);
669         $chart->add_series($series);
670         return $chart;
671     }