dirroot/mod/quiz/lib.php"); //require_once($CFG->libdir.'/questionlib.php'); require_once("{$CFG->dirroot}/question/editlib.php"); /// CONSTANTS /////////////////////////////////////////////////////////////////// /**#@+ * Options determining how the grades from individual attempts are combined to give * the overall grade for a user */ define("QUIZ_GRADEHIGHEST", "1"); define("QUIZ_GRADEAVERAGE", "2"); define("QUIZ_ATTEMPTFIRST", "3"); define("QUIZ_ATTEMPTLAST", "4"); $QUIZ_GRADE_METHOD = array ( QUIZ_GRADEHIGHEST => get_string("gradehighest", "quiz"), QUIZ_GRADEAVERAGE => get_string("gradeaverage", "quiz"), QUIZ_ATTEMPTFIRST => get_string("attemptfirst", "quiz"), QUIZ_ATTEMPTLAST => get_string("attemptlast", "quiz")); /**#@-*/ /// FUNCTIONS RELATED TO ATTEMPTS ///////////////////////////////////////// /** * Creates an object to represent a new attempt at a quiz * * Creates an attempt object to represent an attempt at the quiz by the current * user starting at the current time. The ->id field is not set. The object is * NOT written to the database. * @return object The newly created attempt object. * @param object $quiz The quiz to create an attempt for. * @param integer $attemptnumber The sequence number for the attempt. */ function quiz_create_attempt($quiz, $attemptnumber) { global $USER, $CFG; if (!$attemptnumber > 1 or !$quiz->attemptonlast or !$attempt = get_record('quiz_attempts', 'quiz', $quiz->id, 'userid', $USER->id, 'attempt', $attemptnumber-1)) { // we are not building on last attempt so create a new attempt $attempt->quiz = $quiz->id; $attempt->userid = $USER->id; $attempt->preview = 0; if ($quiz->shufflequestions) { $attempt->layout = quiz_repaginate($quiz->questions, $quiz->questionsperpage, true); } else { $attempt->layout = $quiz->questions; } } $timenow = time(); $attempt->attempt = $attemptnumber; $attempt->sumgrades = 0.0; $attempt->timestart = $timenow; $attempt->timefinish = 0; $attempt->timemodified = $timenow; $attempt->uniqueid = question_new_attempt_uniqueid(); return $attempt; } /** * Returns an unfinished attempt (if there is one) for the given * user on the given quiz. This function does not return preview attempts. * * @param integer $quizid the id of the quiz. * @param integer $userid the id of the user. * * @return mixed the unfinished attempt if there is one, false if not. */ function quiz_get_user_attempt_unfinished($quizid, $userid) { return get_record_select("quiz_attempts", "quiz = $quizid AND userid = $userid AND timefinish = 0 AND preview = 0"); } /** * @param integer $quizid the quiz id. * @param integer $userid the userid. * @return an array of all the ueser's attempts at this quiz. Returns an empty array if there are none. */ function quiz_get_user_attempts($quizid, $userid) { if ($attempts = get_records_select("quiz_attempts", "quiz = '$quizid' AND userid = '$userid' AND timefinish > 0", "attempt ASC")) { return $attempts; } else { return array(); } } /// FUNCTIONS TO DO WITH QUIZ LAYOUT AND PAGES //////////////////////////////// /** * Returns a comma separated list of question ids for the current page * * @return string Comma separated list of question ids * @param string $layout The string representing the quiz layout. Each page is represented as a * comma separated list of question ids and 0 indicating page breaks. * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2 * @param integer $page The number of the current page. */ function quiz_questions_on_page($layout, $page) { $pages = explode(',0', $layout); return trim($pages[$page], ','); } /** * Returns a comma separated list of question ids for the quiz * * @return string Comma separated list of question ids * @param string $layout The string representing the quiz layout. Each page is represented as a * comma separated list of question ids and 0 indicating page breaks. * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2 */ function quiz_questions_in_quiz($layout) { return str_replace(',0', '', $layout); } /** * Returns the number of pages in the quiz layout * * @return integer Comma separated list of question ids * @param string $layout The string representing the quiz layout. */ function quiz_number_of_pages($layout) { return substr_count($layout, ',0'); } /** * Returns the first question number for the current quiz page * * @return integer The number of the first question * @param string $quizlayout The string representing the layout for the whole quiz * @param string $pagelayout The string representing the layout for the current page */ function quiz_first_questionnumber($quizlayout, $pagelayout) { // this works by finding all the questions from the quizlayout that // come before the current page and then adding up their lengths. global $CFG; $start = strpos($quizlayout, ','.$pagelayout.',')-2; if ($start > 0) { $prevlist = substr($quizlayout, 0, $start); return get_field_sql("SELECT sum(length)+1 FROM {$CFG->prefix}question WHERE id IN ($prevlist)"); } else { return 1; } } /** * Re-paginates the quiz layout * * @return string The new layout string * @param string $layout The string representing the quiz layout. * @param integer $perpage The number of questions per page * @param boolean $shuffle Should the questions be reordered randomly? */ function quiz_repaginate($layout, $perpage, $shuffle=false) { $layout = str_replace(',0', '', $layout); // remove existing page breaks $questions = explode(',', $layout); if ($shuffle) { srand((float)microtime() * 1000000); // for php < 4.2 shuffle($questions); } $i = 1; $layout = ''; foreach ($questions as $question) { if ($perpage and $i > $perpage) { $layout .= '0,'; $i = 1; } $layout .= $question.','; $i++; } return $layout.'0'; } /** * Print navigation panel for quiz attempt and review pages * * @param integer $page The number of the current page (counting from 0). * @param integer $pages The total number of pages. */ function quiz_print_navigation_panel($page, $pages) { //$page++; echo '
'; } /// FUNCTIONS TO DO WITH QUIZ GRADES ////////////////////////////////////////// /** * Creates an array of maximum grades for a quiz * * The grades are extracted from the quiz_question_instances table. * @return array Array of grades indexed by question id * These are the maximum possible grades that * students can achieve for each of the questions * @param integer $quiz The quiz object */ function quiz_get_all_question_grades($quiz) { global $CFG; $questionlist = quiz_questions_in_quiz($quiz->questions); if (empty($questionlist)) { return array(); } $instances = get_records_sql("SELECT question,grade,id FROM {$CFG->prefix}quiz_question_instances WHERE quiz = '$quiz->id'" . (is_null($questionlist) ? '' : "AND question IN ($questionlist)")); $list = explode(",", $questionlist); $grades = array(); foreach ($list as $qid) { if (isset($instances[$qid])) { $grades[$qid] = $instances[$qid]->grade; } else { $grades[$qid] = 1; } } return $grades; } /** * Get the best current grade for a particular user in a quiz. * * @param object $quiz the quiz object. * @param integer $userid the id of the user. * @return float the user's current grade for this quiz. */ function quiz_get_best_grade($quiz, $userid) { $grade = get_field('quiz_grades', 'grade', 'quiz', $quiz->id, 'userid', $userid); // Need to detect errors/no result, without catching 0 scores. if (is_numeric($grade)) { return round($grade,$quiz->decimalpoints); } else { return NULL; } } /** * Convert the raw grade stored in $attempt into a grade out of the maximum * grade for this quiz. * * @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used. * @return float the rescaled grade. */ function quiz_rescale_grade($rawgrade, $quiz) { if ($quiz->sumgrades) { return round($rawgrade*$quiz->grade/$quiz->sumgrades, $quiz->decimalpoints); } else { return 0; } } /** * Get the feedback text that should be show to a student who * got this grade on this quiz. * * @param float $grade a grade on this quiz. * @param integer $quizid the id of the quiz object. * @return string the comment that corresponds to this grade (empty string if there is not one. */ function quiz_feedback_for_grade($grade, $quizid) { $feedback = get_field_select('quiz_feedback', 'feedbacktext', "quizid = $quizid AND mingrade <= $grade AND $grade < maxgrade"); if (empty($feedback)) { $feedback = ''; } return $feedback; } /** * @param integer $quizid the id of the quiz object. * @return boolean Whether this quiz has any non-blank feedback text. */ function quiz_has_feedback($quizid) { static $cache = array(); if (!array_key_exists($quizid, $cache)) { $cache[$quizid] = record_exists_select('quiz_feedback', "quizid = $quizid AND feedbacktext <> ''"); } return $cache[$quizid]; } /** * The quiz grade is the score that student's results are marked out of. When it * changes, the corresponding data in quiz_grades and quiz_feedback needs to be * rescaled. * * @param float $newgrade the new maximum grade for the quiz. * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too. * @return boolean indicating success or failure. */ function quiz_set_grade($newgrade, &$quiz) { // This is potentially expensive, so only do it if necessary. if (abs($quiz->grade - $newgrade) < 1e-7) { // Nothing to do. return true; } // Use a transaction, so that on those databases that support it, this is safer. begin_sql(); // Update the quiz table. $success = set_field('quiz', 'grade', $newgrade, 'id', $quiz->instance); // Rescaling the other data is only possible if the old grade was non-zero. if ($quiz->grade > 1e-7) { global $CFG; $factor = $newgrade/$quiz->grade; $quiz->grade = $newgrade; // Update the quiz_grades table. $timemodified = time(); $success = $success && execute_sql(" UPDATE {$CFG->prefix}quiz_grades SET grade = $factor * grade, timemodified = $timemodified WHERE quiz = $quiz->id ", false); // Update the quiz_grades table. $success = $success && execute_sql(" UPDATE {$CFG->prefix}quiz_feedback SET mingrade = $factor * mingrade, maxgrade = $factor * maxgrade WHERE quizid = $quiz->id ", false); } if ($success) { return commit_sql(); } else { rollback_sql(); return false; } } /** * Save the overall grade for a user at a quiz in the quiz_grades table * * @param object $quiz The quiz for which the best grade is to be calculated and then saved. * @param integer $userid The userid to calculate the grade for. Defaults to the current user. * @return boolean Indicates success or failure. */ function quiz_save_best_grade($quiz, $userid = null) { global $USER; if (empty($userid)) { $userid = $USER->id; } // Get all the attempts made by the user if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) { notify('Could not find any user attempts'); return false; } // Calculate the best grade $bestgrade = quiz_calculate_best_grade($quiz, $attempts); $bestgrade = quiz_rescale_grade($bestgrade, $quiz); // Save the best grade in the database if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) { $grade->grade = $bestgrade; $grade->timemodified = time(); if (!update_record('quiz_grades', $grade)) { notify('Could not update best grade'); return false; } } else { $grade->quiz = $quiz->id; $grade->userid = $userid; $grade->grade = $bestgrade; $grade->timemodified = time(); if (!insert_record('quiz_grades', $grade)) { notify('Could not insert new best grade'); return false; } } return true; } /** * Calculate the overall grade for a quiz given a number of attempts by a particular user. * * @return float The overall grade * @param object $quiz The quiz for which the best grade is to be calculated * @param array $attempts An array of all the attempts of the user at the quiz */ function quiz_calculate_best_grade($quiz, $attempts) { switch ($quiz->grademethod) { case QUIZ_ATTEMPTFIRST: foreach ($attempts as $attempt) { return $attempt->sumgrades; } break; case QUIZ_ATTEMPTLAST: foreach ($attempts as $attempt) { $final = $attempt->sumgrades; } return $final; case QUIZ_GRADEAVERAGE: $sum = 0; $count = 0; foreach ($attempts as $attempt) { $sum += $attempt->sumgrades; $count++; } return (float)$sum/$count; default: case QUIZ_GRADEHIGHEST: $max = 0; foreach ($attempts as $attempt) { if ($attempt->sumgrades > $max) { $max = $attempt->sumgrades; } } return $max; } } /** * Return the attempt with the best grade for a quiz * * Which attempt is the best depends on $quiz->grademethod. If the grade * method is GRADEAVERAGE then this function simply returns the last attempt. * @return object The attempt with the best grade * @param object $quiz The quiz for which the best grade is to be calculated * @param array $attempts An array of all the attempts of the user at the quiz */ function quiz_calculate_best_attempt($quiz, $attempts) { switch ($quiz->grademethod) { case QUIZ_ATTEMPTFIRST: foreach ($attempts as $attempt) { return $attempt; } break; case QUIZ_GRADEAVERAGE: // need to do something with it :-) case QUIZ_ATTEMPTLAST: foreach ($attempts as $attempt) { $final = $attempt; } return $final; default: case QUIZ_GRADEHIGHEST: $max = -1; foreach ($attempts as $attempt) { if ($attempt->sumgrades > $max) { $max = $attempt->sumgrades; $maxattempt = $attempt; } } return $maxattempt; } } /// OTHER QUIZ FUNCTIONS //////////////////////////////////////////////////// /** * Print a box with quiz start and due dates * * @param object $quiz */ function quiz_view_dates($quiz) { if (!$quiz->timeopen && !$quiz->timeclose) { return; } print_simple_box_start('center', '', '', '', 'generalbox', 'dates'); echo ''.get_string("quizopen", "quiz").': | '; echo ''.userdate($quiz->timeopen).' |
'.get_string("quizclose", "quiz").': | '; echo ''.userdate($quiz->timeclose).' |