2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Helper functions for the quiz reports.
21 * @copyright 2008 Jamie Pratt
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->dirroot . '/mod/quiz/lib.php');
29 require_once($CFG->libdir . '/filelib.php');
32 * Takes an array of objects and constructs a multidimensional array keyed by
33 * the keys it finds on the object.
34 * @param array $datum an array of objects with properties on the object
35 * including the keys passed as the next param.
36 * @param array $keys Array of strings with the names of the properties on the
37 * objects in datum that you want to index the multidimensional array by.
38 * @param bool $keysunique If there is not only one object for each
39 * combination of keys you are using you should set $keysunique to true.
40 * Otherwise all the object will be added to a zero based array. So the array
41 * returned will have count($keys) + 1 indexs.
42 * @return array multidimensional array properly indexed.
44 function quiz_report_index_by_keys($datum, $keys, $keysunique = true) {
48 $key = array_shift($keys);
49 $datumkeyed = array();
50 foreach ($datum as $data) {
51 if ($keys || !$keysunique) {
52 $datumkeyed[$data->{$key}][]= $data;
54 $datumkeyed[$data->{$key}]= $data;
58 foreach ($datumkeyed as $datakey => $datakeyed) {
59 $datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys, $keysunique);
65 function quiz_report_unindex($datum) {
69 $datumunkeyed = array();
70 foreach ($datum as $value) {
71 if (is_array($value)) {
72 $datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value));
74 $datumunkeyed[] = $value;
81 * Get the slots of real questions (not descriptions) in this quiz, in order.
82 * @param object $quiz the quiz.
83 * @return array of slot => $question object with fields
84 * ->slot, ->id, ->maxmark, ->number, ->length.
86 function quiz_report_get_significant_questions($quiz) {
89 $questionids = quiz_questions_in_quiz($quiz->questions);
90 if (empty($questionids)) {
94 list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionids));
95 $params[] = $quiz->id;
96 $questions = $DB->get_records_sql("
103 JOIN {quiz_question_instances} qqi ON qqi.question = q.id
108 length > 0", $params);
112 foreach (explode(',', $questionids) as $key => $id) {
113 if (!array_key_exists($id, $questions)) {
118 $question = $questions[$id];
119 $question->slot = $slot;
120 $question->number = $number;
122 $qsbyslot[$slot] = $question;
124 $number += $question->length;
131 * @param object $quiz the quiz settings.
132 * @return bool whether, for this quiz, it is possible to filter attempts to show
133 * only those that gave the final grade.
135 function quiz_report_can_filter_only_graded($quiz) {
136 return $quiz->attempts != 1 && $quiz->grademethod != QUIZ_GRADEAVERAGE;
140 * Given the quiz grading method return sub select sql to find the id of the
141 * one attempt that will be graded for each user. Or return
142 * empty string if all attempts contribute to final grade.
144 function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') {
145 if ($quiz->attempts == 1) {
146 // This quiz only allows one attempt.
150 switch ($quiz->grademethod) {
151 case QUIZ_GRADEHIGHEST :
152 return "$quizattemptsalias.id = (
154 FROM {quiz_attempts} qa2
155 WHERE qa2.quiz = $quizattemptsalias.quiz AND
156 qa2.userid = $quizattemptsalias.userid AND
157 COALESCE(qa2.sumgrades, 0) = (
158 SELECT MAX(COALESCE(qa3.sumgrades, 0))
159 FROM {quiz_attempts} qa3
160 WHERE qa3.quiz = $quizattemptsalias.quiz AND
161 qa3.userid = $quizattemptsalias.userid
165 case QUIZ_GRADEAVERAGE :
168 case QUIZ_ATTEMPTFIRST :
169 return "$quizattemptsalias.id = (
171 FROM {quiz_attempts} qa2
172 WHERE qa2.quiz = $quizattemptsalias.quiz AND
173 qa2.userid = $quizattemptsalias.userid)";
175 case QUIZ_ATTEMPTLAST :
176 return "$quizattemptsalias.id = (
178 FROM {quiz_attempts} qa2
179 WHERE qa2.quiz = $quizattemptsalias.quiz AND
180 qa2.userid = $quizattemptsalias.userid)";
185 * Get the nuber of students whose score was in a particular band for this quiz.
186 * @param number $bandwidth the width of each band.
187 * @param int $bands the number of bands
188 * @param int $quizid the quiz id.
189 * @param array $userids list of user ids.
190 * @return array band number => number of users with scores in that band.
192 function quiz_report_grade_bands($bandwidth, $bands, $quizid, $userids = array()) {
196 list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u');
197 $usql = "qg.userid $usql AND";
203 SELECT band, COUNT(1)
206 SELECT FLOOR(qg.grade / :bandwidth) AS band
207 FROM {quiz_grades} qg
208 WHERE $usql qg.quiz = :quizid
217 $params['quizid'] = $quizid;
218 $params['bandwidth'] = $bandwidth;
220 $data = $DB->get_records_sql_menu($sql, $params);
222 // We need to create array elements with values 0 at indexes where there is no element.
223 $data = $data + array_fill(0, $bands+1, 0);
226 // Place the maximum (prefect grade) into the last band i.e. make last
227 // band for example 9 <= g <=10 (where 10 is the perfect grade) rather than
229 $data[$bands - 1] += $data[$bands];
230 unset($data[$bands]);
235 function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter) {
236 if ($quiz->attempts == 1) {
237 return '<p>' . get_string('onlyoneattemptallowed', 'quiz_overview') . '</p>';
239 } else if (!$qmsubselect) {
240 return '<p>' . get_string('allattemptscontributetograde', 'quiz_overview') . '</p>';
242 } else if ($qmfilter) {
243 return '<p>' . get_string('showinggraded', 'quiz_overview') . '</p>';
246 return '<p>' . get_string('showinggradedandungraded', 'quiz_overview',
247 '<span class="gradedattempt">' . quiz_get_grading_option_name($quiz->grademethod) .
253 * Get the feedback text for a grade on this quiz. The feedback is
254 * processed ready for display.
256 * @param float $grade a grade on this quiz.
257 * @param int $quizid the id of the quiz object.
258 * @return string the comment that corresponds to this grade (empty string if there is not one.
260 function quiz_report_feedback_for_grade($grade, $quizid, $context) {
263 static $feedbackcache = array();
265 if (!isset($feedbackcache[$quizid])) {
266 $feedbackcache[$quizid] = $DB->get_records('quiz_feedback', array('quizid' => $quizid));
269 // With CBM etc, it is possible to get -ve grades, which would then not match
270 // any feedback. Therefore, we replace -ve grades with 0.
271 $grade = max($grade, 0);
273 $feedbacks = $feedbackcache[$quizid];
276 $feedbacktextformat = FORMAT_MOODLE;
277 foreach ($feedbacks as $feedback) {
278 if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade) {
279 $feedbackid = $feedback->id;
280 $feedbacktext = $feedback->feedbacktext;
281 $feedbacktextformat = $feedback->feedbacktextformat;
286 // Clean the text, ready for display.
287 $formatoptions = new stdClass();
288 $formatoptions->noclean = true;
289 $feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php',
290 $context->id, 'mod_quiz', 'feedback', $feedbackid);
291 $feedbacktext = format_text($feedbacktext, $feedbacktextformat, $formatoptions);
293 return $feedbacktext;
297 * Format a number as a percentage out of $quiz->sumgrades
298 * @param number $rawgrade the mark to format.
299 * @param object $quiz the quiz settings
300 * @param bool $round whether to round the results ot $quiz->decimalpoints.
302 function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) {
303 if ($quiz->sumgrades == 0) {
306 if (!is_numeric($rawmark)) {
310 $mark = $rawmark * 100 / $quiz->sumgrades;
312 $mark = quiz_format_grade($quiz, $mark);
318 * Returns an array of reports to which the current user has access to.
319 * @return array reports are ordered as they should be for display in tabs.
321 function quiz_report_list($context) {
323 static $reportlist = null;
324 if (!is_null($reportlist)) {
328 $reports = $DB->get_records('quiz_reports', null, 'displayorder DESC', 'name, capability');
329 $reportdirs = get_plugin_list('quiz');
331 // Order the reports tab in descending order of displayorder.
332 $reportcaps = array();
333 foreach ($reports as $key => $report) {
334 if (array_key_exists($report->name, $reportdirs)) {
335 $reportcaps[$report->name] = $report->capability;
339 // Add any other reports, which are on disc but not in the DB, on the end.
340 foreach ($reportdirs as $reportname => $notused) {
341 if (!isset($reportcaps[$reportname])) {
342 $reportcaps[$reportname] = null;
345 $reportlist = array();
346 foreach ($reportcaps as $name => $capability) {
347 if (empty($capability)) {
348 $capability = 'mod/quiz:viewreports';
350 if (has_capability($capability, $context)) {
351 $reportlist[] = $name;
358 * Create a filename for use when downloading data from a quiz report. It is
359 * expected that this will be passed to flexible_table::is_downloading, which
360 * cleans the filename of bad characters and adds the file extension.
361 * @param string $report the type of report.
362 * @param string $courseshortname the course shortname.
363 * @param string $quizname the quiz name.
364 * @return string the filename.
366 function quiz_report_download_filename($report, $courseshortname, $quizname) {
367 return $courseshortname . '-' . format_string($quizname, true) . '-' . $report;
371 * Get the default report for the current user.
372 * @param object $context the quiz context.
374 function quiz_report_default_report($context) {
375 $reports = quiz_report_list($context);
376 return reset($reports);
380 * Generate a message saying that this quiz has no questions, with a button to
381 * go to the edit page, if the user has the right capability.
382 * @param object $quiz the quiz settings.
383 * @param object $cm the course_module object.
384 * @param object $context the quiz context.
385 * @return string HTML to output.
387 function quiz_no_questions_message($quiz, $cm, $context) {
391 $output .= $OUTPUT->notification(get_string('noquestions', 'quiz'));
392 if (has_capability('mod/quiz:manage', $context)) {
393 $output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php',
394 array('cmid' => $cm->id)), get_string('editquiz', 'quiz'), 'get');
401 * Should the grades be displayed in this report. That depends on the quiz
402 * display options, and whether the quiz is graded.
403 * @param object $quiz the quiz settings.
404 * @param context $context the quiz context.
407 function quiz_report_should_show_grades($quiz, context $context) {
408 if ($quiz->timeclose && time() > $quiz->timeclose) {
409 $when = mod_quiz_display_options::AFTER_CLOSE;
411 $when = mod_quiz_display_options::LATER_WHILE_OPEN;
413 $reviewoptions = mod_quiz_display_options::make_from_quiz($quiz, $when);
415 return quiz_has_grades($quiz) &&
416 ($reviewoptions->marks >= question_display_options::MARK_AND_MAX ||
417 has_capability('moodle/grade:viewhidden', $context));