|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00033 defined('MOODLE_INTERNAL') || die(); 00034 00035 require_once($CFG->dirroot . '/mod/quiz/lib.php'); 00036 require_once($CFG->dirroot . '/mod/quiz/accessmanager.php'); 00037 require_once($CFG->dirroot . '/mod/quiz/accessmanager_form.php'); 00038 require_once($CFG->dirroot . '/mod/quiz/renderer.php'); 00039 require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); 00040 require_once($CFG->dirroot . '/question/editlib.php'); 00041 require_once($CFG->libdir . '/eventslib.php'); 00042 require_once($CFG->libdir . '/filelib.php'); 00043 00044 00049 define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600'); 00050 00052 00069 function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $ispreview = false) { 00070 global $USER; 00071 00072 if ($quiz->sumgrades < 0.000005 && $quiz->grade > 0.000005) { 00073 throw new moodle_exception('cannotstartgradesmismatch', 'quiz', 00074 new moodle_url('/mod/quiz/view.php', array('q' => $quiz->id))); 00075 } 00076 00077 if ($attemptnumber == 1 || !$quiz->attemptonlast) { 00078 // We are not building on last attempt so create a new attempt. 00079 $attempt = new stdClass(); 00080 $attempt->quiz = $quiz->id; 00081 $attempt->userid = $USER->id; 00082 $attempt->preview = 0; 00083 $attempt->layout = quiz_clean_layout($quiz->questions, true); 00084 if ($quiz->shufflequestions) { 00085 $attempt->layout = quiz_repaginate($attempt->layout, $quiz->questionsperpage, true); 00086 } 00087 } else { 00088 // Build on last attempt. 00089 if (empty($lastattempt)) { 00090 print_error('cannotfindprevattempt', 'quiz'); 00091 } 00092 $attempt = $lastattempt; 00093 } 00094 00095 $attempt->attempt = $attemptnumber; 00096 $attempt->timestart = $timenow; 00097 $attempt->timefinish = 0; 00098 $attempt->timemodified = $timenow; 00099 00100 // If this is a preview, mark it as such. 00101 if ($ispreview) { 00102 $attempt->preview = 1; 00103 } 00104 00105 return $attempt; 00106 } 00107 00117 function quiz_get_user_attempt_unfinished($quizid, $userid) { 00118 $attempts = quiz_get_user_attempts($quizid, $userid, 'unfinished', true); 00119 if ($attempts) { 00120 return array_shift($attempts); 00121 } else { 00122 return false; 00123 } 00124 } 00125 00132 function quiz_delete_attempt($attempt, $quiz) { 00133 global $DB; 00134 if (is_numeric($attempt)) { 00135 if (!$attempt = $DB->get_record('quiz_attempts', array('id' => $attempt))) { 00136 return; 00137 } 00138 } 00139 00140 if ($attempt->quiz != $quiz->id) { 00141 debugging("Trying to delete attempt $attempt->id which belongs to quiz $attempt->quiz " . 00142 "but was passed quiz $quiz->id."); 00143 return; 00144 } 00145 00146 question_engine::delete_questions_usage_by_activity($attempt->uniqueid); 00147 $DB->delete_records('quiz_attempts', array('id' => $attempt->id)); 00148 00149 // Search quiz_attempts for other instances by this user. 00150 // If none, then delete record for this quiz, this user from quiz_grades 00151 // else recalculate best grade 00152 $userid = $attempt->userid; 00153 if (!$DB->record_exists('quiz_attempts', array('userid' => $userid, 'quiz' => $quiz->id))) { 00154 $DB->delete_records('quiz_grades', array('userid' => $userid, 'quiz' => $quiz->id)); 00155 } else { 00156 quiz_save_best_grade($quiz, $userid); 00157 } 00158 00159 quiz_update_grades($quiz, $userid); 00160 } 00161 00168 function quiz_delete_previews($quiz, $userid = null) { 00169 global $DB; 00170 $conditions = array('quiz' => $quiz->id, 'preview' => 1); 00171 if (!empty($userid)) { 00172 $conditions['userid'] = $userid; 00173 } 00174 $previewattempts = $DB->get_records('quiz_attempts', $conditions); 00175 foreach ($previewattempts as $attempt) { 00176 quiz_delete_attempt($attempt, $quiz); 00177 } 00178 } 00179 00184 function quiz_has_attempts($quizid) { 00185 global $DB; 00186 return $DB->record_exists('quiz_attempts', array('quiz' => $quizid, 'preview' => 0)); 00187 } 00188 00190 00200 function quiz_questions_in_quiz($layout) { 00201 $questions = str_replace(',0', '', quiz_clean_layout($layout, true)); 00202 if ($questions === '0') { 00203 return ''; 00204 } else { 00205 return $questions; 00206 } 00207 } 00208 00215 function quiz_number_of_pages($layout) { 00216 return substr_count(',' . $layout, ',0'); 00217 } 00218 00225 function quiz_number_of_questions_in_quiz($layout) { 00226 $layout = quiz_questions_in_quiz(quiz_clean_layout($layout)); 00227 $count = substr_count($layout, ','); 00228 if ($layout !== '') { 00229 $count++; 00230 } 00231 return $count; 00232 } 00233 00244 function quiz_repaginate($layout, $perpage, $shuffle = false) { 00245 $questions = quiz_questions_in_quiz($layout); 00246 if (!$questions) { 00247 return '0'; 00248 } 00249 00250 $questions = explode(',', quiz_questions_in_quiz($layout)); 00251 if ($shuffle) { 00252 shuffle($questions); 00253 } 00254 00255 $onthispage = 0; 00256 $layout = array(); 00257 foreach ($questions as $question) { 00258 if ($perpage and $onthispage >= $perpage) { 00259 $layout[] = 0; 00260 $onthispage = 0; 00261 } 00262 $layout[] = $question; 00263 $onthispage += 1; 00264 } 00265 00266 $layout[] = 0; 00267 return implode(',', $layout); 00268 } 00269 00271 00280 function quiz_get_all_question_grades($quiz) { 00281 global $CFG, $DB; 00282 00283 $questionlist = quiz_questions_in_quiz($quiz->questions); 00284 if (empty($questionlist)) { 00285 return array(); 00286 } 00287 00288 $params = array($quiz->id); 00289 $wheresql = ''; 00290 if (!is_null($questionlist)) { 00291 list($usql, $question_params) = $DB->get_in_or_equal(explode(',', $questionlist)); 00292 $wheresql = " AND question $usql "; 00293 $params = array_merge($params, $question_params); 00294 } 00295 00296 $instances = $DB->get_records_sql("SELECT question, grade, id 00297 FROM {quiz_question_instances} 00298 WHERE quiz = ? $wheresql", $params); 00299 00300 $list = explode(",", $questionlist); 00301 $grades = array(); 00302 00303 foreach ($list as $qid) { 00304 if (isset($instances[$qid])) { 00305 $grades[$qid] = $instances[$qid]->grade; 00306 } else { 00307 $grades[$qid] = 1; 00308 } 00309 } 00310 return $grades; 00311 } 00312 00324 function quiz_rescale_grade($rawgrade, $quiz, $format = true) { 00325 if (is_null($rawgrade)) { 00326 $grade = null; 00327 } else if ($quiz->sumgrades >= 0.000005) { 00328 $grade = $rawgrade * $quiz->grade / $quiz->sumgrades; 00329 } else { 00330 $grade = 0; 00331 } 00332 if ($format === 'question') { 00333 $grade = quiz_format_question_grade($quiz, $grade); 00334 } else if ($format) { 00335 $grade = quiz_format_grade($quiz, $grade); 00336 } 00337 return $grade; 00338 } 00339 00349 function quiz_feedback_for_grade($grade, $quiz, $context) { 00350 global $DB; 00351 00352 if (is_null($grade)) { 00353 return ''; 00354 } 00355 00356 // With CBM etc, it is possible to get -ve grades, which would then not match 00357 // any feedback. Therefore, we replace -ve grades with 0. 00358 $grade = max($grade, 0); 00359 00360 $feedback = $DB->get_record_select('quiz_feedback', 00361 'quizid = ? AND mingrade <= ? AND ? < maxgrade', array($quiz->id, $grade, $grade)); 00362 00363 if (empty($feedback->feedbacktext)) { 00364 return ''; 00365 } 00366 00367 // Clean the text, ready for display. 00368 $formatoptions = new stdClass(); 00369 $formatoptions->noclean = true; 00370 $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php', 00371 $context->id, 'mod_quiz', 'feedback', $feedback->id); 00372 $feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions); 00373 00374 return $feedbacktext; 00375 } 00376 00381 function quiz_has_feedback($quiz) { 00382 global $DB; 00383 static $cache = array(); 00384 if (!array_key_exists($quiz->id, $cache)) { 00385 $cache[$quiz->id] = quiz_has_grades($quiz) && 00386 $DB->record_exists_select('quiz_feedback', "quizid = ? AND " . 00387 $DB->sql_isnotempty('quiz_feedback', 'feedbacktext', false, true), 00388 array($quiz->id)); 00389 } 00390 return $cache[$quiz->id]; 00391 } 00392 00402 function quiz_update_sumgrades($quiz) { 00403 global $DB; 00404 00405 $sql = 'UPDATE {quiz} 00406 SET sumgrades = COALESCE(( 00407 SELECT SUM(grade) 00408 FROM {quiz_question_instances} 00409 WHERE quiz = {quiz}.id 00410 ), 0) 00411 WHERE id = ?'; 00412 $DB->execute($sql, array($quiz->id)); 00413 $quiz->sumgrades = $DB->get_field('quiz', 'sumgrades', array('id' => $quiz->id)); 00414 00415 if ($quiz->sumgrades < 0.000005 && quiz_has_attempts($quiz->id)) { 00416 // If the quiz has been attempted, and the sumgrades has been 00417 // set to 0, then we must also set the maximum possible grade to 0, or 00418 // we will get a divide by zero error. 00419 quiz_set_grade(0, $quiz); 00420 } 00421 } 00422 00428 function quiz_update_all_attempt_sumgrades($quiz) { 00429 global $DB; 00430 $dm = new question_engine_data_mapper(); 00431 $timenow = time(); 00432 00433 $sql = "UPDATE {quiz_attempts} 00434 SET 00435 timemodified = :timenow, 00436 sumgrades = ( 00437 {$dm->sum_usage_marks_subquery('uniqueid')} 00438 ) 00439 WHERE quiz = :quizid AND timefinish <> 0"; 00440 $DB->execute($sql, array('timenow' => $timenow, 'quizid' => $quiz->id)); 00441 } 00442 00455 function quiz_set_grade($newgrade, $quiz) { 00456 global $DB; 00457 // This is potentially expensive, so only do it if necessary. 00458 if (abs($quiz->grade - $newgrade) < 1e-7) { 00459 // Nothing to do. 00460 return true; 00461 } 00462 00463 // Use a transaction, so that on those databases that support it, this is safer. 00464 $transaction = $DB->start_delegated_transaction(); 00465 00466 // Update the quiz table. 00467 $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance)); 00468 00469 // Rescaling the other data is only possible if the old grade was non-zero. 00470 if ($quiz->grade > 1e-7) { 00471 global $CFG; 00472 00473 $factor = $newgrade/$quiz->grade; 00474 $quiz->grade = $newgrade; 00475 00476 // Update the quiz_grades table. 00477 $timemodified = time(); 00478 $DB->execute(" 00479 UPDATE {quiz_grades} 00480 SET grade = ? * grade, timemodified = ? 00481 WHERE quiz = ? 00482 ", array($factor, $timemodified, $quiz->id)); 00483 00484 // Update the quiz_feedback table. 00485 $DB->execute(" 00486 UPDATE {quiz_feedback} 00487 SET mingrade = ? * mingrade, maxgrade = ? * maxgrade 00488 WHERE quizid = ? 00489 ", array($factor, $factor, $quiz->id)); 00490 } 00491 00492 // update grade item and send all grades to gradebook 00493 quiz_grade_item_update($quiz); 00494 quiz_update_grades($quiz); 00495 00496 $transaction->allow_commit(); 00497 return true; 00498 } 00499 00510 function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) { 00511 global $DB, $OUTPUT, $USER; 00512 00513 if (empty($userid)) { 00514 $userid = $USER->id; 00515 } 00516 00517 if (!$attempts) { 00518 // Get all the attempts made by the user 00519 $attempts = quiz_get_user_attempts($quiz->id, $userid); 00520 } 00521 00522 // Calculate the best grade 00523 $bestgrade = quiz_calculate_best_grade($quiz, $attempts); 00524 $bestgrade = quiz_rescale_grade($bestgrade, $quiz, false); 00525 00526 // Save the best grade in the database 00527 if (is_null($bestgrade)) { 00528 $DB->delete_records('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid)); 00529 00530 } else if ($grade = $DB->get_record('quiz_grades', 00531 array('quiz' => $quiz->id, 'userid' => $userid))) { 00532 $grade->grade = $bestgrade; 00533 $grade->timemodified = time(); 00534 $DB->update_record('quiz_grades', $grade); 00535 00536 } else { 00537 $grade->quiz = $quiz->id; 00538 $grade->userid = $userid; 00539 $grade->grade = $bestgrade; 00540 $grade->timemodified = time(); 00541 $DB->insert_record('quiz_grades', $grade); 00542 } 00543 00544 quiz_update_grades($quiz, $userid); 00545 } 00546 00554 function quiz_calculate_best_grade($quiz, $attempts) { 00555 00556 switch ($quiz->grademethod) { 00557 00558 case QUIZ_ATTEMPTFIRST: 00559 $firstattempt = reset($attempts); 00560 return $firstattempt->sumgrades; 00561 00562 case QUIZ_ATTEMPTLAST: 00563 $lastattempt = end($attempts); 00564 return $lastattempt->sumgrades; 00565 00566 case QUIZ_GRADEAVERAGE: 00567 $sum = 0; 00568 $count = 0; 00569 foreach ($attempts as $attempt) { 00570 if (!is_null($attempt->sumgrades)) { 00571 $sum += $attempt->sumgrades; 00572 $count++; 00573 } 00574 } 00575 if ($count == 0) { 00576 return null; 00577 } 00578 return $sum / $count; 00579 00580 case QUIZ_GRADEHIGHEST: 00581 default: 00582 $max = null; 00583 foreach ($attempts as $attempt) { 00584 if ($attempt->sumgrades > $max) { 00585 $max = $attempt->sumgrades; 00586 } 00587 } 00588 return $max; 00589 } 00590 } 00591 00600 function quiz_update_all_final_grades($quiz) { 00601 global $DB; 00602 00603 if (!$quiz->sumgrades) { 00604 return; 00605 } 00606 00607 $param = array('iquizid' => $quiz->id); 00608 $firstlastattemptjoin = "JOIN ( 00609 SELECT 00610 iquiza.userid, 00611 MIN(attempt) AS firstattempt, 00612 MAX(attempt) AS lastattempt 00613 00614 FROM {quiz_attempts} iquiza 00615 00616 WHERE 00617 iquiza.timefinish <> 0 AND 00618 iquiza.preview = 0 AND 00619 iquiza.quiz = :iquizid 00620 00621 GROUP BY iquiza.userid 00622 ) first_last_attempts ON first_last_attempts.userid = quiza.userid"; 00623 00624 switch ($quiz->grademethod) { 00625 case QUIZ_ATTEMPTFIRST: 00626 // Because of the where clause, there will only be one row, but we 00627 // must still use an aggregate function. 00628 $select = 'MAX(quiza.sumgrades)'; 00629 $join = $firstlastattemptjoin; 00630 $where = 'quiza.attempt = first_last_attempts.firstattempt AND'; 00631 break; 00632 00633 case QUIZ_ATTEMPTLAST: 00634 // Because of the where clause, there will only be one row, but we 00635 // must still use an aggregate function. 00636 $select = 'MAX(quiza.sumgrades)'; 00637 $join = $firstlastattemptjoin; 00638 $where = 'quiza.attempt = first_last_attempts.lastattempt AND'; 00639 break; 00640 00641 case QUIZ_GRADEAVERAGE: 00642 $select = 'AVG(quiza.sumgrades)'; 00643 $join = ''; 00644 $where = ''; 00645 break; 00646 00647 default: 00648 case QUIZ_GRADEHIGHEST: 00649 $select = 'MAX(quiza.sumgrades)'; 00650 $join = ''; 00651 $where = ''; 00652 break; 00653 } 00654 00655 if ($quiz->sumgrades >= 0.000005) { 00656 $finalgrade = $select . ' * ' . ($quiz->grade / $quiz->sumgrades); 00657 } else { 00658 $finalgrade = '0'; 00659 } 00660 $param['quizid'] = $quiz->id; 00661 $param['quizid2'] = $quiz->id; 00662 $param['quizid3'] = $quiz->id; 00663 $param['quizid4'] = $quiz->id; 00664 $finalgradesubquery = " 00665 SELECT quiza.userid, $finalgrade AS newgrade 00666 FROM {quiz_attempts} quiza 00667 $join 00668 WHERE 00669 $where 00670 quiza.timefinish <> 0 AND 00671 quiza.preview = 0 AND 00672 quiza.quiz = :quizid3 00673 GROUP BY quiza.userid"; 00674 00675 $changedgrades = $DB->get_records_sql(" 00676 SELECT users.userid, qg.id, qg.grade, newgrades.newgrade 00677 00678 FROM ( 00679 SELECT userid 00680 FROM {quiz_grades} qg 00681 WHERE quiz = :quizid 00682 UNION 00683 SELECT DISTINCT userid 00684 FROM {quiz_attempts} quiza2 00685 WHERE 00686 quiza2.timefinish <> 0 AND 00687 quiza2.preview = 0 AND 00688 quiza2.quiz = :quizid2 00689 ) users 00690 00691 LEFT JOIN {quiz_grades} qg ON qg.userid = users.userid AND qg.quiz = :quizid4 00692 00693 LEFT JOIN ( 00694 $finalgradesubquery 00695 ) newgrades ON newgrades.userid = users.userid 00696 00697 WHERE 00698 ABS(newgrades.newgrade - qg.grade) > 0.000005 OR 00699 ((newgrades.newgrade IS NULL OR qg.grade IS NULL) AND NOT 00700 (newgrades.newgrade IS NULL AND qg.grade IS NULL))", 00701 // The mess on the previous line is detecting where the value is 00702 // NULL in one column, and NOT NULL in the other, but SQL does 00703 // not have an XOR operator, and MS SQL server can't cope with 00704 // (newgrades.newgrade IS NULL) <> (qg.grade IS NULL). 00705 $param); 00706 00707 $timenow = time(); 00708 $todelete = array(); 00709 foreach ($changedgrades as $changedgrade) { 00710 00711 if (is_null($changedgrade->newgrade)) { 00712 $todelete[] = $changedgrade->userid; 00713 00714 } else if (is_null($changedgrade->grade)) { 00715 $toinsert = new stdClass(); 00716 $toinsert->quiz = $quiz->id; 00717 $toinsert->userid = $changedgrade->userid; 00718 $toinsert->timemodified = $timenow; 00719 $toinsert->grade = $changedgrade->newgrade; 00720 $DB->insert_record('quiz_grades', $toinsert); 00721 00722 } else { 00723 $toupdate = new stdClass(); 00724 $toupdate->id = $changedgrade->id; 00725 $toupdate->grade = $changedgrade->newgrade; 00726 $toupdate->timemodified = $timenow; 00727 $DB->update_record('quiz_grades', $toupdate); 00728 } 00729 } 00730 00731 if (!empty($todelete)) { 00732 list($test, $params) = $DB->get_in_or_equal($todelete); 00733 $DB->delete_records_select('quiz_grades', 'quiz = ? AND userid ' . $test, 00734 array_merge(array($quiz->id), $params)); 00735 } 00736 } 00737 00747 function quiz_calculate_best_attempt($quiz, $attempts) { 00748 00749 switch ($quiz->grademethod) { 00750 00751 case QUIZ_ATTEMPTFIRST: 00752 foreach ($attempts as $attempt) { 00753 return $attempt; 00754 } 00755 break; 00756 00757 case QUIZ_GRADEAVERAGE: // need to do something with it :-) 00758 case QUIZ_ATTEMPTLAST: 00759 foreach ($attempts as $attempt) { 00760 $final = $attempt; 00761 } 00762 return $final; 00763 00764 default: 00765 case QUIZ_GRADEHIGHEST: 00766 $max = -1; 00767 foreach ($attempts as $attempt) { 00768 if ($attempt->sumgrades > $max) { 00769 $max = $attempt->sumgrades; 00770 $maxattempt = $attempt; 00771 } 00772 } 00773 return $maxattempt; 00774 } 00775 } 00776 00780 function quiz_get_grading_options() { 00781 return array( 00782 QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'), 00783 QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'), 00784 QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'), 00785 QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz') 00786 ); 00787 } 00788 00794 function quiz_get_grading_option_name($option) { 00795 $strings = quiz_get_grading_options(); 00796 return $strings[$option]; 00797 } 00798 00800 00809 function quiz_question_action_icons($quiz, $cmid, $question, $returnurl) { 00810 $html = quiz_question_preview_button($quiz, $question) . ' ' . 00811 quiz_question_edit_button($cmid, $question, $returnurl); 00812 return $html; 00813 } 00814 00823 function quiz_question_edit_button($cmid, $question, $returnurl, $contentaftericon = '') { 00824 global $CFG, $OUTPUT; 00825 00826 // Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page. 00827 static $stredit = null; 00828 static $strview = null; 00829 if ($stredit === null) { 00830 $stredit = get_string('edit'); 00831 $strview = get_string('view'); 00832 } 00833 00834 // What sort of icon should we show? 00835 $action = ''; 00836 if (!empty($question->id) && 00837 (question_has_capability_on($question, 'edit', $question->category) || 00838 question_has_capability_on($question, 'move', $question->category))) { 00839 $action = $stredit; 00840 $icon = '/t/edit'; 00841 } else if (!empty($question->id) && 00842 question_has_capability_on($question, 'view', $question->category)) { 00843 $action = $strview; 00844 $icon = '/i/info'; 00845 } 00846 00847 // Build the icon. 00848 if ($action) { 00849 if ($returnurl instanceof moodle_url) { 00850 $returnurl = str_replace($CFG->wwwroot, '', $returnurl->out(false)); 00851 } 00852 $questionparams = array('returnurl' => $returnurl, 'cmid' => $cmid, 'id' => $question->id); 00853 $questionurl = new moodle_url("$CFG->wwwroot/question/question.php", $questionparams); 00854 return '<a title="' . $action . '" href="' . $questionurl->out() . '" class="questioneditbutton"><img src="' . 00855 $OUTPUT->pix_url($icon) . '" alt="' . $action . '" />' . $contentaftericon . 00856 '</a>'; 00857 } else if ($contentaftericon) { 00858 return '<span class="questioneditbutton">' . $contentaftericon . '</span>'; 00859 } else { 00860 return ''; 00861 } 00862 } 00863 00869 function quiz_question_preview_url($quiz, $question) { 00870 // Get the appropriate display options. 00871 $displayoptions = mod_quiz_display_options::make_from_quiz($quiz, 00872 mod_quiz_display_options::DURING); 00873 00874 $maxmark = null; 00875 if (isset($question->maxmark)) { 00876 $maxmark = $question->maxmark; 00877 } 00878 00879 // Work out the correcte preview URL. 00880 return question_preview_url($question->id, $quiz->preferredbehaviour, 00881 $maxmark, $displayoptions); 00882 } 00883 00890 function quiz_question_preview_button($quiz, $question, $label = false) { 00891 global $CFG, $OUTPUT; 00892 if (!question_has_capability_on($question, 'use', $question->category)) { 00893 return ''; 00894 } 00895 00896 $url = quiz_question_preview_url($quiz, $question); 00897 00898 // Do we want a label? 00899 $strpreviewlabel = ''; 00900 if ($label) { 00901 $strpreviewlabel = get_string('preview', 'quiz'); 00902 } 00903 00904 // Build the icon. 00905 $strpreviewquestion = get_string('previewquestion', 'quiz'); 00906 $image = $OUTPUT->pix_icon('t/preview', $strpreviewquestion); 00907 00908 $action = new popup_action('click', $url, 'questionpreview', 00909 question_preview_popup_params()); 00910 00911 return $OUTPUT->action_link($url, $image, $action, array('title' => $strpreviewquestion)); 00912 } 00913 00919 function quiz_get_flag_option($attempt, $context) { 00920 global $USER; 00921 if (!has_capability('moodle/question:flag', $context)) { 00922 return question_display_options::HIDDEN; 00923 } else if ($attempt->userid == $USER->id) { 00924 return question_display_options::EDITABLE; 00925 } else { 00926 return question_display_options::VISIBLE; 00927 } 00928 } 00929 00937 function quiz_attempt_state($quiz, $attempt) { 00938 if ($attempt->timefinish == 0) { 00939 return mod_quiz_display_options::DURING; 00940 } else if (time() < $attempt->timefinish + 120) { 00941 return mod_quiz_display_options::IMMEDIATELY_AFTER; 00942 } else if (!$quiz->timeclose || time() < $quiz->timeclose) { 00943 return mod_quiz_display_options::LATER_WHILE_OPEN; 00944 } else { 00945 return mod_quiz_display_options::AFTER_CLOSE; 00946 } 00947 } 00948 00959 function quiz_get_review_options($quiz, $attempt, $context) { 00960 $options = mod_quiz_display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt)); 00961 00962 $options->readonly = true; 00963 $options->flags = quiz_get_flag_option($attempt, $context); 00964 if (!empty($attempt->id)) { 00965 $options->questionreviewlink = new moodle_url('/mod/quiz/reviewquestion.php', 00966 array('attempt' => $attempt->id)); 00967 } 00968 00969 // Show a link to the comment box only for closed attempts 00970 if (!empty($attempt->id) && $attempt->timefinish && !$attempt->preview && 00971 !is_null($context) && has_capability('mod/quiz:grade', $context)) { 00972 $options->manualcomment = question_display_options::VISIBLE; 00973 $options->manualcommentlink = new moodle_url('/mod/quiz/comment.php', 00974 array('attempt' => $attempt->id)); 00975 } 00976 00977 if (!is_null($context) && !$attempt->preview && 00978 has_capability('mod/quiz:viewreports', $context) && 00979 has_capability('moodle/grade:viewhidden', $context)) { 00980 // People who can see reports and hidden grades should be shown everything, 00981 // except during preview when teachers want to see what students see. 00982 $options->attempt = question_display_options::VISIBLE; 00983 $options->correctness = question_display_options::VISIBLE; 00984 $options->marks = question_display_options::MARK_AND_MAX; 00985 $options->feedback = question_display_options::VISIBLE; 00986 $options->numpartscorrect = question_display_options::VISIBLE; 00987 $options->generalfeedback = question_display_options::VISIBLE; 00988 $options->rightanswer = question_display_options::VISIBLE; 00989 $options->overallfeedback = question_display_options::VISIBLE; 00990 $options->history = question_display_options::VISIBLE; 00991 00992 } 00993 00994 return $options; 00995 } 00996 01012 function quiz_get_combined_reviewoptions($quiz, $attempts) { 01013 $fields = array('feedback', 'generalfeedback', 'rightanswer', 'overallfeedback'); 01014 $someoptions = new stdClass(); 01015 $alloptions = new stdClass(); 01016 foreach ($fields as $field) { 01017 $someoptions->$field = false; 01018 $alloptions->$field = true; 01019 } 01020 $someoptions->marks = question_display_options::HIDDEN; 01021 $alloptions->marks = question_display_options::MARK_AND_MAX; 01022 01023 foreach ($attempts as $attempt) { 01024 $attemptoptions = mod_quiz_display_options::make_from_quiz($quiz, 01025 quiz_attempt_state($quiz, $attempt)); 01026 foreach ($fields as $field) { 01027 $someoptions->$field = $someoptions->$field || $attemptoptions->$field; 01028 $alloptions->$field = $alloptions->$field && $attemptoptions->$field; 01029 } 01030 $someoptions->marks = max($someoptions->marks, $attemptoptions->marks); 01031 $alloptions->marks = min($alloptions->marks, $attemptoptions->marks); 01032 } 01033 return array($someoptions, $alloptions); 01034 } 01035 01047 function quiz_clean_layout($layout, $removeemptypages = false) { 01048 // Remove repeated ','s. This can happen when a restore fails to find the right 01049 // id to relink to. 01050 $layout = preg_replace('/,{2,}/', ',', trim($layout, ',')); 01051 01052 // Remove duplicate question ids 01053 $layout = explode(',', $layout); 01054 $cleanerlayout = array(); 01055 $seen = array(); 01056 foreach ($layout as $item) { 01057 if ($item == 0) { 01058 $cleanerlayout[] = '0'; 01059 } else if (!in_array($item, $seen)) { 01060 $cleanerlayout[] = $item; 01061 $seen[] = $item; 01062 } 01063 } 01064 01065 if ($removeemptypages) { 01066 // Avoid duplicate page breaks 01067 $layout = $cleanerlayout; 01068 $cleanerlayout = array(); 01069 $stripfollowingbreaks = true; // Ensure breaks are stripped from the start. 01070 foreach ($layout as $item) { 01071 if ($stripfollowingbreaks && $item == 0) { 01072 continue; 01073 } 01074 $cleanerlayout[] = $item; 01075 $stripfollowingbreaks = $item == 0; 01076 } 01077 } 01078 01079 // Add a page break at the end if there is none 01080 if (end($cleanerlayout) !== '0') { 01081 $cleanerlayout[] = '0'; 01082 } 01083 01084 return implode(',', $cleanerlayout); 01085 } 01086 01093 function quiz_get_slot_for_question($quiz, $questionid) { 01094 $questionids = quiz_questions_in_quiz($quiz->questions); 01095 foreach (explode(',', $questionids) as $key => $id) { 01096 if ($id == $questionid) { 01097 return $key + 1; 01098 } 01099 } 01100 return null; 01101 } 01102 01104 01113 function quiz_send_confirmation($recipient, $a) { 01114 01115 // Add information about the recipient to $a 01116 // Don't do idnumber. we want idnumber to be the submitter's idnumber. 01117 $a->username = fullname($recipient); 01118 $a->userusername = $recipient->username; 01119 01120 // Prepare message 01121 $eventdata = new stdClass(); 01122 $eventdata->component = 'mod_quiz'; 01123 $eventdata->name = 'confirmation'; 01124 $eventdata->notification = 1; 01125 01126 $eventdata->userfrom = get_admin(); 01127 $eventdata->userto = $recipient; 01128 $eventdata->subject = get_string('emailconfirmsubject', 'quiz', $a); 01129 $eventdata->fullmessage = get_string('emailconfirmbody', 'quiz', $a); 01130 $eventdata->fullmessageformat = FORMAT_PLAIN; 01131 $eventdata->fullmessagehtml = ''; 01132 01133 $eventdata->smallmessage = get_string('emailconfirmsmall', 'quiz', $a); 01134 $eventdata->contexturl = $a->quizurl; 01135 $eventdata->contexturlname = $a->quizname; 01136 01137 // ... and send it. 01138 return message_send($eventdata); 01139 } 01140 01149 function quiz_send_notification($recipient, $submitter, $a) { 01150 01151 // Recipient info for template 01152 $a->useridnumber = $recipient->idnumber; 01153 $a->username = fullname($recipient); 01154 $a->userusername = $recipient->username; 01155 01156 // Prepare message 01157 $eventdata = new stdClass(); 01158 $eventdata->component = 'mod_quiz'; 01159 $eventdata->name = 'submission'; 01160 $eventdata->notification = 1; 01161 01162 $eventdata->userfrom = $submitter; 01163 $eventdata->userto = $recipient; 01164 $eventdata->subject = get_string('emailnotifysubject', 'quiz', $a); 01165 $eventdata->fullmessage = get_string('emailnotifybody', 'quiz', $a); 01166 $eventdata->fullmessageformat = FORMAT_PLAIN; 01167 $eventdata->fullmessagehtml = ''; 01168 01169 $eventdata->smallmessage = get_string('emailnotifysmall', 'quiz', $a); 01170 $eventdata->contexturl = $a->quizreviewurl; 01171 $eventdata->contexturlname = $a->quizname; 01172 01173 // ... and send it. 01174 return message_send($eventdata); 01175 } 01176 01188 function quiz_send_notification_messages($course, $quiz, $attempt, $context, $cm) { 01189 global $CFG, $DB; 01190 01191 // Do nothing if required objects not present 01192 if (empty($course) or empty($quiz) or empty($attempt) or empty($context)) { 01193 throw new coding_exception('$course, $quiz, $attempt, $context and $cm must all be set.'); 01194 } 01195 01196 $submitter = $DB->get_record('user', array('id' => $attempt->userid), '*', MUST_EXIST); 01197 01198 // Check for confirmation required 01199 $sendconfirm = false; 01200 $notifyexcludeusers = ''; 01201 if (has_capability('mod/quiz:emailconfirmsubmission', $context, $submitter, false)) { 01202 $notifyexcludeusers = $submitter->id; 01203 $sendconfirm = true; 01204 } 01205 01206 // check for notifications required 01207 $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.idnumber, u.email, u.emailstop, ' . 01208 'u.lang, u.timezone, u.mailformat, u.maildisplay'; 01209 $groups = groups_get_all_groups($course->id, $submitter->id); 01210 if (is_array($groups) && count($groups) > 0) { 01211 $groups = array_keys($groups); 01212 } else if (groups_get_activity_groupmode($cm, $course) != NOGROUPS) { 01213 // If the user is not in a group, and the quiz is set to group mode, 01214 // then set $groups to a non-existant id so that only users with 01215 // 'moodle/site:accessallgroups' get notified. 01216 $groups = -1; 01217 } else { 01218 $groups = ''; 01219 } 01220 $userstonotify = get_users_by_capability($context, 'mod/quiz:emailnotifysubmission', 01221 $notifyfields, '', '', '', $groups, $notifyexcludeusers, false, false, true); 01222 01223 if (empty($userstonotify) && !$sendconfirm) { 01224 return true; // Nothing to do. 01225 } 01226 01227 $a = new stdClass(); 01228 // Course info 01229 $a->coursename = $course->fullname; 01230 $a->courseshortname = $course->shortname; 01231 // Quiz info 01232 $a->quizname = $quiz->name; 01233 $a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id; 01234 $a->quizreportlink = '<a href="' . $a->quizreporturl . '">' . 01235 format_string($quiz->name) . ' report</a>'; 01236 $a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id; 01237 $a->quizreviewlink = '<a href="' . $a->quizreviewurl . '">' . 01238 format_string($quiz->name) . ' review</a>'; 01239 $a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id; 01240 $a->quizlink = '<a href="' . $a->quizurl . '">' . format_string($quiz->name) . '</a>'; 01241 // Attempt info 01242 $a->submissiontime = userdate($attempt->timefinish); 01243 $a->timetaken = format_time($attempt->timefinish - $attempt->timestart); 01244 // Student who sat the quiz info 01245 $a->studentidnumber = $submitter->idnumber; 01246 $a->studentname = fullname($submitter); 01247 $a->studentusername = $submitter->username; 01248 01249 $allok = true; 01250 01251 // Send notifications if required 01252 if (!empty($userstonotify)) { 01253 foreach ($userstonotify as $recipient) { 01254 $allok = $allok && quiz_send_notification($recipient, $submitter, $a); 01255 } 01256 } 01257 01258 // Send confirmation if required. We send the student confirmation last, so 01259 // that if message sending is being intermittently buggy, which means we send 01260 // some but not all messages, and then try again later, then teachers may get 01261 // duplicate messages, but the student will always get exactly one. 01262 if ($sendconfirm) { 01263 $allok = $allok && quiz_send_confirmation($submitter, $a); 01264 } 01265 01266 return $allok; 01267 } 01268 01276 function quiz_attempt_submitted_handler($event) { 01277 global $DB; 01278 01279 $course = $DB->get_record('course', array('id' => $event->courseid)); 01280 $quiz = $DB->get_record('quiz', array('id' => $event->quizid)); 01281 $cm = get_coursemodule_from_id('quiz', $event->cmid, $event->courseid); 01282 $attempt = $DB->get_record('quiz_attempts', array('id' => $event->attemptid)); 01283 01284 if (!($course && $quiz && $cm && $attempt)) { 01285 // Something has been deleted since the event was raised. Therefore, the 01286 // event is no longer relevant. 01287 return true; 01288 } 01289 01290 return quiz_send_notification_messages($course, $quiz, $attempt, 01291 get_context_instance(CONTEXT_MODULE, $cm->id), $cm); 01292 } 01293 01294 function quiz_get_js_module() { 01295 global $PAGE; 01296 01297 return array( 01298 'name' => 'mod_quiz', 01299 'fullpath' => '/mod/quiz/module.js', 01300 'requires' => array('base', 'dom', 'event-delegate', 'event-key', 01301 'core_question_engine'), 01302 'strings' => array( 01303 array('cancel', 'moodle'), 01304 array('flagged', 'question'), 01305 array('functiondisabledbysecuremode', 'quiz'), 01306 array('startattempt', 'quiz'), 01307 array('timesup', 'quiz'), 01308 ), 01309 ); 01310 } 01311 01312 01320 class mod_quiz_display_options extends question_display_options { 01325 const DURING = 0x10000; 01326 const IMMEDIATELY_AFTER = 0x01000; 01327 const LATER_WHILE_OPEN = 0x00100; 01328 const AFTER_CLOSE = 0x00010; 01335 public $attempt = true; 01336 01341 public $overallfeedback = self::VISIBLE; 01342 01350 public static function make_from_quiz($quiz, $when) { 01351 $options = new self(); 01352 01353 $options->attempt = self::extract($quiz->reviewattempt, $when, true, false); 01354 $options->correctness = self::extract($quiz->reviewcorrectness, $when); 01355 $options->marks = self::extract($quiz->reviewmarks, $when, 01356 self::MARK_AND_MAX, self::MAX_ONLY); 01357 $options->feedback = self::extract($quiz->reviewspecificfeedback, $when); 01358 $options->generalfeedback = self::extract($quiz->reviewgeneralfeedback, $when); 01359 $options->rightanswer = self::extract($quiz->reviewrightanswer, $when); 01360 $options->overallfeedback = self::extract($quiz->reviewoverallfeedback, $when); 01361 01362 $options->numpartscorrect = $options->feedback; 01363 01364 if ($quiz->questiondecimalpoints != -1) { 01365 $options->markdp = $quiz->questiondecimalpoints; 01366 } else { 01367 $options->markdp = $quiz->decimalpoints; 01368 } 01369 01370 return $options; 01371 } 01372 01373 protected static function extract($bitmask, $bit, 01374 $whenset = self::VISIBLE, $whennotset = self::HIDDEN) { 01375 if ($bitmask & $bit) { 01376 return $whenset; 01377 } else { 01378 return $whennotset; 01379 } 01380 } 01381 } 01382 01383 01391 class qubaids_for_quiz extends qubaid_join { 01392 public function __construct($quizid, $includepreviews = true, $onlyfinished = false) { 01393 $where = 'quiza.quiz = :quizaquiz'; 01394 if (!$includepreviews) { 01395 $where .= ' AND preview = 0'; 01396 } 01397 if ($onlyfinished) { 01398 $where .= ' AND timefinish <> 0'; 01399 } 01400 01401 parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, 01402 array('quizaquiz' => $quizid)); 01403 } 01404 }