PageRenderTime 53ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/moodle/mod/quiz/report/reportlib.php

#
PHP | 396 lines | 239 code | 55 blank | 102 comment | 36 complexity | 9f07d7cd8dbb33ac30d4b4c51dd1aad8 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1, BSD-3-Clause, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, Apache-2.0
  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/>.
  16. /**
  17. * Helper functions for the quiz reports.
  18. *
  19. * @package mod
  20. * @subpackage quiz
  21. * @copyright 2008 Jamie Pratt
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. require_once($CFG->dirroot . '/mod/quiz/lib.php');
  26. require_once($CFG->libdir . '/filelib.php');
  27. define('QUIZ_REPORT_DEFAULT_PAGE_SIZE', 30);
  28. define('QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE', 10);
  29. define('QUIZ_REPORT_ATTEMPTS_ALL', 0);
  30. define('QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO', 1);
  31. define('QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH', 2);
  32. define('QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS', 3);
  33. /**
  34. * Takes an array of objects and constructs a multidimensional array keyed by
  35. * the keys it finds on the object.
  36. * @param array $datum an array of objects with properties on the object
  37. * including the keys passed as the next param.
  38. * @param array $keys Array of strings with the names of the properties on the
  39. * objects in datum that you want to index the multidimensional array by.
  40. * @param bool $keysunique If there is not only one object for each
  41. * combination of keys you are using you should set $keysunique to true.
  42. * Otherwise all the object will be added to a zero based array. So the array
  43. * returned will have count($keys) + 1 indexs.
  44. * @return array multidimensional array properly indexed.
  45. */
  46. function quiz_report_index_by_keys($datum, $keys, $keysunique = true) {
  47. if (!$datum) {
  48. return array();
  49. }
  50. $key = array_shift($keys);
  51. $datumkeyed = array();
  52. foreach ($datum as $data) {
  53. if ($keys || !$keysunique) {
  54. $datumkeyed[$data->{$key}][]= $data;
  55. } else {
  56. $datumkeyed[$data->{$key}]= $data;
  57. }
  58. }
  59. if ($keys) {
  60. foreach ($datumkeyed as $datakey => $datakeyed) {
  61. $datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys, $keysunique);
  62. }
  63. }
  64. return $datumkeyed;
  65. }
  66. function quiz_report_unindex($datum) {
  67. if (!$datum) {
  68. return $datum;
  69. }
  70. $datumunkeyed = array();
  71. foreach ($datum as $value) {
  72. if (is_array($value)) {
  73. $datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value));
  74. } else {
  75. $datumunkeyed[] = $value;
  76. }
  77. }
  78. return $datumunkeyed;
  79. }
  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;
  88. $questionids = quiz_questions_in_quiz($quiz->questions);
  89. if (empty($questionids)) {
  90. return array();
  91. }
  92. list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionids));
  93. $params[] = $quiz->id;
  94. $questions = $DB->get_records_sql("
  95. SELECT
  96. q.id,
  97. q.length,
  98. qqi.grade AS maxmark
  99. FROM {question} q
  100. JOIN {quiz_question_instances} qqi ON qqi.question = q.id
  101. WHERE
  102. q.id $usql AND
  103. qqi.quiz = ? AND
  104. length > 0", $params);
  105. $qsbyslot = array();
  106. $number = 1;
  107. foreach (explode(',', $questionids) as $key => $id) {
  108. if (!array_key_exists($id, $questions)) {
  109. continue;
  110. }
  111. $slot = $key + 1;
  112. $question = $questions[$id];
  113. $question->slot = $slot;
  114. $question->number = $number;
  115. $qsbyslot[$slot] = $question;
  116. $number += $question->length;
  117. }
  118. return $qsbyslot;
  119. }
  120. /**
  121. * Given the quiz grading method return sub select sql to find the id of the
  122. * one attempt that will be graded for each user. Or return
  123. * empty string if all attempts contribute to final grade.
  124. */
  125. function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') {
  126. if ($quiz->attempts == 1) { // Only one attempt allowed on this quiz
  127. return '';
  128. }
  129. switch ($quiz->grademethod) {
  130. case QUIZ_GRADEHIGHEST :
  131. return "$quizattemptsalias.id = (
  132. SELECT MIN(qa2.id)
  133. FROM {quiz_attempts} qa2
  134. WHERE qa2.quiz = $quizattemptsalias.quiz AND
  135. qa2.userid = $quizattemptsalias.userid AND
  136. COALESCE(qa2.sumgrades, 0) = (
  137. SELECT MAX(COALESCE(qa3.sumgrades, 0))
  138. FROM {quiz_attempts} qa3
  139. WHERE qa3.quiz = $quizattemptsalias.quiz AND
  140. qa3.userid = $quizattemptsalias.userid
  141. )
  142. )";
  143. case QUIZ_GRADEAVERAGE :
  144. return '';
  145. case QUIZ_ATTEMPTFIRST :
  146. return "$quizattemptsalias.id = (
  147. SELECT MIN(qa2.id)
  148. FROM {quiz_attempts} qa2
  149. WHERE qa2.quiz = $quizattemptsalias.quiz AND
  150. qa2.userid = $quizattemptsalias.userid)";
  151. case QUIZ_ATTEMPTLAST :
  152. return "$quizattemptsalias.id = (
  153. SELECT MAX(qa2.id)
  154. FROM {quiz_attempts} qa2
  155. WHERE qa2.quiz = $quizattemptsalias.quiz AND
  156. qa2.userid = $quizattemptsalias.userid)";
  157. }
  158. }
  159. /**
  160. * Get the nuber of students whose score was in a particular band for this quiz.
  161. * @param number $bandwidth the width of each band.
  162. * @param int $bands the number of bands
  163. * @param int $quizid the quiz id.
  164. * @param array $userids list of user ids.
  165. * @return array band number => number of users with scores in that band.
  166. */
  167. function quiz_report_grade_bands($bandwidth, $bands, $quizid, $userids = array()) {
  168. global $DB;
  169. if ($userids) {
  170. list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u');
  171. $usql = "qg.userid $usql AND";
  172. } else {
  173. $usql = '';
  174. $params = array();
  175. }
  176. $sql = "
  177. SELECT band, COUNT(1)
  178. FROM (
  179. SELECT FLOOR(qg.grade / :bandwidth) AS band
  180. FROM {quiz_grades} qg
  181. WHERE $usql qg.quiz = :quizid
  182. ) subquery
  183. GROUP BY
  184. band
  185. ORDER BY
  186. band";
  187. $params['quizid'] = $quizid;
  188. $params['bandwidth'] = $bandwidth;
  189. $data = $DB->get_records_sql_menu($sql, $params);
  190. //need to create array elements with values 0 at indexes where there is no element
  191. $data = $data + array_fill(0, $bands+1, 0);
  192. ksort($data);
  193. //place the maximum (prefect grade) into the last band i.e. make last
  194. //band for example 9 <= g <=10 (where 10 is the perfect grade) rather than
  195. //just 9 <= g <10.
  196. $data[$bands - 1] += $data[$bands];
  197. unset($data[$bands]);
  198. return $data;
  199. }
  200. function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter) {
  201. if ($quiz->attempts == 1) {
  202. return '<p>' . get_string('onlyoneattemptallowed', 'quiz_overview') . '</p>';
  203. } else if (!$qmsubselect) {
  204. return '<p>' . get_string('allattemptscontributetograde', 'quiz_overview') . '</p>';
  205. } else if ($qmfilter) {
  206. return '<p>' . get_string('showinggraded', 'quiz_overview') . '</p>';
  207. } else {
  208. return '<p>' . get_string('showinggradedandungraded', 'quiz_overview',
  209. '<span class="gradedattempt">' . quiz_get_grading_option_name($quiz->grademethod) .
  210. '</span>') . '</p>';
  211. }
  212. }
  213. /**
  214. * Get the feedback text for a grade on this quiz. The feedback is
  215. * processed ready for display.
  216. *
  217. * @param float $grade a grade on this quiz.
  218. * @param int $quizid the id of the quiz object.
  219. * @return string the comment that corresponds to this grade (empty string if there is not one.
  220. */
  221. function quiz_report_feedback_for_grade($grade, $quizid, $context) {
  222. global $DB;
  223. static $feedbackcache = array();
  224. if (!isset($feedbackcache[$quizid])) {
  225. $feedbackcache[$quizid] = $DB->get_records('quiz_feedback', array('quizid' => $quizid));
  226. }
  227. // With CBM etc, it is possible to get -ve grades, which would then not match
  228. // any feedback. Therefore, we replace -ve grades with 0.
  229. $grade = max($grade, 0);
  230. $feedbacks = $feedbackcache[$quizid];
  231. $feedbackid = 0;
  232. $feedbacktext = '';
  233. $feedbacktextformat = FORMAT_MOODLE;
  234. foreach ($feedbacks as $feedback) {
  235. if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade) {
  236. $feedbackid = $feedback->id;
  237. $feedbacktext = $feedback->feedbacktext;
  238. $feedbacktextformat = $feedback->feedbacktextformat;
  239. break;
  240. }
  241. }
  242. // Clean the text, ready for display.
  243. $formatoptions = new stdClass();
  244. $formatoptions->noclean = true;
  245. $feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php',
  246. $context->id, 'mod_quiz', 'feedback', $feedbackid);
  247. $feedbacktext = format_text($feedbacktext, $feedbacktextformat, $formatoptions);
  248. return $feedbacktext;
  249. }
  250. /**
  251. * Format a number as a percentage out of $quiz->sumgrades
  252. * @param number $rawgrade the mark to format.
  253. * @param object $quiz the quiz settings
  254. * @param bool $round whether to round the results ot $quiz->decimalpoints.
  255. */
  256. function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) {
  257. if ($quiz->sumgrades == 0) {
  258. return '';
  259. }
  260. if (!is_numeric($rawmark)) {
  261. return $rawmark;
  262. }
  263. $mark = $rawmark * 100 / $quiz->sumgrades;
  264. if ($round) {
  265. $mark = quiz_format_grade($quiz, $mark);
  266. }
  267. return $mark . '%';
  268. }
  269. /**
  270. * Returns an array of reports to which the current user has access to.
  271. * @return array reports are ordered as they should be for display in tabs.
  272. */
  273. function quiz_report_list($context) {
  274. global $DB;
  275. static $reportlist = null;
  276. if (!is_null($reportlist)) {
  277. return $reportlist;
  278. }
  279. $reports = $DB->get_records('quiz_reports', null, 'displayorder DESC', 'name, capability');
  280. $reportdirs = get_plugin_list('quiz');
  281. // Order the reports tab in descending order of displayorder
  282. $reportcaps = array();
  283. foreach ($reports as $key => $report) {
  284. if (array_key_exists($report->name, $reportdirs)) {
  285. $reportcaps[$report->name] = $report->capability;
  286. }
  287. }
  288. // Add any other reports, which are on disc but not in the DB, on the end
  289. foreach ($reportdirs as $reportname => $notused) {
  290. if (!isset($reportcaps[$reportname])) {
  291. $reportcaps[$reportname] = null;
  292. }
  293. }
  294. $reportlist = array();
  295. foreach ($reportcaps as $name => $capability) {
  296. if (empty($capability)) {
  297. $capability = 'mod/quiz:viewreports';
  298. }
  299. if (has_capability($capability, $context)) {
  300. $reportlist[] = $name;
  301. }
  302. }
  303. return $reportlist;
  304. }
  305. /**
  306. * Create a filename for use when downloading data from a quiz report. It is
  307. * expected that this will be passed to flexible_table::is_downloading, which
  308. * cleans the filename of bad characters and adds the file extension.
  309. * @param string $report the type of report.
  310. * @param string $courseshortname the course shortname.
  311. * @param string $quizname the quiz name.
  312. * @return string the filename.
  313. */
  314. function quiz_report_download_filename($report, $courseshortname, $quizname) {
  315. return $courseshortname . '-' . format_string($quizname, true) . '-' . $report;
  316. }
  317. /**
  318. * Get the default report for the current user.
  319. * @param object $context the quiz context.
  320. */
  321. function quiz_report_default_report($context) {
  322. return reset(quiz_report_list($context));
  323. }
  324. /**
  325. * Generate a message saying that this quiz has no questions, with a button to
  326. * go to the edit page, if the user has the right capability.
  327. * @param object $quiz the quiz settings.
  328. * @param object $cm the course_module object.
  329. * @param object $context the quiz context.
  330. * @return string HTML to output.
  331. */
  332. function quiz_no_questions_message($quiz, $cm, $context) {
  333. global $OUTPUT;
  334. $output = '';
  335. $output .= $OUTPUT->notification(get_string('noquestions', 'quiz'));
  336. if (has_capability('mod/quiz:manage', $context)) {
  337. $output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php',
  338. array('cmid' => $cm->id)), get_string('editquiz', 'quiz'), 'get');
  339. }
  340. return $output;
  341. }