MDL-34164 quiz reports: only try to show graphs if GD is installed.
[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) {
04de7aff 43 global $CFG, $DB, $OUTPUT, $PAGE;
7bbe08a2 44
e24ee794 45 list($currentgroup, $students, $groupstudents, $allowed) =
303aa3b8 46 $this->init('overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
e97d60ad 47 $options = new quiz_overview_options('overview', $quiz, $cm, $course);
e24ee794 48
303aa3b8 49 if ($fromform = $this->form->get_data()) {
dcd65f1b 50 $options->process_settings_from_form($fromform);
e24ee794 51
0217f932 52 } else {
dcd65f1b 53 $options->process_settings_from_params();
0217f932 54 }
e24ee794 55
dcd65f1b 56 $this->form->set_data($options->get_initial_form_data());
e24ee794 57
863872e3 58 if ($options->attempts == self::ALL_WITH) {
e24ee794
TH
59 // This option is only available to users who can access all groups in
60 // groups mode, so setting allowed to empty (which means all quiz attempts
61 // are accessible, is not a security porblem.
f05fedc8
TH
62 $allowed = array();
63 }
64
e24ee794 65 // Load the required questions.
88b3eb8b 66 $questions = quiz_report_get_significant_questions($quiz);
e24ee794 67
26aded55
TH
68 // Prepare for downloading, if applicable.
69 $courseshortname = format_string($course->shortname, true,
70 array('context' => context_course::instance($course->id)));
303aa3b8 71 $table = new quiz_overview_table($quiz, $this->context, $this->qmsubselect,
e97d60ad 72 $options, $groupstudents, $students, $questions, $this->get_base_url());
e24ee794 73 $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
8ebbb06a 74 $courseshortname, $quiz->name);
dcd65f1b 75 $table->is_downloading($options->download, $filename,
26aded55 76 $courseshortname . ' ' . format_string($quiz->name, true));
e24ee794
TH
77 if ($table->is_downloading()) {
78 raise_memory_limit(MEMORY_EXTRA);
79 }
80
a558acf9 81 $this->process_actions($quiz, $cm, $currentgroup, $groupstudents, $allowed, $options->get_url());
aeb15530 82
e24ee794
TH
83 // Start output.
84 if (!$table->is_downloading()) {
768a7588 85 // Only print headers if not asked to download data.
361cf27d 86 $this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
e24ee794
TH
87 }
88
768a7588 89 if ($groupmode = groups_get_activity_groupmode($cm)) {
4747c788 90 // Groups are being used, so output the group selector if we are not downloading.
c35f3afc 91 if (!$table->is_downloading()) {
e97d60ad 92 groups_print_activity_menu($cm, $options->get_url());
970d0fe0 93 }
2d7617c6 94 }
98f38217 95
768a7588 96 // Print information on the number of existing attempts.
4747c788
TH
97 if (!$table->is_downloading()) {
98 // Do not print notices when downloading.
e24ee794
TH
99 if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
100 echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
101 }
102 }
103
3c6185e9
TH
104 $hasquestions = quiz_questions_in_quiz($quiz->questions);
105 if (!$table->is_downloading()) {
106 if (!$hasquestions) {
107 echo quiz_no_questions_message($quiz, $cm, $this->context);
108 } else if (!$students) {
414e7276 109 echo $OUTPUT->notification(get_string('nostudentsyet'));
3c6185e9 110 } else if ($currentgroup && !$groupstudents) {
414e7276
TH
111 echo $OUTPUT->notification(get_string('nostudentsingroup'));
112 }
e24ee794 113
4747c788 114 // Print the display options.
303aa3b8 115 $this->form->display();
f6c7f158 116 }
39790bd8 117
3c6185e9 118 $hasstudents = $students && (!$currentgroup || $groupstudents);
863872e3 119 if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
768a7588 120 // Construct the SQL.
25a03faa
TH
121 $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') .
122 ' AS uniqueid, ';
303aa3b8 123 if ($this->qmsubselect) {
55caa1d5 124 $fields .=
125 "(CASE " .
303aa3b8 126 " WHEN {$this->qmsubselect} THEN 1" .
55caa1d5 127 " ELSE 0 " .
128 "END) AS gradedattempt, ";
129 }
aeb15530 130
7ddfd168 131 list($fields, $from, $where, $params) = $table->base_sql($allowed);
aeb15530 132
abe67b24 133 $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params);
98f38217 134
e24ee794 135 // Test to see if there are any regraded attempts to be listed.
25a03faa
TH
136 $fields .= ", COALESCE((
137 SELECT MAX(qqr.regraded)
138 FROM {quiz_overview_regrades} qqr
139 WHERE qqr.questionusageid = quiza.uniqueid
140 ), -1) AS regraded";
dcd65f1b 141 if ($options->onlyregraded) {
25a03faa
TH
142 $where .= " AND COALESCE((
143 SELECT MAX(qqr.regraded)
144 FROM {quiz_overview_regrades} qqr
145 WHERE qqr.questionusageid = quiza.uniqueid
146 ), -1) <> -1";
98f38217 147 }
abe67b24 148 $table->set_sql($fields, $from, $where, $params);
aeb15530 149
e24ee794 150 if (!$table->is_downloading()) {
768a7588 151 // Output the regrade buttons.
07a2b2f0 152 if (has_capability('mod/quiz:regrade', $this->context)) {
e24ee794
TH
153 $regradesneeded = $this->count_question_attempts_needing_regrade(
154 $quiz, $groupstudents);
07a2b2f0 155 if ($currentgroup) {
39790bd8 156 $a= new stdClass();
f29e6691 157 $a->groupname = groups_get_group_name($currentgroup);
9101efd3 158 $a->coursestudents = get_string('participants');
e24ee794 159 $a->countregradeneeded = $regradesneeded;
25a03faa
TH
160 $regradealldrydolabel =
161 get_string('regradealldrydogroup', 'quiz_overview', $a);
162 $regradealldrylabel =
163 get_string('regradealldrygroup', 'quiz_overview', $a);
164 $regradealllabel =
165 get_string('regradeallgroup', 'quiz_overview', $a);
f29e6691 166 } else {
25a03faa
TH
167 $regradealldrydolabel =
168 get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
169 $regradealldrylabel =
170 get_string('regradealldry', 'quiz_overview');
171 $regradealllabel =
172 get_string('regradeall', 'quiz_overview');
f29e6691 173 }
e97d60ad 174 $displayurl = new moodle_url($options->get_url(), array('sesskey' => sesskey()));
f29e6691 175 echo '<div class="mdl-align">';
eb788065 176 echo '<form action="'.$displayurl->out_omit_querystring().'">';
f29e6691 177 echo '<div>';
6ea66ff3 178 echo html_writer::input_hidden_params($displayurl);
f29e6691 179 echo '<input type="submit" name="regradeall" value="'.$regradealllabel.'"/>';
25a03faa
TH
180 echo '<input type="submit" name="regradealldry" value="' .
181 $regradealldrylabel . '"/>';
e24ee794 182 if ($regradesneeded) {
25a03faa
TH
183 echo '<input type="submit" name="regradealldrydo" value="' .
184 $regradealldrydolabel . '"/>';
f29e6691 185 }
186 echo '</div>';
187 echo '</form>';
188 echo '</div>';
189 }
768a7588 190 // Print information on the grading method.
25a03faa 191 if ($strattempthighlight = quiz_report_highlighting_grading_method(
dcd65f1b 192 $quiz, $this->qmsubselect, $options->onlygraded)) {
f29e6691 193 echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
194 }
195 }
aeb15530 196
768a7588 197 // Define table columns.
e24ee794
TH
198 $columns = array();
199 $headers = array();
aeb15530 200
9e67e357 201 if (!$table->is_downloading() && $options->checkboxcolumn) {
e24ee794 202 $columns[] = 'checkbox';
25a03faa 203 $headers[] = null;
abe67b24 204 }
aeb15530 205
e24ee794 206 $this->add_user_columns($table, $columns, $headers);
c547514a 207 $this->add_state_column($columns, $headers);
e24ee794 208 $this->add_time_columns($columns, $headers);
aeb15530 209
26d9ee4e
TH
210 $this->add_grade_columns($quiz, $options->usercanseegrades, $columns, $headers, false);
211
212 if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) &&
213 $this->has_regraded_questions($from, $where, $params)) {
214 $columns[] = 'regraded';
215 $headers[] = get_string('regrade', 'quiz_overview');
216 }
217
dcd65f1b 218 if ($options->slotmarks) {
e24ee794 219 foreach ($questions as $slot => $question) {
768a7588 220 // Ignore questions of zero length.
e24ee794
TH
221 $columns[] = 'qsgrade' . $slot;
222 $header = get_string('qbrief', 'quiz', $question->number);
98f38217 223 if (!$table->is_downloading()) {
e24ee794 224 $header .= '<br />';
98f38217 225 } else {
e24ee794 226 $header .= ' ';
98f38217 227 }
e24ee794 228 $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
98f38217 229 $headers[] = $header;
eacb462e 230 }
abe67b24 231 }
e24ee794 232
e97d60ad 233 $this->set_up_table_columns($table, $columns, $headers, $this->get_base_url(), $options, false);
e24ee794 234 $table->set_attribute('class', 'generaltable generalbox grades');
aeb15530 235
dcd65f1b 236 $table->out($options->pagesize, true);
7bbe08a2 237 }
e24ee794 238
9e67e357 239 if (!$table->is_downloading() && $options->usercanseegrades) {
04de7aff 240 $output = $PAGE->get_renderer('mod_quiz');
07a2b2f0 241 if ($currentgroup && $groupstudents) {
f29e6691 242 list($usql, $params) = $DB->get_in_or_equal($groupstudents);
243 $params[] = $quiz->id;
25a03faa
TH
244 if ($DB->record_exists_select('quiz_grades', "userid $usql AND quiz = ?",
245 $params)) {
04de7aff 246 $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
25a03faa 247 array('id' => $quiz->id, 'groupid' => $currentgroup));
04de7aff 248 $graphname = get_string('overviewreportgraphgroup', 'quiz_overview',
25a03faa 249 groups_get_group_name($currentgroup));
04de7aff 250 echo $output->graph($imageurl, $graphname);
f29e6691 251 }
252 }
e24ee794 253
07a2b2f0 254 if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) {
04de7aff 255 $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
25a03faa 256 array('id' => $quiz->id));
04de7aff
TH
257 $graphname = get_string('overviewreportgraph', 'quiz_overview');
258 echo $output->graph($imageurl, $graphname);
aad5b0fc 259 }
78517b5a 260 }
7bbe08a2 261 return true;
262 }
e24ee794 263
a558acf9
TH
264 protected function process_actions($quiz, $cm, $currentgroup, $groupstudents, $allowed, $redirecturl) {
265 parent::process_actions($quiz, $cm, $currentgroup, $groupstudents, $allowed, $redirecturl);
361cf27d
TH
266
267 if (empty($currentgroup) || $groupstudents) {
268 if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
269 if ($attemptids = optional_param_array('attemptid', array(), PARAM_INT)) {
270 require_capability('mod/quiz:regrade', $this->context);
271 $this->regrade_attempts($quiz, false, $groupstudents, $attemptids);
a558acf9 272 redirect($redirecturl, '', 5);
361cf27d
TH
273 }
274 }
275 }
276
277 if (optional_param('regradeall', 0, PARAM_BOOL) && confirm_sesskey()) {
278 require_capability('mod/quiz:regrade', $this->context);
279 $this->regrade_attempts($quiz, false, $groupstudents);
a558acf9 280 redirect($redirecturl, '', 5);
361cf27d
TH
281
282 } else if (optional_param('regradealldry', 0, PARAM_BOOL) && confirm_sesskey()) {
283 require_capability('mod/quiz:regrade', $this->context);
284 $this->regrade_attempts($quiz, true, $groupstudents);
a558acf9 285 redirect($redirecturl, '', 5);
361cf27d
TH
286
287 } else if (optional_param('regradealldrydo', 0, PARAM_BOOL) && confirm_sesskey()) {
288 require_capability('mod/quiz:regrade', $this->context);
289 $this->regrade_attempts_needing_it($quiz, $groupstudents);
a558acf9 290 redirect($redirecturl, '', 5);
361cf27d
TH
291 }
292 }
293
98f38217 294 /**
e24ee794
TH
295 * Regrade a particular quiz attempt. Either for real ($dryrun = false), or
296 * as a pretend regrade to see which fractions would change. The outcome is
297 * stored in the quiz_overview_regrades table.
298 *
299 * Note, $attempt is not upgraded in the database. The caller needs to do that.
300 * However, $attempt->sumgrades is updated, if this is not a dry run.
301 *
302 * @param object $attempt the quiz attempt to regrade.
f7970e3c 303 * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
e24ee794
TH
304 * @param array $slots if null, regrade all questions, otherwise, just regrade
305 * the quetsions with those slots.
98f38217 306 */
e24ee794
TH
307 protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
308 global $DB;
01118710 309 set_time_limit(30);
98f38217 310
e24ee794 311 $transaction = $DB->start_delegated_transaction();
98f38217 312
e24ee794
TH
313 $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
314
315 if (is_null($slots)) {
316 $slots = $quba->get_slots();
98f38217 317 }
e24ee794 318
be18f589 319 $finished = $attempt->state == quiz_attempt::FINISHED;
e24ee794 320 foreach ($slots as $slot) {
0ff4bd08 321 $qqr = new stdClass();
e24ee794
TH
322 $qqr->oldfraction = $quba->get_question_fraction($slot);
323
324 $quba->regrade_question($slot, $finished);
325
326 $qqr->newfraction = $quba->get_question_fraction($slot);
327
328 if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) {
329 $qqr->questionusageid = $quba->get_id();
330 $qqr->slot = $slot;
331 $qqr->regraded = empty($dryrun);
332 $qqr->timemodified = time();
333 $DB->insert_record('quiz_overview_regrades', $qqr, false);
98f38217 334 }
98f38217 335 }
336
e24ee794
TH
337 if (!$dryrun) {
338 question_engine::save_questions_usage_by_activity($quba);
98f38217 339 }
e24ee794
TH
340
341 $transaction->allow_commit();
01118710
K
342
343 // Really, PHP should not need this hint, but without this, we just run out of memory.
344 $quba = null;
345 $transaction = null;
346 gc_collect_cycles();
98f38217 347 }
e24ee794
TH
348
349 /**
350 * Regrade attempts for this quiz, exactly which attempts are regraded is
351 * controlled by the parameters.
352 * @param object $quiz the quiz settings.
f7970e3c 353 * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
e24ee794
TH
354 * @param array $groupstudents blank for all attempts, otherwise regrade attempts
355 * for these users.
356 * @param array $attemptids blank for all attempts, otherwise only regrade
357 * attempts whose id is in this list.
358 */
359 protected function regrade_attempts($quiz, $dryrun = false,
360 $groupstudents = array(), $attemptids = array()) {
98f38217 361 global $DB;
e24ee794
TH
362
363 $where = "quiz = ? AND preview = 0";
364 $params = array($quiz->id);
365
07a2b2f0 366 if ($groupstudents) {
e24ee794
TH
367 list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
368 $where .= " AND userid $usql";
369 $params = array_merge($params, $uparams);
98f38217 370 }
e24ee794
TH
371
372 if ($attemptids) {
373 list($asql, $aparams) = $DB->get_in_or_equal($attemptids);
374 $where .= " AND id $asql";
375 $params = array_merge($params, $aparams);
98f38217 376 }
98f38217 377
e24ee794
TH
378 $attempts = $DB->get_records_select('quiz_attempts', $where, $params);
379 if (!$attempts) {
380 return;
381 }
98f38217 382
e24ee794 383 $this->clear_regrade_table($quiz, $groupstudents);
98f38217 384
98f38217 385 foreach ($attempts as $attempt) {
e24ee794
TH
386 $this->regrade_attempt($attempt, $dryrun);
387 }
388
389 if (!$dryrun) {
390 $this->update_overall_grades($quiz);
98f38217 391 }
98f38217 392 }
393
e24ee794
TH
394 /**
395 * Regrade those questions in those attempts that are marked as needing regrading
396 * in the quiz_overview_regrades table.
397 * @param object $quiz the quiz settings.
398 * @param array $groupstudents blank for all attempts, otherwise regrade attempts
399 * for these users.
400 */
401 protected function regrade_attempts_needing_it($quiz, $groupstudents) {
98f38217 402 global $DB;
e24ee794
TH
403
404 $where = "quiza.quiz = ? AND quiza.preview = 0 AND qqr.regraded = 0";
405 $params = array($quiz->id);
406
768a7588 407 // Fetch all attempts that need regrading.
07a2b2f0 408 if ($groupstudents) {
e24ee794
TH
409 list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
410 $where .= " AND quiza.userid $usql";
411 $params += $uparams;
98f38217 412 }
23277af8 413
e24ee794
TH
414 $toregrade = $DB->get_records_sql("
415 SELECT quiza.uniqueid, qqr.slot
416 FROM {quiz_attempts} quiza
417 JOIN {quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid
418 WHERE $where", $params);
419
420 if (!$toregrade) {
421 return;
98f38217 422 }
423
e24ee794
TH
424 $attemptquestions = array();
425 foreach ($toregrade as $row) {
426 $attemptquestions[$row->uniqueid][] = $row->slot;
98f38217 427 }
25a03faa
TH
428 $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
429 array_keys($attemptquestions));
e24ee794
TH
430
431 $this->clear_regrade_table($quiz, $groupstudents);
432
433 foreach ($attempts as $attempt) {
e24ee794 434 $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]);
98f38217 435 }
e24ee794
TH
436
437 $this->update_overall_grades($quiz);
98f38217 438 }
f05fedc8 439
e24ee794
TH
440 /**
441 * Count the number of attempts in need of a regrade.
442 * @param object $quiz the quiz settings.
443 * @param array $groupstudents user ids. If this is given, only data relating
444 * to these users is cleared.
445 */
446 protected function count_question_attempts_needing_regrade($quiz, $groupstudents) {
447 global $DB;
448
449 $usertest = '';
450 $params = array();
451 if ($groupstudents) {
b149b788 452 list($usql, $params) = $DB->get_in_or_equal($groupstudents);
e24ee794 453 $usertest = "quiza.userid $usql AND ";
98f38217 454 }
e24ee794
TH
455
456 $params[] = $quiz->id;
457 $sql = "SELECT COUNT(DISTINCT quiza.id)
458 FROM {quiz_attempts} quiza
459 JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid
460 WHERE
461 $usertest
462 quiza.quiz = ? AND
463 quiza.preview = 0 AND
464 qqr.regraded = 0";
465 return $DB->count_records_sql($sql, $params);
466 }
467
468 /**
469 * Are there any pending regrades in the table we are going to show?
470 * @param string $from tables used by the main query.
471 * @param string $where where clause used by the main query.
472 * @param array $params required by the SQL.
473 * @return bool whether there are pending regrades.
474 */
475 protected function has_regraded_questions($from, $where, $params) {
476 global $DB;
477 $qubaids = new qubaid_join($from, 'uniqueid', $where, $params);
478 return $DB->record_exists_select('quiz_overview_regrades',
25a03faa
TH
479 'questionusageid ' . $qubaids->usage_id_in(),
480 $qubaids->usage_id_in_params());
98f38217 481 }
f05fedc8 482
e24ee794
TH
483 /**
484 * Remove all information about pending/complete regrades from the database.
485 * @param object $quiz the quiz settings.
486 * @param array $groupstudents user ids. If this is given, only data relating
487 * to these users is cleared.
488 */
489 protected function clear_regrade_table($quiz, $groupstudents) {
98f38217 490 global $DB;
e24ee794 491
768a7588 492 // Fetch all attempts that need regrading.
e24ee794
TH
493 $where = '';
494 $params = array();
07a2b2f0 495 if ($groupstudents) {
98f38217 496 list($usql, $params) = $DB->get_in_or_equal($groupstudents);
e24ee794 497 $where = "userid $usql AND ";
98f38217 498 }
aeb15530 499
98f38217 500 $params[] = $quiz->id;
e24ee794
TH
501 $DB->delete_records_select('quiz_overview_regrades',
502 "questionusageid IN (
503 SELECT uniqueid
504 FROM {quiz_attempts}
505 WHERE $where quiz = ?
506 )", $params);
507 }
98f38217 508
e24ee794
TH
509 /**
510 * Update the final grades for all attempts. This method is used following
511 * a regrade.
512 * @param object $quiz the quiz settings.
513 * @param array $userids only update scores for these userids.
514 * @param array $attemptids attemptids only update scores for these attempt ids.
515 */
516 protected function update_overall_grades($quiz) {
517 quiz_update_all_attempt_sumgrades($quiz);
518 quiz_update_all_final_grades($quiz);
519 quiz_update_grades($quiz);
98f38217 520 }
7bbe08a2 521}