Moodle  2.2.1
http://www.collinsharper.com
C:/xampp/htdocs/moodle/mod/quiz/locallib.php
Go to the documentation of this file.
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 }
 All Data Structures Namespaces Files Functions Variables Enumerations