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