See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
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 /** 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 */ 24 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->dirroot . '/mod/quiz/lib.php'); 29 require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); 30 require_once($CFG->libdir . '/filelib.php'); 31 require_once($CFG->dirroot . '/mod/quiz/accessmanager.php'); 32 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 67 function quiz_report_unindex($datum) { 68 if (!$datum) { 69 return $datum; 70 } 71 $datumunkeyed = array(); 72 foreach ($datum as $value) { 73 if (is_array($value)) { 74 $datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value)); 75 } else { 76 $datumunkeyed[] = $value; 77 } 78 } 79 return $datumunkeyed; 80 } 81 82 /** 83 * Are there any questions in this quiz? 84 * @param int $quizid the quiz id. 85 */ 86 function quiz_has_questions($quizid) { 87 global $DB; 88 return $DB->record_exists('quiz_slots', array('quizid' => $quizid)); 89 } 90 91 /** 92 * Get the slots of real questions (not descriptions) in this quiz, in order. 93 * @param object $quiz the quiz. 94 * @return array of slot => objects with fields 95 * ->slot, ->id, ->qtype, ->length, ->number, ->maxmark, ->category (for random questions). 96 */ 97 function quiz_report_get_significant_questions($quiz) { 98 $quizobj = \quiz::create($quiz->id); 99 $structure = \mod_quiz\structure::create_for_quiz($quizobj); 100 $slots = $structure->get_slots(); 101 102 $qsbyslot = []; 103 $number = 1; 104 foreach ($slots as $slot) { 105 // Ignore 'questions' of zero length. 106 if ($slot->length == 0) { 107 continue; 108 } 109 110 $slotreport = new \stdClass(); 111 $slotreport->slot = $slot->slot; 112 $slotreport->id = $slot->questionid; 113 $slotreport->qtype = $slot->qtype; 114 $slotreport->length = $slot->length; 115 $slotreport->number = $number; 116 $number += $slot->length; 117 $slotreport->maxmark = $slot->maxmark; 118 $slotreport->category = $slot->category; 119 120 $qsbyslot[$slotreport->slot] = $slotreport; 121 } 122 123 return $qsbyslot; 124 } 125 126 /** 127 * @param object $quiz the quiz settings. 128 * @return bool whether, for this quiz, it is possible to filter attempts to show 129 * only those that gave the final grade. 130 */ 131 function quiz_report_can_filter_only_graded($quiz) { 132 return $quiz->attempts != 1 && $quiz->grademethod != QUIZ_GRADEAVERAGE; 133 } 134 135 /** 136 * This is a wrapper for {@link quiz_report_grade_method_sql} that takes the whole quiz object instead of just the grading method 137 * as a param. See definition for {@link quiz_report_grade_method_sql} below. 138 * 139 * @param object $quiz 140 * @param string $quizattemptsalias sql alias for 'quiz_attempts' table 141 * @return string sql to test if this is an attempt that will contribute towards the grade of the user 142 */ 143 function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') { 144 if ($quiz->attempts == 1) { 145 // This quiz only allows one attempt. 146 return ''; 147 } 148 return quiz_report_grade_method_sql($quiz->grademethod, $quizattemptsalias); 149 } 150 151 /** 152 * Given a quiz grading method return sql to test if this is an 153 * attempt that will be contribute towards the grade of the user. Or return an 154 * empty string if the grading method is QUIZ_GRADEAVERAGE and thus all attempts 155 * contribute to final grade. 156 * 157 * @param string $grademethod quiz grading method. 158 * @param string $quizattemptsalias sql alias for 'quiz_attempts' table 159 * @return string sql to test if this is an attempt that will contribute towards the graded of the user 160 */ 161 function quiz_report_grade_method_sql($grademethod, $quizattemptsalias = 'quiza') { 162 switch ($grademethod) { 163 case QUIZ_GRADEHIGHEST : 164 return "($quizattemptsalias.state = 'finished' AND NOT EXISTS ( 165 SELECT 1 FROM {quiz_attempts} qa2 166 WHERE qa2.quiz = $quizattemptsalias.quiz AND 167 qa2.userid = $quizattemptsalias.userid AND 168 qa2.state = 'finished' AND ( 169 COALESCE(qa2.sumgrades, 0) > COALESCE($quizattemptsalias.sumgrades, 0) OR 170 (COALESCE(qa2.sumgrades, 0) = COALESCE($quizattemptsalias.sumgrades, 0) AND qa2.attempt < $quizattemptsalias.attempt) 171 )))"; 172 173 case QUIZ_GRADEAVERAGE : 174 return ''; 175 176 case QUIZ_ATTEMPTFIRST : 177 return "($quizattemptsalias.state = 'finished' AND NOT EXISTS ( 178 SELECT 1 FROM {quiz_attempts} qa2 179 WHERE qa2.quiz = $quizattemptsalias.quiz AND 180 qa2.userid = $quizattemptsalias.userid AND 181 qa2.state = 'finished' AND 182 qa2.attempt < $quizattemptsalias.attempt))"; 183 184 case QUIZ_ATTEMPTLAST : 185 return "($quizattemptsalias.state = 'finished' AND NOT EXISTS ( 186 SELECT 1 FROM {quiz_attempts} qa2 187 WHERE qa2.quiz = $quizattemptsalias.quiz AND 188 qa2.userid = $quizattemptsalias.userid AND 189 qa2.state = 'finished' AND 190 qa2.attempt > $quizattemptsalias.attempt))"; 191 } 192 } 193 194 /** 195 * Get the number of students whose score was in a particular band for this quiz. 196 * @param number $bandwidth the width of each band. 197 * @param int $bands the number of bands 198 * @param int $quizid the quiz id. 199 * @param \core\dml\sql_join $usersjoins (joins, wheres, params) to get enrolled users 200 * @return array band number => number of users with scores in that band. 201 */ 202 function quiz_report_grade_bands($bandwidth, $bands, $quizid, \core\dml\sql_join $usersjoins = null) { 203 global $DB; 204 if (!is_int($bands)) { 205 debugging('$bands passed to quiz_report_grade_bands must be an integer. (' . 206 gettype($bands) . ' passed.)', DEBUG_DEVELOPER); 207 $bands = (int) $bands; 208 } 209 210 if ($usersjoins && !empty($usersjoins->joins)) { 211 $userjoin = "JOIN {user} u ON u.id = qg.userid 212 {$usersjoins->joins}"; 213 $usertest = $usersjoins->wheres; 214 $params = $usersjoins->params; 215 } else { 216 $userjoin = ''; 217 $usertest = '1=1'; 218 $params = array(); 219 } 220 $sql = " 221 SELECT band, COUNT(1) 222 223 FROM ( 224 SELECT FLOOR(qg.grade / :bandwidth) AS band 225 FROM {quiz_grades} qg 226 $userjoin 227 WHERE $usertest AND qg.quiz = :quizid 228 ) subquery 229 230 GROUP BY 231 band 232 233 ORDER BY 234 band"; 235 236 $params['quizid'] = $quizid; 237 $params['bandwidth'] = $bandwidth; 238 239 $data = $DB->get_records_sql_menu($sql, $params); 240 241 // We need to create array elements with values 0 at indexes where there is no element. 242 $data = $data + array_fill(0, $bands + 1, 0); 243 ksort($data); 244 245 // Place the maximum (perfect grade) into the last band i.e. make last 246 // band for example 9 <= g <=10 (where 10 is the perfect grade) rather than 247 // just 9 <= g <10. 248 $data[$bands - 1] += $data[$bands]; 249 unset($data[$bands]); 250 251 // See MDL-60632. When a quiz participant achieves an overall negative grade the chart fails to render. 252 foreach ($data as $databand => $datanum) { 253 if ($databand < 0) { 254 $data["0"] += $datanum; // Add to band 0. 255 unset($data[$databand]); // Remove entry below 0. 256 } 257 } 258 259 return $data; 260 } 261 262 function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter) { 263 if ($quiz->attempts == 1) { 264 return '<p>' . get_string('onlyoneattemptallowed', 'quiz_overview') . '</p>'; 265 266 } else if (!$qmsubselect) { 267 return '<p>' . get_string('allattemptscontributetograde', 'quiz_overview') . '</p>'; 268 269 } else if ($qmfilter) { 270 return '<p>' . get_string('showinggraded', 'quiz_overview') . '</p>'; 271 272 } else { 273 return '<p>' . get_string('showinggradedandungraded', 'quiz_overview', 274 '<span class="gradedattempt">' . quiz_get_grading_option_name($quiz->grademethod) . 275 '</span>') . '</p>'; 276 } 277 } 278 279 /** 280 * Get the feedback text for a grade on this quiz. The feedback is 281 * processed ready for display. 282 * 283 * @param float $grade a grade on this quiz. 284 * @param int $quizid the id of the quiz object. 285 * @return string the comment that corresponds to this grade (empty string if there is not one. 286 */ 287 function quiz_report_feedback_for_grade($grade, $quizid, $context) { 288 global $DB; 289 290 static $feedbackcache = array(); 291 292 if (!isset($feedbackcache[$quizid])) { 293 $feedbackcache[$quizid] = $DB->get_records('quiz_feedback', array('quizid' => $quizid)); 294 } 295 296 // With CBM etc, it is possible to get -ve grades, which would then not match 297 // any feedback. Therefore, we replace -ve grades with 0. 298 $grade = max($grade, 0); 299 300 $feedbacks = $feedbackcache[$quizid]; 301 $feedbackid = 0; 302 $feedbacktext = ''; 303 $feedbacktextformat = FORMAT_MOODLE; 304 foreach ($feedbacks as $feedback) { 305 if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade) { 306 $feedbackid = $feedback->id; 307 $feedbacktext = $feedback->feedbacktext; 308 $feedbacktextformat = $feedback->feedbacktextformat; 309 break; 310 } 311 } 312 313 // Clean the text, ready for display. 314 $formatoptions = new stdClass(); 315 $formatoptions->noclean = true; 316 $feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php', 317 $context->id, 'mod_quiz', 'feedback', $feedbackid); 318 $feedbacktext = format_text($feedbacktext, $feedbacktextformat, $formatoptions); 319 320 return $feedbacktext; 321 } 322 323 /** 324 * Format a number as a percentage out of $quiz->sumgrades 325 * @param number $rawgrade the mark to format. 326 * @param object $quiz the quiz settings 327 * @param bool $round whether to round the results ot $quiz->decimalpoints. 328 */ 329 function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) { 330 if ($quiz->sumgrades == 0) { 331 return ''; 332 } 333 if (!is_numeric($rawmark)) { 334 return $rawmark; 335 } 336 337 $mark = $rawmark * 100 / $quiz->sumgrades; 338 if ($round) { 339 $mark = quiz_format_grade($quiz, $mark); 340 } 341 342 return get_string('percents', 'moodle', $mark); 343 } 344 345 /** 346 * Returns an array of reports to which the current user has access to. 347 * @return array reports are ordered as they should be for display in tabs. 348 */ 349 function quiz_report_list($context) { 350 global $DB; 351 static $reportlist = null; 352 if (!is_null($reportlist)) { 353 return $reportlist; 354 } 355 356 $reports = $DB->get_records('quiz_reports', null, 'displayorder DESC', 'name, capability'); 357 $reportdirs = core_component::get_plugin_list('quiz'); 358 359 // Order the reports tab in descending order of displayorder. 360 $reportcaps = array(); 361 foreach ($reports as $key => $report) { 362 if (array_key_exists($report->name, $reportdirs)) { 363 $reportcaps[$report->name] = $report->capability; 364 } 365 } 366 367 // Add any other reports, which are on disc but not in the DB, on the end. 368 foreach ($reportdirs as $reportname => $notused) { 369 if (!isset($reportcaps[$reportname])) { 370 $reportcaps[$reportname] = null; 371 } 372 } 373 $reportlist = array(); 374 foreach ($reportcaps as $name => $capability) { 375 if (empty($capability)) { 376 $capability = 'mod/quiz:viewreports'; 377 } 378 if (has_capability($capability, $context)) { 379 $reportlist[] = $name; 380 } 381 } 382 return $reportlist; 383 } 384 385 /** 386 * Create a filename for use when downloading data from a quiz report. It is 387 * expected that this will be passed to flexible_table::is_downloading, which 388 * cleans the filename of bad characters and adds the file extension. 389 * @param string $report the type of report. 390 * @param string $courseshortname the course shortname. 391 * @param string $quizname the quiz name. 392 * @return string the filename. 393 */ 394 function quiz_report_download_filename($report, $courseshortname, $quizname) { 395 return $courseshortname . '-' . format_string($quizname, true) . '-' . $report; 396 } 397 398 /** 399 * Get the default report for the current user. 400 * @param object $context the quiz context. 401 */ 402 function quiz_report_default_report($context) { 403 $reports = quiz_report_list($context); 404 return reset($reports); 405 } 406 407 /** 408 * Generate a message saying that this quiz has no questions, with a button to 409 * go to the edit page, if the user has the right capability. 410 * @param object $quiz the quiz settings. 411 * @param object $cm the course_module object. 412 * @param object $context the quiz context. 413 * @return string HTML to output. 414 */ 415 function quiz_no_questions_message($quiz, $cm, $context) { 416 global $OUTPUT; 417 418 $output = ''; 419 $output .= $OUTPUT->notification(get_string('noquestions', 'quiz')); 420 if (has_capability('mod/quiz:manage', $context)) { 421 $output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php', 422 array('cmid' => $cm->id)), get_string('editquiz', 'quiz'), 'get'); 423 } 424 425 return $output; 426 } 427 428 /** 429 * Should the grades be displayed in this report. That depends on the quiz 430 * display options, and whether the quiz is graded. 431 * @param object $quiz the quiz settings. 432 * @param context $context the quiz context. 433 * @return bool 434 */ 435 function quiz_report_should_show_grades($quiz, context $context) { 436 if ($quiz->timeclose && time() > $quiz->timeclose) { 437 $when = mod_quiz_display_options::AFTER_CLOSE; 438 } else { 439 $when = mod_quiz_display_options::LATER_WHILE_OPEN; 440 } 441 $reviewoptions = mod_quiz_display_options::make_from_quiz($quiz, $when); 442 443 return quiz_has_grades($quiz) && 444 ($reviewoptions->marks >= question_display_options::MARK_AND_MAX || 445 has_capability('moodle/grade:viewhidden', $context)); 446 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body