MDL-32322 quiz reports: killing more duplication.
[moodle.git] / mod / quiz / report / reportlib.php
CommitLineData
f33c438e 1<?php
2709ee45
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
17/**
18 * Helper functions for the quiz reports.
19 *
8d76124c
TH
20 * @package mod_quiz
21 * @copyright 2008 Jamie Pratt
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2709ee45
TH
23 */
24
a17b297d
TH
25
26defined('MOODLE_INTERNAL') || die();
27
530d9866 28require_once($CFG->dirroot . '/mod/quiz/lib.php');
446166a6 29require_once($CFG->libdir . '/filelib.php');
530d9866 30
869309b8 31/**
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.
f7970e3c 38 * @param bool $keysunique If there is not only one object for each
869309b8 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.
43 */
2709ee45
TH
44function quiz_report_index_by_keys($datum, $keys, $keysunique = true) {
45 if (!$datum) {
46 return array();
98f38217 47 }
48 $key = array_shift($keys);
49 $datumkeyed = array();
2709ee45
TH
50 foreach ($datum as $data) {
51 if ($keys || !$keysunique) {
98f38217 52 $datumkeyed[$data->{$key}][]= $data;
53 } else {
54 $datumkeyed[$data->{$key}]= $data;
55 }
56 }
2709ee45
TH
57 if ($keys) {
58 foreach ($datumkeyed as $datakey => $datakeyed) {
869309b8 59 $datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys, $keysunique);
98f38217 60 }
61 }
62 return $datumkeyed;
63}
2709ee45
TH
64
65function quiz_report_unindex($datum) {
66 if (!$datum) {
869309b8 67 return $datum;
68 }
69 $datumunkeyed = array();
2709ee45
TH
70 foreach ($datum as $value) {
71 if (is_array($value)) {
869309b8 72 $datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value));
73 } else {
74 $datumunkeyed[] = $value;
75 }
76 }
77 return $datumunkeyed;
78}
720be6f2 79
2badf2e6 80/**
2709ee45
TH
81 * Get the slots of real questions (not descriptions) in this quiz, in order.
82 * @param object $quiz the quiz.
25a03faa
TH
83 * @return array of slot => $question object with fields
84 * ->slot, ->id, ->maxmark, ->number, ->length.
2badf2e6 85 */
cf3b6568 86function quiz_report_get_significant_questions($quiz) {
2709ee45
TH
87 global $DB;
88
cf3b6568 89 $questionids = quiz_questions_in_quiz($quiz->questions);
3c6185e9
TH
90 if (empty($questionids)) {
91 return array();
92 }
93
cf3b6568 94 list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionids));
9cf4a18b 95 $params[] = $quiz->id;
2709ee45
TH
96 $questions = $DB->get_records_sql("
97SELECT
98 q.id,
99 q.length,
100 qqi.grade AS maxmark
101
102FROM {question} q
103JOIN {quiz_question_instances} qqi ON qqi.question = q.id
104
105WHERE
2709ee45 106 q.id $usql AND
cf3b6568 107 qqi.quiz = ? AND
2709ee45
TH
108 length > 0", $params);
109
110 $qsbyslot = array();
2badf2e6 111 $number = 1;
2709ee45
TH
112 foreach (explode(',', $questionids) as $key => $id) {
113 if (!array_key_exists($id, $questions)) {
114 continue;
2badf2e6 115 }
2709ee45
TH
116
117 $slot = $key + 1;
118 $question = $questions[$id];
119 $question->slot = $slot;
120 $question->number = $number;
121
122 $qsbyslot[$slot] = $question;
123
124 $number += $question->length;
2badf2e6 125 }
2709ee45
TH
126
127 return $qsbyslot;
2badf2e6 128}
2709ee45 129
4469159e 130/**
131 * Given the quiz grading method return sub select sql to find the id of the
9cf4a18b 132 * one attempt that will be graded for each user. Or return
4469159e 133 * empty string if all attempts contribute to final grade.
134 */
2709ee45 135function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') {
768a7588
TH
136 if ($quiz->attempts == 1) {
137 // This quiz only allows one attempt.
b621e1a0 138 return '';
139 }
9b40c540 140
b621e1a0 141 switch ($quiz->grademethod) {
25a03faa
TH
142 case QUIZ_GRADEHIGHEST :
143 return "$quizattemptsalias.id = (
144 SELECT MIN(qa2.id)
145 FROM {quiz_attempts} qa2
146 WHERE qa2.quiz = $quizattemptsalias.quiz AND
147 qa2.userid = $quizattemptsalias.userid AND
148 COALESCE(qa2.sumgrades, 0) = (
149 SELECT MAX(COALESCE(qa3.sumgrades, 0))
150 FROM {quiz_attempts} qa3
151 WHERE qa3.quiz = $quizattemptsalias.quiz AND
152 qa3.userid = $quizattemptsalias.userid
153 )
154 )";
155
156 case QUIZ_GRADEAVERAGE :
157 return '';
158
159 case QUIZ_ATTEMPTFIRST :
160 return "$quizattemptsalias.id = (
161 SELECT MIN(qa2.id)
162 FROM {quiz_attempts} qa2
163 WHERE qa2.quiz = $quizattemptsalias.quiz AND
164 qa2.userid = $quizattemptsalias.userid)";
165
166 case QUIZ_ATTEMPTLAST :
167 return "$quizattemptsalias.id = (
168 SELECT MAX(qa2.id)
169 FROM {quiz_attempts} qa2
170 WHERE qa2.quiz = $quizattemptsalias.quiz AND
171 qa2.userid = $quizattemptsalias.userid)";
4469159e 172 }
4469159e 173}
8b87ab00 174
2709ee45
TH
175/**
176 * Get the nuber of students whose score was in a particular band for this quiz.
177 * @param number $bandwidth the width of each band.
f7970e3c
TH
178 * @param int $bands the number of bands
179 * @param int $quizid the quiz id.
2709ee45
TH
180 * @param array $userids list of user ids.
181 * @return array band number => number of users with scores in that band.
182 */
183function quiz_report_grade_bands($bandwidth, $bands, $quizid, $userids = array()) {
446166a6 184 global $DB;
2709ee45
TH
185
186 if ($userids) {
a2ac2349 187 list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u');
2709ee45 188 $usql = "qg.userid $usql AND";
8b2f8253 189 } else {
2daffca5 190 $usql = '';
8b2f8253 191 $params = array();
192 }
2709ee45 193 $sql = "
330c1148 194SELECT band, COUNT(1)
2709ee45 195
330c1148
TH
196FROM (
197 SELECT FLOOR(qg.grade / :bandwidth) AS band
198 FROM {quiz_grades} qg
199 WHERE $usql qg.quiz = :quizid
200) subquery
2709ee45
TH
201
202GROUP BY
330c1148 203 band
2709ee45
TH
204
205ORDER BY
206 band";
2daffca5
TH
207
208 $params['quizid'] = $quizid;
330c1148 209 $params['bandwidth'] = $bandwidth;
2709ee45 210
9cf4a18b 211 $data = $DB->get_records_sql_menu($sql, $params);
2709ee45 212
768a7588 213 // We need to create array elements with values 0 at indexes where there is no element.
a5686531 214 $data = $data + array_fill(0, $bands+1, 0);
8b87ab00 215 ksort($data);
2709ee45 216
768a7588
TH
217 // Place the maximum (prefect grade) into the last band i.e. make last
218 // band for example 9 <= g <=10 (where 10 is the perfect grade) rather than
219 // just 9 <= g <10.
2709ee45 220 $data[$bands - 1] += $data[$bands];
a5686531 221 unset($data[$bands]);
2709ee45 222
8b87ab00 223 return $data;
224}
2709ee45
TH
225
226function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter) {
227 if ($quiz->attempts == 1) {
228 return '<p>' . get_string('onlyoneattemptallowed', 'quiz_overview') . '</p>';
229
230 } else if (!$qmsubselect) {
231 return '<p>' . get_string('allattemptscontributetograde', 'quiz_overview') . '</p>';
232
233 } else if ($qmfilter) {
234 return '<p>' . get_string('showinggraded', 'quiz_overview') . '</p>';
235
236 } else {
237 return '<p>' . get_string('showinggradedandungraded', 'quiz_overview',
238 '<span class="gradedattempt">' . quiz_get_grading_option_name($quiz->grademethod) .
239 '</span>') . '</p>';
b621e1a0 240 }
241}
aad5b0fc 242
aad5b0fc 243/**
244 * Get the feedback text for a grade on this quiz. The feedback is
245 * processed ready for display.
246 *
247 * @param float $grade a grade on this quiz.
f7970e3c 248 * @param int $quizid the id of the quiz object.
aad5b0fc 249 * @return string the comment that corresponds to this grade (empty string if there is not one.
250 */
8ad67658 251function quiz_report_feedback_for_grade($grade, $quizid, $context) {
446166a6 252 global $DB;
99d19c13 253
aad5b0fc 254 static $feedbackcache = array();
2709ee45
TH
255
256 if (!isset($feedbackcache[$quizid])) {
9cf4a18b 257 $feedbackcache[$quizid] = $DB->get_records('quiz_feedback', array('quizid' => $quizid));
aad5b0fc 258 }
2709ee45 259
b7ab57f3
TH
260 // With CBM etc, it is possible to get -ve grades, which would then not match
261 // any feedback. Therefore, we replace -ve grades with 0.
262 $grade = max($grade, 0);
263
aad5b0fc 264 $feedbacks = $feedbackcache[$quizid];
8ad67658 265 $feedbackid = 0;
aad5b0fc 266 $feedbacktext = '';
8ad67658 267 $feedbacktextformat = FORMAT_MOODLE;
aad5b0fc 268 foreach ($feedbacks as $feedback) {
25a03faa 269 if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade) {
8ad67658 270 $feedbackid = $feedback->id;
aad5b0fc 271 $feedbacktext = $feedback->feedbacktext;
8ad67658 272 $feedbacktextformat = $feedback->feedbacktextformat;
aad5b0fc 273 break;
274 }
275 }
276
277 // Clean the text, ready for display.
0ff4bd08 278 $formatoptions = new stdClass();
aad5b0fc 279 $formatoptions->noclean = true;
25a03faa
TH
280 $feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php',
281 $context->id, 'mod_quiz', 'feedback', $feedbackid);
8ad67658 282 $feedbacktext = format_text($feedbacktext, $feedbacktextformat, $formatoptions);
aad5b0fc 283
284 return $feedbacktext;
285}
0c1c764e 286
2709ee45
TH
287/**
288 * Format a number as a percentage out of $quiz->sumgrades
289 * @param number $rawgrade the mark to format.
290 * @param object $quiz the quiz settings
f7970e3c 291 * @param bool $round whether to round the results ot $quiz->decimalpoints.
2709ee45
TH
292 */
293function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) {
294 if ($quiz->sumgrades == 0) {
869309b8 295 return '';
0c1c764e 296 }
2709ee45
TH
297 if (!is_numeric($rawmark)) {
298 return $rawmark;
299 }
300
301 $mark = $rawmark * 100 / $quiz->sumgrades;
302 if ($round) {
303 $mark = quiz_format_grade($quiz, $mark);
304 }
305 return $mark . '%';
0c1c764e 306}
2709ee45 307
bbf4f440 308/**
309 * Returns an array of reports to which the current user has access to.
2709ee45 310 * @return array reports are ordered as they should be for display in tabs.
bbf4f440 311 */
a49cb927 312function quiz_report_list($context) {
bbf4f440 313 global $DB;
314 static $reportlist = null;
2709ee45 315 if (!is_null($reportlist)) {
bbf4f440 316 return $reportlist;
317 }
2709ee45 318
f2557823 319 $reports = $DB->get_records('quiz_reports', null, 'displayorder DESC', 'name, capability');
a49cb927 320 $reportdirs = get_plugin_list('quiz');
1ddfb914 321
768a7588 322 // Order the reports tab in descending order of displayorder.
bbf4f440 323 $reportcaps = array();
2709ee45
TH
324 foreach ($reports as $key => $report) {
325 if (array_key_exists($report->name, $reportdirs)) {
326 $reportcaps[$report->name] = $report->capability;
bbf4f440 327 }
328 }
329
768a7588 330 // Add any other reports, which are on disc but not in the DB, on the end.
1ddfb914 331 foreach ($reportdirs as $reportname => $notused) {
bbf4f440 332 if (!isset($reportcaps[$reportname])) {
1ddfb914 333 $reportcaps[$reportname] = null;
bbf4f440 334 }
335 }
336 $reportlist = array();
2709ee45
TH
337 foreach ($reportcaps as $name => $capability) {
338 if (empty($capability)) {
bbf4f440 339 $capability = 'mod/quiz:viewreports';
340 }
2709ee45 341 if (has_capability($capability, $context)) {
bbf4f440 342 $reportlist[] = $name;
343 }
344 }
345 return $reportlist;
346}
347
2709ee45
TH
348/**
349 * Create a filename for use when downloading data from a quiz report. It is
350 * expected that this will be passed to flexible_table::is_downloading, which
351 * cleans the filename of bad characters and adds the file extension.
352 * @param string $report the type of report.
353 * @param string $courseshortname the course shortname.
354 * @param string $quizname the quiz name.
355 * @return string the filename.
356 */
357function quiz_report_download_filename($report, $courseshortname, $quizname) {
358 return $courseshortname . '-' . format_string($quizname, true) . '-' . $report;
359}
360
a49cb927
TH
361/**
362 * Get the default report for the current user.
363 * @param object $context the quiz context.
364 */
365function quiz_report_default_report($context) {
6b4e2d76
TH
366 $reports = quiz_report_list($context);
367 return reset($reports);
a49cb927 368}
d755b0f5
TH
369
370/**
371 * Generate a message saying that this quiz has no questions, with a button to
372 * go to the edit page, if the user has the right capability.
373 * @param object $quiz the quiz settings.
374 * @param object $cm the course_module object.
375 * @param object $context the quiz context.
376 * @return string HTML to output.
377 */
378function quiz_no_questions_message($quiz, $cm, $context) {
379 global $OUTPUT;
380
381 $output = '';
382 $output .= $OUTPUT->notification(get_string('noquestions', 'quiz'));
383 if (has_capability('mod/quiz:manage', $context)) {
384 $output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php',
385 array('cmid' => $cm->id)), get_string('editquiz', 'quiz'), 'get');
386 }
387
388 return $output;
389}