get_string("gradehighest", "quiz"), GRADEAVERAGE => get_string("gradeaverage", "quiz"), ATTEMPTFIRST => get_string("attemptfirst", "quiz"), ATTEMPTLAST => get_string("attemptlast", "quiz")); define("SHORTANSWER", "1"); define("TRUEFALSE", "2"); define("MULTICHOICE", "3"); define("RANDOM", "4"); define("MATCH", "5"); define("RANDOMSAMATCH", "6"); define("DESCRIPTION", "7"); define("NUMERICAL", "8"); define("MULTIANSWER", "9"); define("CALCULATED", "10"); // The $QUIZ_QUESTION_TYPE array holds the names of all the question types that the user should // be able to create directly. Some internal question types like random questions are excluded. // The complete list of question types can be found in $QUIZ_QTYPES. $QUIZ_QUESTION_TYPE = array ( MULTICHOICE => get_string("multichoice", "quiz"), TRUEFALSE => get_string("truefalse", "quiz"), SHORTANSWER => get_string("shortanswer", "quiz"), NUMERICAL => get_string("numerical", "quiz"), CALCULATED => get_string("calculated", "quiz"), MATCH => get_string("match", "quiz"), DESCRIPTION => get_string("description", "quiz"), RANDOMSAMATCH => get_string("randomsamatch", "quiz"), MULTIANSWER => get_string("multianswer", "quiz") ); define("QUIZ_PICTURE_MAX_HEIGHT", "600"); // Not currently implemented define("QUIZ_PICTURE_MAX_WIDTH", "600"); // Not currently implemented define("QUIZ_MAX_NUMBER_ANSWERS", "10"); define("QUIZ_CATEGORIES_SORTORDER", "999"); define('QUIZ_REVIEW_AFTER', 1); define('QUIZ_REVIEW_BEFORE', 2); $QUIZ_QTYPES= array(); /// QUIZ_QTYPES INITIATION ////////////////// class quiz_default_questiontype { function name() { return 'default'; } function uses_quizfile($question, $relativefilepath) { // The default does only check whether the file is used as image: return $question->image == $relativefilepath; } function save_question_options($question) { /// Given some question info and some data about the the answers /// this function parses, organises and saves the question /// It is used by question.php through ->save_question when /// saving new data from a form, and also by import.php when /// importing questions /// /// If this is an update, and old answers already exist, then /// these are overwritten using an update(). To do this, it /// it is assumed that the IDs in quiz_answers are in the same /// sort order as the new answers being saved. This should always /// be true, but it's something to keep in mind if fiddling with /// question.php /// /// Returns $result->error or $result->noticeyesno or $result->notice /// This default implementation must be overridden: $result->error = "Unsupported question type ($question->qtype)!"; return $result; } function save_question($question, $form, $course) { // This default implementation is suitable for most // question types. // First, save the basic question itself $question->name = trim($form->name); $question->questiontext = trim($form->questiontext); $question->questiontextformat = $form->questiontextformat; if (empty($form->image)) { $question->image = ""; } else { $question->image = $form->image; } if (empty($question->name)) { $question->name = strip_tags($question->questiontext); if (empty($question->name)) { $question->name = '-'; } } if (isset($form->defaultgrade)) { $question->defaultgrade = $form->defaultgrade; } if (!empty($question->id)) { // Question already exists $question->version ++; // Update version number of question if (!update_record("quiz_questions", $question)) { error("Could not update question!"); } } else { // Question is a new one $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) $question->version = 1; if (!$question->id = insert_record("quiz_questions", $question)) { error("Could not insert new question!"); } } // Now to save all the answers and type-specific options $form->id = $question->id; $form->qtype = $question->qtype; $form->category = $question->category; $result = $this->save_question_options($form); if (!empty($result->error)) { error($result->error); } if (!empty($result->notice)) { notice($result->notice, "question.php?id=$question->id"); } if (!empty($result->noticeyesno)) { notice_yesno($result->noticeyesno, "question.php?id=$question->id", "edit.php"); print_footer($course); exit; } redirect("edit.php"); } /// Convenience function that is used within the question types only function extract_response_id($responsekey) { if (ereg('[0-9]'.$this->name().'([0-9]+)', $responsekey, $regs)) { return $regs[1]; } else { return false; } } function wrapped_questions($question) { /// Overridden only by question types whose questions can /// wrap other questions. Two question types that do this /// are RANDOMSAMATCH and RANDOM /// If there are wrapped questions, then this method returns /// comma separated list of them... return false; } function convert_to_response_answer_field($questionresponse) { /// This function is very much the inverse of extract_response /// This function and extract_response, should be /// obsolete as soon as we get a better response storage /// Right now they are a bridge between a consistent /// response model and the old field answer in quiz_responses /// This is the default implemention... return implode(',', $questionresponse); } function get_answers($question) { // Returns the answers for the specified question // The default behaviour that signals that something is wrong return false; } function create_response($question, $nameprefix, $questionsinuse) { /// This rather smart solution works for most cases: $rawresponse->question = $question->id; $rawresponse->answer = ''; return $this->extract_response($rawresponse, $nameprefix); } function extract_response($rawresponse, $nameprefix) { /// This function is very much the inverse of convert_to_response_answer_field /// This function and convert_to_response_answer_field, should be /// obsolete as soon as we get a better response storage /// Right now they are a bridge between a consistent /// response model and the old field answer in quiz_responses /// Default behaviour that works for singlton response question types /// like SHORTANSWER, NUMERICAL and TRUEFALSE return array($nameprefix => $rawresponse->answer); } function print_question_number_and_grading_details ($number, $grade, $actualgrade=false, $recentlyadded=false, $questionid=0, $courseid=0) { /// Print question number and grade: global $CFG; static $streditquestions, $strmarks, $strrecentlyaddedquestion; if (!isset($streditquestions)) { $streditquestions = get_string('editquestions', 'quiz'); $strmarks = get_string('marks', 'quiz'); $strrecentlyaddedquestion = get_string('recentlyaddedquestion', 'quiz'); } echo '
';
echo '
'; $this->print_question_number_and_grading_details ($currentnumber, $quiz->grade ? $question->maxgrade : false, empty($resultdetails) ? false : $resultdetails->grade, isset($question->recentlyadded) ? $question->recentlyadded : false, $question->id, $quiz->course); $this->print_question_formulation_and_controls( $question, $quiz, $readonly, empty($resultdetails) ? false : $resultdetails->answers, empty($resultdetails) ? false : $resultdetails->correctanswers, quiz_qtype_nameprefix($question)); echo " |
$text
"; } function quiz_print_question_icon($question, $editlink=true) { // Prints a question icon global $QUIZ_QUESTION_TYPE; global $QUIZ_QTYPES; if ($editlink) { echo "id\" title=\"" .$QUIZ_QTYPES[$question->qtype]->name()."\">"; } echo '"; echo "$strcategory: "; echo " | "; popup_form ("edit.php?cat=", $catmenu, "catmenu", $current, "", "", "", false, "self"); echo " | "; echo ""; echo ' |
"; print_string("noquestions", "quiz"); echo "
"; return; } $order = explode(",", $questionlist); if (!$questions = get_list_of_questions($questionlist)) { echo ""; print_string("noquestions", "quiz"); echo "
"; return; } $strorder = get_string("order"); $strquestionname = get_string("questionname", "quiz"); $strgrade = get_string("grade"); $strremove = get_string('remove', 'quiz'); $stredit = get_string("edit"); $strmoveup = get_string("moveup"); $strmovedown = get_string("movedown"); $strsavegrades = get_string("savegrades", "quiz"); $strtype = get_string("type", "quiz"); $strpreview = get_string("preview", "quiz"); $count = 0; $sumgrade = 0; $total = count($order); echo "\n"; return $sumgrade; } function quiz_print_cat_question_list($categoryid, $quizselected=true, $recurse=1, $page, $perpage) { // Prints the table of questions in a category with interactions global $QUIZ_QUESTION_TYPE, $USER; $strcategory = get_string("category", "quiz"); $strquestion = get_string("question", "quiz"); $straddquestions = get_string("addquestions", "quiz"); $strimportquestions = get_string("importquestions", "quiz"); $strexportquestions = get_string("exportquestions", "quiz"); $strnoquestions = get_string("noquestions", "quiz"); $strselect = get_string("select", "quiz"); $strselectall = get_string("selectall", "quiz"); $strcreatenewquestion = get_string("createnewquestion", "quiz"); $strquestionname = get_string("questionname", "quiz"); $strdelete = get_string("delete"); $stredit = get_string("edit"); $strcopy = get_string("copy"); $straddselectedtoquiz = get_string("addselectedtoquiz", "quiz"); $strtype = get_string("type", "quiz"); $strcreatemultiple = get_string("createmultiple", "quiz"); $strpreview = get_string("preview","quiz"); if (!$categoryid) { echo ""; print_string("selectcategoryabove", "quiz"); echo "
"; if ($quizselected) { echo ""; print_string("addingquestions", "quiz"); echo "
"; } return; } if (!$category = get_record("quiz_categories", "id", "$categoryid")) { notify("Category not found!"); return; } echo "$strcreatenewquestion: | "; echo ''; popup_form ("question.php?category=$category->id&qtype=", $QUIZ_QUESTION_TYPE, "addquestion", "", "choose", "", "", false, "self"); echo ' | '; helpbutton("questiontypes", $strcreatenewquestion, "quiz"); echo ' |
'; print_string("publishedit","quiz"); echo ' | ||
'; if (isteacheredit($category->course)) { echo ''.$strimportquestions.''; helpbutton("import", $strimportquestions, "quiz"); echo ' | '; } echo ''.$strexportquestions.''; helpbutton("export", $strexportquestions, "quiz"); echo ' |
"; print_string("noquestions", "quiz"); echo "
"; return; } $canedit = isteacheredit($category->course); echo "\n"; if ($quizselected and isteacheredit($category->course)) { for ($i=1;$i<=10; $i++) { $randomcount[$i] = $i; } echo ''; } } function quiz_start_attempt($quizid, $userid, $numattempt) { $attempt->quiz = $quizid; $attempt->userid = $userid; $attempt->attempt = $numattempt; $attempt->timestart = time(); $attempt->timefinish = 0; $attempt->timemodified = time(); $attempt->id = insert_record("quiz_attempts", $attempt); return $attempt; } function quiz_get_user_attempt_unfinished($quizid, $userid) { // Returns an object containing an unfinished attempt (if there is one) return get_record("quiz_attempts", "quiz", $quizid, "userid", $userid, "timefinish", 0); } function quiz_get_user_attempts($quizid, $userid) { // Returns a list of all attempts by a user return get_records_select("quiz_attempts", "quiz = '$quizid' AND userid = '$userid' AND timefinish > 0", "attempt ASC"); } function quiz_get_user_attempts_string($quiz, $attempts, $bestgrade) { /// Returns a simple little comma-separated list of all attempts, /// with each grade linked to the feedback report and with the best grade highlighted $bestgrade = format_float($bestgrade,$quiz->decimalpoints); foreach ($attempts as $attempt) { $attemptgrade = format_float(($attempt->sumgrades / $quiz->sumgrades) * $quiz->grade, $quiz->decimalpoints); if ($attemptgrade == $bestgrade) { $userattempts[] = "id&attempt=$attempt->id\">$attemptgrade"; } else { $userattempts[] = "id&attempt=$attempt->id\">$attemptgrade"; } } return implode(",", $userattempts); } function quiz_get_best_grade($quiz, $userid) { /// Get the best current grade for a particular user in a quiz if (!$grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) { return NULL; } return (format_float($grade->grade,$quiz->decimalpoints)); } function quiz_save_best_grade($quiz, $userid) { /// Calculates the best grade out of all attempts at a quiz for a user, /// and then saves that grade in the quiz_grades table. if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) { notify('Could not find any user attempts'); return false; } $bestgrade = quiz_calculate_best_grade($quiz, $attempts); $bestgrade = (($bestgrade / $quiz->sumgrades) * $quiz->grade); if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) { $grade->grade = round($bestgrade, $quiz->decimalpoints); $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 = round($bestgrade, $quiz->decimalpoints); $grade->timemodified = time(); if (!insert_record('quiz_grades', $grade)) { notify('Could not insert new best grade'); return false; } } return true; } function quiz_calculate_best_grade($quiz, $attempts) { /// Calculate the best grade for a quiz given a number of attempts by a particular user. switch ($quiz->grademethod) { case ATTEMPTFIRST: foreach ($attempts as $attempt) { return $attempt->sumgrades; } break; case ATTEMPTLAST: foreach ($attempts as $attempt) { $final = $attempt->sumgrades; } return $final; case GRADEAVERAGE: $sum = 0; $count = 0; foreach ($attempts as $attempt) { $sum += $attempt->sumgrades; $count++; } return (float)$sum/$count; default: case GRADEHIGHEST: $max = 0; foreach ($attempts as $attempt) { if ($attempt->sumgrades > $max) { $max = $attempt->sumgrades; } } return $max; } } function quiz_calculate_best_attempt($quiz, $attempts) { /// Return the attempt with the best grade for a quiz switch ($quiz->grademethod) { case ATTEMPTFIRST: foreach ($attempts as $attempt) { return $attempt; } break; case GRADEAVERAGE: // need to do something with it :-) case ATTEMPTLAST: foreach ($attempts as $attempt) { $final = $attempt; } return $final; default: case GRADEHIGHEST: $max = -1; foreach ($attempts as $attempt) { if ($attempt->sumgrades > $max) { $max = $attempt->sumgrades; $maxattempt = $attempt; } } return $maxattempt; } } function quiz_save_attempt($quiz, $questions, $result, $attemptnum, $finished = true) { /// Given a quiz, a list of attempted questions and a total grade /// this function saves EVERYTHING so it can be reconstructed later /// if necessary. global $USER; global $QUIZ_QTYPES; // First find the attempt in the database (start of attempt) if (!$attempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) { notify("Trying to save an attempt that was not started!"); return false; } // Not usually necessary, but there's some sort of very rare glitch // I've seen where the number wasn't already the same. In these cases // We upgrade the database to match the attemptnum we calculated $attempt->attempt = $attemptnum; // Now let's complete this record and save it $attempt->sumgrades = $result->sumgrades; if ($finished) { $attempt->timefinish = time(); } $attempt->timemodified = time(); if (!update_record("quiz_attempts", $attempt)) { notify("Error while saving attempt"); return false; } // Now let's save all the questions for this attempt foreach ($questions as $question) { // Fetch the response record for this question... $response = get_record('quiz_responses', 'attempt', $attempt->id, 'question', $question->id); $response->grade = $result->details[$question->id]->grade; if (!empty($question->response)) { $responseanswerfield = $QUIZ_QTYPES[$question->qtype] ->convert_to_response_answer_field($question->response); $response->answer = $responseanswerfield; } else if (!isset($response->answer)) { $response->answer = ''; } if (!update_record("quiz_responses", $response)) { notify("Error while saving response"); return false; } } return $attempt; } function quiz_extract_correctanswers($answers, $nameprefix) { /// Convinience function that is used by some single-response /// question-types for determining correct answers. $bestanswerfraction = 0.0; $correctanswers = array(); foreach ($answers as $answer) { if ($answer->fraction > $bestanswerfraction) { $correctanswers = array($nameprefix.$answer->id => $answer); $bestanswerfraction = $answer->fraction; } else if ($answer->fraction == $bestanswerfraction) { $correctanswers[$nameprefix.$answer->id] = $answer; } } return $correctanswers; } function quiz_grade_responses($quiz, $questions, $attemptid=0) { /// Given a list of questions (including ->response[] and ->maxgrade /// on each question) this function does all the hard work of calculating the /// score for each question, as well as a total grade for /// the whole quiz. It returns everything in a structure /// that lookas like this /// ->sumgrades (sum of all grades for all questions) /// ->grade (final grade result for the whole quiz) /// ->percentage (Percentage of the max grade achieved) /// ->details[] /// The array ->details[] is indexed like the $questions argument /// and contains scoring information per question. Each element has /// this structure: /// []->grade (Grade awarded on the specific question) /// []->answers[] (result answer records for the question response(s)) /// []->correctanswers[] (answer records if question response(s) had been correct) /// - HOWEVER, ->answers[] and ->correctanswers[] are supplied only /// if there is a response on the question... /// The array ->answers[] is indexed like ->response[] on its corresponding /// element in $questions. It is the case for ->correctanswers[] when /// there can be multiple responses per question but if there can be only one /// response per question then all possible correctanswers will be /// represented, indexed like the response index concatenated with the ->id /// of its answer record. global $QUIZ_QTYPES; if (!$questions) { error("No questions!"); } $result->sumgrades = 0.0; foreach ($questions as $qid => $question) { if (!isset($question->response) && $attemptid) { /// No response on the question /// This case is common if the quiz shows a limited /// number of questions per page. $response = get_record('quiz_responses', 'attempt', $attemptid, 'question', $qid); $resultdetails->grade = $response->grade; } else if (empty($question->qtype)) { continue; } else { $resultdetails = $QUIZ_QTYPES[$question->qtype]->grade_response ($question, quiz_qtype_nameprefix($question)); // Negative grades will not do: if (((float)($resultdetails->grade)) <= 0.0) { $resultdetails->grade = 0.0; // Neither will extra credit: } else if (((float)($resultdetails->grade)) >= 1.0) { $resultdetails->grade = $question->maxgrade; } else { $resultdetails->grade *= $question->maxgrade; } } // if time limit is enabled and exceeded, return zero grades if ($quiz->timelimit > 0) { if (($quiz->timelimit + 60) <= $quiz->timesincestart) { $resultdetails->grade = 0; } } $result->sumgrades += $resultdetails->grade; $resultdetails->grade = round($resultdetails->grade, 2); $result->details[$qid] = $resultdetails; } $fraction = (float)($result->sumgrades / $quiz->sumgrades); $result->percentage = format_float($fraction * 100.0); $result->grade = format_float($fraction * $quiz->grade); $result->sumgrades = round($result->sumgrades, 2); return $result; } // this function creates default export filename function default_export_filename($course,$category) { //Take off some characters in the filename !! $takeoff = array(" ", ":", "/", "\\", "|"); $export_word = str_replace($takeoff,"_",strtolower(get_string("exportfilename","quiz"))); //If non-translated, use "export" if (substr($export_word,0,1) == "[") { $export_word= "export"; } //Calculate the date format string $export_date_format = str_replace(" ","_",get_string("exportnameformat","quiz")); //If non-translated, use "%Y%m%d-%H%M" if (substr($export_date_format,0,1) == "[") { $export_date_format = "%%Y%%m%%d-%%H%%M"; } //Calculate the shortname $export_shortname = clean_filename($course->shortname); if (empty($export_shortname) or $export_shortname == '_' ) { $export_shortname = $course->id; } //Calculate the category name $export_categoryname = clean_filename($category->name); //Calculate the final export filename //The export word $export_name = $export_word."-"; //The shortname $export_name .= strtolower($export_shortname)."-"; //The category name $export_name .= strtolower($export_categoryname)."-"; //The date format $export_name .= userdate(time(),$export_date_format,99,false); //The extension - no extension, supplied by format // $export_name .= ".txt"; return $export_name; } // function to read all questions for category into big array // added by Howard Miller June 2004 function get_questions_category( $category ) { // questions will be added to an array $qresults = array(); // get the list of questions for the category if ($questions = get_records("quiz_questions","category",$category->id)) { // iterate through questions, getting stuff we need foreach($questions as $question) { $new_question = get_question_data( $question ); $qresults[] = $new_question; } } return $qresults; } // function to read single question, parameter is object view of // quiz_categories record, results is a combined object // defined as follows... // ->id quiz_questions id // ->category category // ->name q name // ->questiontext // ->image // ->qtype see defines at the top of this file // ->stamp not too sure // ->version not sure // ----SHORTANSWER // ->usecase // ->answers array of answers // ----TRUEFALSE // ->trueanswer truefalse answer // ->falseanswer truefalse answer // ----MULTICHOICE // ->layout // ->single many or just one correct answer // ->answers array of answer objects // ----NUMERIC // ->min minimum answer span // ->max maximum answer span // ->answer single answer // ----MATCH // ->subquestions array of sub questions // ---->questiontext // ---->answertext function get_question_data( $question ) { // what to do next depends of question type (qtype) switch ($question->qtype) { case SHORTANSWER: $shortanswer = get_record("quiz_shortanswer","question",$question->id); $question->usecase = $shortanswer->usecase; $question->answers = get_exp_answers( $question->id ); break; case TRUEFALSE: if (!$truefalse = get_record("quiz_truefalse","question",$question->id)) { error( "quiz_truefalse record $question->id not found" ); } $question->trueanswer = get_exp_answer( $truefalse->trueanswer ); $question->falseanswer = get_exp_answer( $truefalse->falseanswer ); break; case MULTICHOICE: if (!$multichoice = get_record("quiz_multichoice","question",$question->id)) { error( "quiz_multichoice $question->id not found" ); } $question->layout = $multichoice->layout; $question->single = $multichoice->single; $question->answers = get_exp_answers( $multichoice->question ); break; case NUMERICAL: if (!$numeric = get_record("quiz_numerical","question",$question->id)) { error( "quiz_numerical $question->id not found" ); } $question->min = $numeric->min; $question->max = $numeric->max; $question->answer = get_exp_answer( $numeric->answer ); break; case MATCH: if (!$subquestions = get_records("quiz_match_sub","question",$question->id)) { error( "quiz_match_sub $question->id not found" ); } $question->subquestions = $subquestions; break; case DESCRIPTION: // nothing to do break; case MULTIANSWER: // nothing to do break; default: notify("No handler for question type $question->qtype in get_question"); } return $question; } // function to return single answer // ->id answer id // ->question question number // ->answer // ->fraction // ->feedback function get_exp_answer( $id ) { if (!$answer = get_record("quiz_answers","id",$id )) { error( "quiz_answers record $id not found" ); } return $answer; } // function to return array of answers for export function get_exp_answers( $question_num ) { if (!$answers = get_records("quiz_answers","question",$question_num)) { error( "quiz_answers question $question_num not found" ); } return $answers; } function quiz_categorylist($categoryid) { // returns a comma separated list of ids of the category and all subcategories $categorylist = $categoryid; if ($subcategories = get_records('quiz_categories', 'parent', $categoryid, 'sortorder ASC', 'id, id')) { foreach ($subcategories as $subcategory) { $categorylist .= ','. quiz_categorylist($subcategory->id); } } return $categorylist; } // function to determine where question is in use function quizzes_question_used( $id, $published=false, $courseid=0 ) { // $id = question id // $published = is category published // $courseid = course id, required only if $published=true // returns array of names of quizzes it appears in if ($published) { $quizzes = get_records("quiz"); } else { $quizzes = get_records("quiz","course",$courseid); } $beingused = array(); if ($quizzes) { foreach ($quizzes as $quiz) { $questions = explode(',', $quiz->questions); foreach ($questions as $question) { if ($question==$id) { $beingused[] = $quiz->name; } } } } return $beingused; } ?>