MDL-34054 quiz reports: missing context.
[moodle.git] / mod / quiz / report / reportlib.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  * Helper functions for the quiz reports.
19  *
20  * @package   mod_quiz
21  * @copyright 2008 Jamie Pratt
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/lib.php');
29 require_once($CFG->libdir . '/filelib.php');
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.
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.
43  */
44 function quiz_report_index_by_keys($datum, $keys, $keysunique = true) {
45     if (!$datum) {
46         return array();
47     }
48     $key = array_shift($keys);
49     $datumkeyed = array();
50     foreach ($datum as $data) {
51         if ($keys || !$keysunique) {
52             $datumkeyed[$data->{$key}][]= $data;
53         } else {
54             $datumkeyed[$data->{$key}]= $data;
55         }
56     }
57     if ($keys) {
58         foreach ($datumkeyed as $datakey => $datakeyed) {
59             $datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys, $keysunique);
60         }
61     }
62     return $datumkeyed;
63 }
65 function quiz_report_unindex($datum) {
66     if (!$datum) {
67         return $datum;
68     }
69     $datumunkeyed = array();
70     foreach ($datum as $value) {
71         if (is_array($value)) {
72             $datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value));
73         } else {
74             $datumunkeyed[] = $value;
75         }
76     }
77     return $datumunkeyed;
78 }
80 /**
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.
85  */
86 function quiz_report_get_significant_questions($quiz) {
87     global $DB;
89     $questionids = quiz_questions_in_quiz($quiz->questions);
90     if (empty($questionids)) {
91         return array();
92     }
94     list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionids));
95     $params[] = $quiz->id;
96     $questions = $DB->get_records_sql("
97 SELECT
98     q.id,
99     q.length,
100     qqi.grade AS maxmark
102 FROM {question} q
103 JOIN {quiz_question_instances} qqi ON qqi.question = q.id
105 WHERE
106     q.id $usql AND
107     qqi.quiz = ? AND
108     length > 0", $params);
110     $qsbyslot = array();
111     $number = 1;
112     foreach (explode(',', $questionids) as $key => $id) {
113         if (!array_key_exists($id, $questions)) {
114             continue;
115         }
117         $slot = $key + 1;
118         $question = $questions[$id];
119         $question->slot = $slot;
120         $question->number = $number;
122         $qsbyslot[$slot] = $question;
124         $number += $question->length;
125     }
127     return $qsbyslot;
130 /**
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.
134  */
135 function quiz_report_can_filter_only_graded($quiz) {
136     return $quiz->attempts != 1 && $quiz->grademethod != QUIZ_GRADEAVERAGE;
139 /**
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.
143  */
144 function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') {
145     if ($quiz->attempts == 1) {
146         // This quiz only allows one attempt.
147         return '';
148     }
150     switch ($quiz->grademethod) {
151         case QUIZ_GRADEHIGHEST :
152             return "$quizattemptsalias.id = (
153                     SELECT MIN(qa2.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
162                         )
163                     )";
165         case QUIZ_GRADEAVERAGE :
166             return '';
168         case QUIZ_ATTEMPTFIRST :
169             return "$quizattemptsalias.id = (
170                     SELECT MIN(qa2.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 = (
177                     SELECT MAX(qa2.id)
178                     FROM {quiz_attempts} qa2
179                     WHERE qa2.quiz = $quizattemptsalias.quiz AND
180                         qa2.userid = $quizattemptsalias.userid)";
181     }
184 /**
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.
191  */
192 function quiz_report_grade_bands($bandwidth, $bands, $quizid, $userids = array()) {
193     global $DB;
195     if ($userids) {
196         list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u');
197         $usql = "qg.userid $usql AND";
198     } else {
199         $usql = '';
200         $params = array();
201     }
202     $sql = "
203 SELECT band, COUNT(1)
205 FROM (
206     SELECT FLOOR(qg.grade / :bandwidth) AS band
207       FROM {quiz_grades} qg
208      WHERE $usql qg.quiz = :quizid
209 ) subquery
211 GROUP BY
212     band
214 ORDER BY
215     band";
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);
224     ksort($data);
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
228     // just 9 <= g <10.
229     $data[$bands - 1] += $data[$bands];
230     unset($data[$bands]);
232     return $data;
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>';
245     } else {
246         return '<p>' . get_string('showinggradedandungraded', 'quiz_overview',
247                 '<span class="gradedattempt">' . quiz_get_grading_option_name($quiz->grademethod) .
248                 '</span>') . '</p>';
249     }
252 /**
253  * Get the feedback text for a grade on this quiz. The feedback is
254  * processed ready for display.
255  *
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.
259  */
260 function quiz_report_feedback_for_grade($grade, $quizid, $context) {
261     global $DB;
263     static $feedbackcache = array();
265     if (!isset($feedbackcache[$quizid])) {
266         $feedbackcache[$quizid] = $DB->get_records('quiz_feedback', array('quizid' => $quizid));
267     }
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];
274     $feedbackid = 0;
275     $feedbacktext = '';
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;
282             break;
283         }
284     }
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;
296 /**
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.
301  */
302 function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) {
303     if ($quiz->sumgrades == 0) {
304         return '';
305     }
306     if (!is_numeric($rawmark)) {
307         return $rawmark;
308     }
310     $mark = $rawmark * 100 / $quiz->sumgrades;
311     if ($round) {
312         $mark = quiz_format_grade($quiz, $mark);
313     }
314     return $mark . '%';
317 /**
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.
320  */
321 function quiz_report_list($context) {
322     global $DB;
323     static $reportlist = null;
324     if (!is_null($reportlist)) {
325         return $reportlist;
326     }
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;
336         }
337     }
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;
343         }
344     }
345     $reportlist = array();
346     foreach ($reportcaps as $name => $capability) {
347         if (empty($capability)) {
348             $capability = 'mod/quiz:viewreports';
349         }
350         if (has_capability($capability, $context)) {
351             $reportlist[] = $name;
352         }
353     }
354     return $reportlist;
357 /**
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.
365  */
366 function quiz_report_download_filename($report, $courseshortname, $quizname) {
367     return $courseshortname . '-' . format_string($quizname, true) . '-' . $report;
370 /**
371  * Get the default report for the current user.
372  * @param object $context the quiz context.
373  */
374 function quiz_report_default_report($context) {
375     $reports = quiz_report_list($context);
376     return reset($reports);
379 /**
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.
386  */
387 function quiz_no_questions_message($quiz, $cm, $context) {
388     global $OUTPUT;
390     $output = '';
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');
395     }
397     return $output;
400 /**
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.
405  * @return bool
406  */
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;
410     } else {
411         $when = mod_quiz_display_options::LATER_WHILE_OPEN;
412     }
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));