53fa2b6763dad144dd10b5a17be8bf1f50a12932
[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
21  * @subpackage quiz
22  * @copyright  2008 Jamie Pratt
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/mod/quiz/lib.php');
30 require_once($CFG->libdir . '/filelib.php');
32 define('QUIZ_REPORT_DEFAULT_PAGE_SIZE', 30);
33 define('QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE', 10);
35 define('QUIZ_REPORT_ATTEMPTS_ALL', 0);
36 define('QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO', 1);
37 define('QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH', 2);
38 define('QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS', 3);
40 /**
41  * Takes an array of objects and constructs a multidimensional array keyed by
42  * the keys it finds on the object.
43  * @param array $datum an array of objects with properties on the object
44  * including the keys passed as the next param.
45  * @param array $keys Array of strings with the names of the properties on the
46  * objects in datum that you want to index the multidimensional array by.
47  * @param bool $keysunique If there is not only one object for each
48  * combination of keys you are using you should set $keysunique to true.
49  * Otherwise all the object will be added to a zero based array. So the array
50  * returned will have count($keys) + 1 indexs.
51  * @return array multidimensional array properly indexed.
52  */
53 function quiz_report_index_by_keys($datum, $keys, $keysunique = true) {
54     if (!$datum) {
55         return array();
56     }
57     $key = array_shift($keys);
58     $datumkeyed = array();
59     foreach ($datum as $data) {
60         if ($keys || !$keysunique) {
61             $datumkeyed[$data->{$key}][]= $data;
62         } else {
63             $datumkeyed[$data->{$key}]= $data;
64         }
65     }
66     if ($keys) {
67         foreach ($datumkeyed as $datakey => $datakeyed) {
68             $datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys, $keysunique);
69         }
70     }
71     return $datumkeyed;
72 }
74 function quiz_report_unindex($datum) {
75     if (!$datum) {
76         return $datum;
77     }
78     $datumunkeyed = array();
79     foreach ($datum as $value) {
80         if (is_array($value)) {
81             $datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value));
82         } else {
83             $datumunkeyed[] = $value;
84         }
85     }
86     return $datumunkeyed;
87 }
89 /**
90  * Get the slots of real questions (not descriptions) in this quiz, in order.
91  * @param object $quiz the quiz.
92  * @return array of slot => $question object with fields
93  *      ->slot, ->id, ->maxmark, ->number, ->length.
94  */
95 function quiz_report_get_significant_questions($quiz) {
96     global $DB;
98     $questionids = quiz_questions_in_quiz($quiz->questions);
99     if (empty($questionids)) {
100         return array();
101     }
103     list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionids));
104     $params[] = $quiz->id;
105     $questions = $DB->get_records_sql("
106 SELECT
107     q.id,
108     q.length,
109     qqi.grade AS maxmark
111 FROM {question} q
112 JOIN {quiz_question_instances} qqi ON qqi.question = q.id
114 WHERE
115     q.id $usql AND
116     qqi.quiz = ? AND
117     length > 0", $params);
119     $qsbyslot = array();
120     $number = 1;
121     foreach (explode(',', $questionids) as $key => $id) {
122         if (!array_key_exists($id, $questions)) {
123             continue;
124         }
126         $slot = $key + 1;
127         $question = $questions[$id];
128         $question->slot = $slot;
129         $question->number = $number;
131         $qsbyslot[$slot] = $question;
133         $number += $question->length;
134     }
136     return $qsbyslot;
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) { // Only one attempt allowed on this quiz
146         return '';
147     }
149     switch ($quiz->grademethod) {
150         case QUIZ_GRADEHIGHEST :
151             return "$quizattemptsalias.id = (
152                     SELECT MIN(qa2.id)
153                     FROM {quiz_attempts} qa2
154                     WHERE qa2.quiz = $quizattemptsalias.quiz AND
155                         qa2.userid = $quizattemptsalias.userid AND
156                         COALESCE(qa2.sumgrades, 0) = (
157                             SELECT MAX(COALESCE(qa3.sumgrades, 0))
158                             FROM {quiz_attempts} qa3
159                             WHERE qa3.quiz = $quizattemptsalias.quiz AND
160                                 qa3.userid = $quizattemptsalias.userid
161                         )
162                     )";
164         case QUIZ_GRADEAVERAGE :
165             return '';
167         case QUIZ_ATTEMPTFIRST :
168             return "$quizattemptsalias.id = (
169                     SELECT MIN(qa2.id)
170                     FROM {quiz_attempts} qa2
171                     WHERE qa2.quiz = $quizattemptsalias.quiz AND
172                         qa2.userid = $quizattemptsalias.userid)";
174         case QUIZ_ATTEMPTLAST :
175             return "$quizattemptsalias.id = (
176                     SELECT MAX(qa2.id)
177                     FROM {quiz_attempts} qa2
178                     WHERE qa2.quiz = $quizattemptsalias.quiz AND
179                         qa2.userid = $quizattemptsalias.userid)";
180     }
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.
186  * @param int $bands the number of bands
187  * @param int $quizid the quiz id.
188  * @param array $userids list of user ids.
189  * @return array band number => number of users with scores in that band.
190  */
191 function quiz_report_grade_bands($bandwidth, $bands, $quizid, $userids = array()) {
192     global $DB;
194     if ($userids) {
195         list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u');
196         $usql = "qg.userid $usql AND";
197     } else {
198         $usql = '';
199         $params = array();
200     }
201     $sql = "
202 SELECT band, COUNT(1)
204 FROM (
205     SELECT FLOOR(qg.grade / :bandwidth) AS band
206       FROM {quiz_grades} qg
207      WHERE $usql qg.quiz = :quizid
208 ) subquery
210 GROUP BY
211     band
213 ORDER BY
214     band";
216     $params['quizid'] = $quizid;
217     $params['bandwidth'] = $bandwidth;
219     $data = $DB->get_records_sql_menu($sql, $params);
221     //need to create array elements with values 0 at indexes where there is no element
222     $data =  $data + array_fill(0, $bands+1, 0);
223     ksort($data);
225     //place the maximum (prefect grade) into the last band i.e. make last
226     //band for example 9 <= g <=10 (where 10 is the perfect grade) rather than
227     //just 9 <= g <10.
228     $data[$bands - 1] += $data[$bands];
229     unset($data[$bands]);
231     return $data;
234 function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter) {
235     if ($quiz->attempts == 1) {
236         return '<p>' . get_string('onlyoneattemptallowed', 'quiz_overview') . '</p>';
238     } else if (!$qmsubselect) {
239         return '<p>' . get_string('allattemptscontributetograde', 'quiz_overview') . '</p>';
241     } else if ($qmfilter) {
242         return '<p>' . get_string('showinggraded', 'quiz_overview') . '</p>';
244     } else {
245         return '<p>' . get_string('showinggradedandungraded', 'quiz_overview',
246                 '<span class="gradedattempt">' . quiz_get_grading_option_name($quiz->grademethod) .
247                 '</span>') . '</p>';
248     }
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.
256  * @param int $quizid the id of the quiz object.
257  * @return string the comment that corresponds to this grade (empty string if there is not one.
258  */
259 function quiz_report_feedback_for_grade($grade, $quizid, $context) {
260     global $DB;
262     static $feedbackcache = array();
264     if (!isset($feedbackcache[$quizid])) {
265         $feedbackcache[$quizid] = $DB->get_records('quiz_feedback', array('quizid' => $quizid));
266     }
268     $feedbacks = $feedbackcache[$quizid];
269     $feedbackid = 0;
270     $feedbacktext = '';
271     $feedbacktextformat = FORMAT_MOODLE;
272     foreach ($feedbacks as $feedback) {
273         if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade) {
274             $feedbackid = $feedback->id;
275             $feedbacktext = $feedback->feedbacktext;
276             $feedbacktextformat = $feedback->feedbacktextformat;
277             break;
278         }
279     }
281     // Clean the text, ready for display.
282     $formatoptions = new stdClass();
283     $formatoptions->noclean = true;
284     $feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php',
285             $context->id, 'mod_quiz', 'feedback', $feedbackid);
286     $feedbacktext = format_text($feedbacktext, $feedbacktextformat, $formatoptions);
288     return $feedbacktext;
291 /**
292  * Format a number as a percentage out of $quiz->sumgrades
293  * @param number $rawgrade the mark to format.
294  * @param object $quiz the quiz settings
295  * @param bool $round whether to round the results ot $quiz->decimalpoints.
296  */
297 function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) {
298     if ($quiz->sumgrades == 0) {
299         return '';
300     }
301     if (!is_numeric($rawmark)) {
302         return $rawmark;
303     }
305     $mark = $rawmark * 100 / $quiz->sumgrades;
306     if ($round) {
307         $mark = quiz_format_grade($quiz, $mark);
308     }
309     return $mark . '%';
312 /**
313  * Returns an array of reports to which the current user has access to.
314  * @return array reports are ordered as they should be for display in tabs.
315  */
316 function quiz_report_list($context) {
317     global $DB;
318     static $reportlist = null;
319     if (!is_null($reportlist)) {
320         return $reportlist;
321     }
323     $reports = $DB->get_records('quiz_reports', null, 'displayorder DESC', 'name, capability');
324     $reportdirs = get_plugin_list('quiz');
326     // Order the reports tab in descending order of displayorder
327     $reportcaps = array();
328     foreach ($reports as $key => $report) {
329         if (array_key_exists($report->name, $reportdirs)) {
330             $reportcaps[$report->name] = $report->capability;
331         }
332     }
334     // Add any other reports, which are on disc but not in the DB, on the end
335     foreach ($reportdirs as $reportname => $notused) {
336         if (!isset($reportcaps[$reportname])) {
337             $reportcaps[$reportname] = null;
338         }
339     }
340     $reportlist = array();
341     foreach ($reportcaps as $name => $capability) {
342         if (empty($capability)) {
343             $capability = 'mod/quiz:viewreports';
344         }
345         if (has_capability($capability, $context)) {
346             $reportlist[] = $name;
347         }
348     }
349     return $reportlist;
352 /**
353  * Create a filename for use when downloading data from a quiz report. It is
354  * expected that this will be passed to flexible_table::is_downloading, which
355  * cleans the filename of bad characters and adds the file extension.
356  * @param string $report the type of report.
357  * @param string $courseshortname the course shortname.
358  * @param string $quizname the quiz name.
359  * @return string the filename.
360  */
361 function quiz_report_download_filename($report, $courseshortname, $quizname) {
362     return $courseshortname . '-' . format_string($quizname, true) . '-' . $report;
365 /**
366  * Get the default report for the current user.
367  * @param object $context the quiz context.
368  */
369 function quiz_report_default_report($context) {
370     return reset(quiz_report_list($context));
373 /**
374  * Generate a message saying that this quiz has no questions, with a button to
375  * go to the edit page, if the user has the right capability.
376  * @param object $quiz the quiz settings.
377  * @param object $cm the course_module object.
378  * @param object $context the quiz context.
379  * @return string HTML to output.
380  */
381 function quiz_no_questions_message($quiz, $cm, $context) {
382     global $OUTPUT;
384     $output = '';
385     $output .= $OUTPUT->notification(get_string('noquestions', 'quiz'));
386     if (has_capability('mod/quiz:manage', $context)) {
387         $output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php',
388         array('cmid' => $cm->id)), get_string('editquiz', 'quiz'), 'get');
389     }
391     return $output;