From: moodler Date: Wed, 21 Jul 2004 13:01:08 +0000 (+0000) Subject: Quiz refactoring is now merged into the main development code! X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=8966a111310d0cc1c7012d68bcd137a5bafdf11f;p=moodle.git Quiz refactoring is now merged into the main development code! I've done some basic testing using existing quizzes trying as much as I could think of, and it all worked well! Well done, Henrik! Great job! Still, this should be regarded as unstable at least until more people have had a try. Please test this thing solidly on new and old quizzes. --- diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index 3bd337eab3..a93239ce35 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -117,8 +117,8 @@ $timelimit = $quiz->timelimit * 60; + $unattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id); if($timelimit > 0) { - $unattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id); $timestart = $unattempt->timestart; if($timestart) { $timesincestart = time() - $timestart; @@ -153,28 +153,16 @@ error("No questions found!"); } - foreach ($rawanswers as $key => $value) { // Parse input for question -> answers + foreach ($rawanswers as $key => $value) { // Parse input for question->response - if (ereg('^q([0-9]+)$', $key, $keyregs)) { // It's a real question number, not a coded one - $questions[$keyregs[1]]->answer[] = trim($value); - - } else if (ereg('^q([0-9]+)rq([0-9]+)$', $key, $keyregs)) { // Random Question information - $questions[$keyregs[1]]->random = $keyregs[2]; - - } else if (ereg('^q([0-9]+)a([0-9]+)$', $key, $keyregs)) { // Checkbox style multiple answers - $questions[$keyregs[1]]->answer[] = $keyregs[2]; - - } else if (ereg('^q([0-9]+)r([0-9]+)$', $key, $keyregs)) { // Random-style answers - $questions[$keyregs[1]]->answer[] = "$keyregs[2]-$value"; - - } else if (ereg('^q([0-9]+)ma([0-9]+)$', $key, $keyregs)) { // Multi-answer questions - $questions[$keyregs[1]]->answer[] = "$keyregs[2]-$value"; + if ($postedquestionid = quiz_extract_posted_id($key)) { + $questions[$postedquestionid]->response[$key] = trim($value); } else if ('shuffleorder' == $key) { $shuffleorder = explode(",", $value); // Actual order questions were given in } else { // Useful for debugging new question types. Must be last. - error("Answer received for non-existent question ($key -> $value)"); + error("Unrecognizable input has been posted ($key -> $value)"); } } @@ -184,7 +172,15 @@ } } - if (!$result = quiz_grade_attempt_results($quiz, $questions)) { + /// Retrieve ->maxgrade for all questions + If (!($grades = quiz_get_question_grades($quiz->id, $quiz->questions))) { + $grades = array(); + } + foreach ($grades as $qid => $grade) { + $questions[$qid]->maxgrade = $grade->grade; + } + + if (!$result = quiz_grade_responses($quiz, $questions)) { error("Could not grade your quiz attempt!"); } @@ -213,7 +209,7 @@ if ($quiz->feedback) { $quiz->shuffleanswers = false; // Never shuffle answers in feedback - quiz_print_quiz_questions($quiz, $result, $questions, $shuffleorder); + quiz_print_quiz_questions($quiz, $questions, $result, $shuffleorder); print_continue("view.php?id=$cm->id"); } @@ -233,13 +229,14 @@ /// Actually seeing the questions marks the start of an attempt - if (!$unfinished = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) { - if ($newattemptid = quiz_start_attempt($quiz->id, $USER->id, $attemptnumber)) { - add_to_log($course->id, "quiz", "attempt", - "review.php?id=$cm->id&attempt=$newattemptid", "$quiz->id", $cm->id); - } else { - error("Sorry! Could not start the quiz (could not save starting time)"); - } + if (isset($unattempt) && $unattempt) { + $attempt = $unattempt; + + } else if ($attempt = quiz_start_attempt($quiz->id, $USER->id, $attemptnumber)) { + add_to_log($course->id, "quiz", "attempt", + "review.php?id=$cm->id&attempt=$attempt->id", "$quiz->id", $cm->id); + } else { + error("Sorry! Could not start the quiz (could not save starting time)"); } /// First print the headings and so on @@ -266,58 +263,32 @@ echo "
"; - $result = NULL; // Default - $questions = NULL; // Default - if ($quiz->attemptonlast && !empty($attempts)) { - $latestfinishedattempt->attempt = 0; - foreach ($attempts as $attempt) { - if ($attempt->timefinish - && $attempt->attempt > $latestfinishedattempt->attempt) - { - $latestfinishedattempt = $attempt; - } - } - if ($latestfinishedattempt->attempt > 0 - and $questions = - quiz_get_attempt_responses($latestfinishedattempt)) - { - // An previous attempt to continue on is found: - quiz_remove_unwanted_questions($questions, $quiz); // In case the quiz has been changed - - if (!($result = quiz_grade_attempt_results($quiz, $questions))) { - // No results, reset to defaults: - $questions = NULL; - $result = NULL; - - } else { - // We're on, latest attempt responses are to be included. - // In order to have this accomplished by - // the method quiz_print_quiz_questions we need to - // temporarilly change some of the $quiz attributes - // and remove some of the information from result. - - $quiz->correctanswers = false; // Not a good idea to show them, huh? - $result->feedback = array(); // Not to be printed - $result->attemptbuildsonthelast = true; - } - - } else { - // No latest attempt, or latest attempt was empty - Reset to defaults - $questions = NULL; - } + $questions = quiz_get_attempt_questions($quiz, $attempt, true); + if ($quiz->attemptonlast && $attemptnumber >= 2 and + $quiz->attempts == 0 || !unattempt) { + // There are unlimited attempts or it is a new attempt. + // As the attempt also builds on the last, we can here + // have the student see the scores of the pre-entered + // responses that we here will have graded: + $result = quiz_grade_responses($quiz, $questions); + $result->attemptbuildsonthelast = true; + } else { + $result = NULL; } - if (! quiz_print_quiz_questions($quiz, $result, $questions)) { + + // We do not show feedback or correct answers during an attempt: + $quiz->feedback = $quiz->correctanswers = false; + + if (!quiz_print_quiz_questions($quiz, $questions, $result)) { print_continue("view.php?id=$cm->id"); } -/// BEGIN EDIT if quiz is available and time limit is set -/// include floating timer. +/// If quiz is available and time limit is set include floating timer. - if($available and $timelimit > 0) { + if ($available and $timelimit > 0) { require('jstimer.php'); } -/// END EDIT -/// Finish the page + print_footer($course); ?> diff --git a/mod/quiz/format.php b/mod/quiz/format.php index b9e542a247..bd9339ea73 100644 --- a/mod/quiz/format.php +++ b/mod/quiz/format.php @@ -59,7 +59,9 @@ class quiz_default_format { // Now to save all the answers and type-specific options - $result = quiz_save_question_options($question); + global $QUIZ_QTYPES; + $result = $QUIZ_QTYPES[$question->qtype] + ->save_question_options($question); if (!empty($result->error)) { notify($result->error); diff --git a/mod/quiz/format/multianswer/format.php b/mod/quiz/format/multianswer/format.php index 2fa622e566..6ac834001f 100644 --- a/mod/quiz/format/multianswer/format.php +++ b/mod/quiz/format/multianswer/format.php @@ -9,135 +9,6 @@ // Based on format.php, included by ../../import.php - // REGULAR EXPRESSION CONSTANTS - // I do not know any way to make this easier - // Regexes are always awkard when defined but more comprehensible - // when used as constants in the executive code - -// ANSWER_ALTERNATIVE regexes - -define("ANSWER_ALTERNATIVE_FRACTION_REGEX", - '=|%(-?[0-9]+)%'); -define("ANSWER_ALTERNATIVE_ANSWER_REGEX", - '[^~#}]+'); -define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX", - '[^~}]*'); -define("ANSWER_ALTERNATIVE_REGEX", - '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' - . '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' - . '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?'); - -// Parenthesis positions for ANSWER_ALTERNATIVE_REGEX -define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2); -define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1); -define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3); -define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5); - -// NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used -// for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER -define("NUMBER_REGEX", - '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)'); -define("NUMERICAL_ALTERNATIVE_REGEX", - '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$'); - -// Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX -define("NUMERICAL_CORRECT_ANSWER", 1); -define("NUMERICAL_ABS_ERROR_MARGIN", 6); - -// Remaining ANSWER regexes -define("ANSWER_TYPE_DEF_REGEX", - '(NUMERICAL|NM)|(MULTICHOICE|MC)|(SHORTANSWER|SA|MW)'); -define("ANSWER_START_REGEX", - '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):'); - -define("ANSWER_REGEX", - ANSWER_START_REGEX - . '(' . ANSWER_ALTERNATIVE_REGEX - . '(~' - . ANSWER_ALTERNATIVE_REGEX - . ')*)}' ); - -// Parenthesis positions for singulars in ANSWER_REGEX -define("ANSWER_REGEX_NORM", 1); -define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3); -define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4); -define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 5); -define("ANSWER_REGEX_ALTERNATIVES", 6); - - -function extractMultiAnswerQuestion($text) { - $question = NULL; - $question->qtype= MULTIANSWER; - $question->questiontext= $text; - $question->answers= array(); - $question->defaultgrade = 0; // Will be increased for each answer norm - - for ($positionkey=1 - ; ereg(ANSWER_REGEX, $question->questiontext, $answerregs) - ; ++$positionkey ) - { - unset($multianswer); - - $multianswer->positionkey = $positionkey; - $multianswer->norm = $answerregs[ANSWER_REGEX_NORM] - or $multianswer->norm = '1'; - if ($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL]) { - $multianswer->answertype = NUMERICAL; - } else if($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER]) { - $multianswer->answertype = SHORTANSWER; - } else if($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE]){ - $multianswer->answertype = MULTICHOICE; - } else { - error("Cannot identify answertype $answerregs[2]"); - return false; - } - - $multianswer->alternatives= array(); - $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES]; - while (ereg(ANSWER_ALTERNATIVE_REGEX, $remainingalts, $altregs)) { - unset($alternative); - - if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) { - $alternative->fraction = '1'; - } else { - $alternative->fraction = .01 * - $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION] - or $alternative->fraction = '0'; - } - $alternative->feedback = $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK]; - if ($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL] - && ereg(NUMERICAL_ALTERNATIVE_REGEX, - $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], - $numregs) ) - { - $alternative->answer = $numregs[NUMERICAL_CORRECT_ANSWER]; - if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) { - $alternative->min = $numregs[NUMERICAL_CORRECT_ANSWER] - - $numregs[NUMERICAL_ABS_ERROR_MARGIN]; - $alternative->max = $numregs[NUMERICAL_CORRECT_ANSWER] - + $numregs[NUMERICAL_ABS_ERROR_MARGIN]; - } else { - $alternative->min = $numregs[NUMERICAL_CORRECT_ANSWER]; - $alternative->max = $numregs[NUMERICAL_CORRECT_ANSWER]; - } - } else { // Min and max must stay undefined... - $alternative->answer = - $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER]; - } - - $multianswer->alternatives[] = $alternative; - $tmp = explode($altregs[0], $remainingalts, 2); - $remainingalts = $tmp[1]; - } - - $question->defaultgrade += $multianswer->norm; - $question->answers[] = $multianswer; - $question->questiontext = implode("{#$positionkey}", - explode($answerregs[0], $question->questiontext, 2)); - } - return $question; -} - class quiz_file_format extends quiz_default_format { function readquestions($lines) { @@ -147,7 +18,9 @@ class quiz_file_format extends quiz_default_format { /// multianswer import $questions= array(); - $thequestion= extractMultiAnswerQuestion(addslashes(implode('',$lines))); + $thequestion= quiz_qtype_multianswer_extract_question + (addslashes(implode('',$lines))); + $thequestion->qtype = MULTIANSWER; if (!empty($thequestion)) { $thequestion->name = $lines[0]; diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 8e7ed1bf69..7e5c2ca9be 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -42,6 +42,258 @@ define("QUIZ_MAX_NUMBER_ANSWERS", "10"); define("QUIZ_MAX_EVENT_LENGTH", "432000"); // 5 days maximum +$QUIZ_QTYPES= array(); + +/// QUIZ_QTYPES INITIATION ////////////////// +class quiz_default_questiontype { + + function name() { + return 'default'; + } + + 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) { + // As this function uses formcheck, it can only be used by + // question.php + + // This default implementation is suitable for most + // question types. + + // First, save the basic question itself + + $question->name = $form->name; + $question->questiontext = $form->questiontext; + $question->questiontextformat = $form->questiontextformat; + + if (empty($form->image)) { + $question->image = ""; + } else { + $question->image = $form->image; + } + + if (isset($form->defaultgrade)) { + $question->defaultgrade = $form->defaultgrade; + } + + if ($err = formcheck($question)) { + notify(get_string("someerrorswerefound")); + + } else { + + 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, than 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 mcuh the inverse of convert_to_response_answer + /// This function and convert_to_response_answer, 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) { + + /// Print question number and grade: + + echo '

' . $number . '

'; + if (false !== $grade) { + $strmarks = get_string("marks", "quiz"); + echo '

'; + if (false !== $actualgrade) { + echo "$strmarks: $actualgrade/$grade

"; + } else { + echo "$grade $strmarks

"; + } + } + print_spacer(1,100); + + /// Print possible recently-added information: + + if ($recentlyadded) { + echo ''; + // Notify the user of this recently added question + echo ''; + echo get_string('recentlyaddedquestion', 'quiz'); + echo ''; + echo ''; + + } else { // The normal case + echo ''; + } + } + + function print_question($currentnumber, $quiz, $question, + $readonly, $resultdetails) { + /// Note that this method must return the number of the next + /// question, making it possible not to increase the number when + /// overriding this method (as for qtype=DESCRIPTION). + + echo ''; + echo '
'; + + $this->print_question_number_and_grading_details + ($currentnumber, + $quiz->grade ? $question->maxgrade : false, + empty($resultdetails) ? false : $resultdetails->grade, + $question->recentlyadded); + + $this->print_question_formulation_and_controls( + $question, $quiz, $readonly, $resultdetails->answers, + $resultdetails->correctanswers, + quiz_qtype_nameprefix($question)); + + echo "
"; + return $currentnumber + 1; + } + + function print_question_formulation_and_controls($question, + $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + /// This default implementation must be overridden by all + /// question type implemenations, unless the default + /// implementation of print_question has been overridden... + + notify('Error: Question formulation and input controls has not' + .' been implemented for question type '.$this->name()); + } + + function grade_response($question, $nameprefix) { + // Analyzes $question->response[] and determines the result + // The result is to be returned in this structure: + // ->grade (The fraction of maxgrade awarded on the question) + // ->answers (result answer records) + // ->correctanswers (potential answer records for best ->response[]) + + error('grade_response has not been implemented for question type ' + .$this->name()); + } +} + +quiz_load_questiontypes(); +function quiz_load_questiontypes() { + global $QUIZ_QTYPES; + global $CFG; + + $qtypenames= get_list_of_plugins('mod/quiz/questiontypes'); + foreach($qtypenames as $qtypename) { + // Instanciates all plug-in question types + $qtypefilepath= "questiontypes/$qtypename/questiontype.php"; + + // echo "Loading $qtypename
"; // Uncomment for debugging + if (is_readable($qtypefilepath)) { + require_once($qtypefilepath); + } + } +} + + /// FUNCTIONS /////////////////////////////////////////////////////////////////// function quiz_add_instance($quiz) { @@ -431,20 +683,6 @@ function quiz_get_question_grades($quizid, $questionlist) { AND question IN ($questionlist)"); } -function quiz_get_random_categories($questionlist) { -/// Given an array of questions, this function looks for random -/// questions among them and returns a list of categories with -/// an associated count of random questions for each. - - global $CFG; - - return get_records_sql_menu("SELECT category,count(*) - FROM {$CFG->prefix}quiz_questions - WHERE id IN ($questionlist) - AND qtype = '".RANDOM."' - GROUP BY category "); -} - function quiz_get_grade_records($quiz) { /// Gets all info required to display the table of quiz results /// for report.php @@ -457,126 +695,148 @@ function quiz_get_grade_records($quiz) { AND qg.userid = u.id"); } -function quiz_get_answers($question, $answerids=NULL) { +function quiz_get_answers($question) { // Given a question, returns the correct answers for a given question - global $CFG; - - if (empty($answerids)) { - $answeridconstraint = ''; - } else { - $answeridconstraint = " AND a.id IN ($answerids) "; - } - - switch ($question->qtype) { - case SHORTANSWER: // Could be multiple answers - return get_records_sql("SELECT a.*, sa.usecase - FROM {$CFG->prefix}quiz_shortanswer sa, - {$CFG->prefix}quiz_answers a - WHERE sa.question = '$question->id' - AND sa.question = a.question " - . $answeridconstraint); - - case TRUEFALSE: // Should be always two answers - return get_records("quiz_answers", "question", $question->id); - - case MULTICHOICE: // Should be multiple answers - return get_records_sql("SELECT a.*, mc.single - FROM {$CFG->prefix}quiz_multichoice mc, - {$CFG->prefix}quiz_answers a - WHERE mc.question = '$question->id' - AND mc.question = a.question " - . $answeridconstraint); - - case MATCH: - return get_records("quiz_match_sub", "question", $question->id); - - case RANDOMSAMATCH: // Could be any of many answers, return them all - return get_records_sql("SELECT a.* - FROM {$CFG->prefix}quiz_questions q, - {$CFG->prefix}quiz_answers a - WHERE q.category = '$question->category' - AND q.qtype = ".SHORTANSWER." - AND q.id = a.question "); - - case NUMERICAL: // Logical support for multiple answers - return get_records_sql("SELECT a.*, n.min, n.max - FROM {$CFG->prefix}quiz_numerical n, - {$CFG->prefix}quiz_answers a - WHERE a.question = '$question->id' - AND n.answer = a.id " - . $answeridconstraint); - - case DESCRIPTION: - return true; // there are no answers for description - - case RANDOM: - return quiz_get_answers - (get_record('quiz_questions', 'id', $question->random)); - - case MULTIANSWER: // Includes subanswers - $answers = array(); - - $virtualquestion->id = $question->id; - - if ($multianswers = get_records('quiz_multianswers', 'question', $question->id)) { - foreach ($multianswers as $multianswer) { - $virtualquestion->qtype = $multianswer->answertype; - // Recursive call for subanswers - $multianswer->subanswers = quiz_get_answers($virtualquestion, $multianswer->answers); - $answers[] = $multianswer; - } - } - return $answers; + global $QUIZ_QTYPES; - default: - return false; - } + return $QUIZ_QTYPES[$question->qtype]->get_answers($question); } - -function quiz_get_attempt_responses($attempt) { -// Given an attempt object, this function gets all the -// stored responses and returns them in a format suitable -// for regrading using quiz_grade_attempt_results() +function quiz_get_attempt_questions($quiz, $attempt, $attempting = false) { + /// Returns the questions of the quiz attempt at a format used for + /// grading and printing them... + /// On top of the ordinary persistent question fields, + /// this function also set these properties + + /// ->response - contains names (as keys) and values (as values) + /// for all question html-form inputs + /// ->recentlyadded - true only if the question has been added to the quiz + /// after the responses for the attempt were saved; + /// false otherwise + /// ->maxgrade - the max grade the question has on the quiz if grades + /// are used on the quiz; false otherwise + + global $QUIZ_QTYPES; global $CFG; - if (!$responses = get_records_sql("SELECT q.id, q.qtype, q.category, q.questiontext, - q.defaultgrade, q.image, r.answer - FROM {$CFG->prefix}quiz_responses r, - {$CFG->prefix}quiz_questions q - WHERE r.attempt = '$attempt->id' - AND q.id = r.question")) { - notify("Could not find any responses for that attempt!"); + /// Get the questions: + if (!($questions = + get_records_list('quiz_questions', 'id', $quiz->questions))) { + notify('Error when reading questions from the database!'); return false; } + /// Retrieve ->maxgrade for all questions + If (!($grades = quiz_get_question_grades($quiz->id, $quiz->questions))) { + $grades = array(); + } + + /// Get any existing responses on this attempt: + if (!($rawresponses = get_records_sql + ("SELECT question, answer, attempt FROM {$CFG->prefix}quiz_responses + WHERE attempt = '$attempt->id' + AND question IN ($quiz->questions)")) + and $quiz->attemptonlast + // Try to get responses from the previous attempt: + and $lastattemptnum = $attempt->attempt - 1) { + do { + $lastattempt = get_record('quiz_attempts', + 'quiz', $quiz->id, + 'userid', $attempt->userid, + 'attempt', $lastattemptnum); + } while(empty($lastattempt) && --$lastattemptnum); + + if (0 == $lastattemptnum or + !($rawresponses = get_records_sql + ("SELECT question, answer, attempt + FROM {$CFG->prefix}quiz_responses + WHERE attempt = '$lastattempt->id' + AND question IN ($quiz->questions)"))) { + $rawresponses = array(); + } else { + /// We found a last attempt that is now to be used: - foreach ($responses as $key => $response) { - if ($response->qtype == RANDOM) { - $responses[$key]->random = $response->answer; - $responses[$response->answer]->delete = true; + /// This line can be uncommented for debuging + // echo "Last attempt is $lastattempt->id with number $lastattemptnum"; + } + } - $realanswer = $responses[$response->answer]->answer; + /// Set the additional question properties + /// response, recentlyadded and grade + foreach ($questions as $qid => $question) { - if (is_array($realanswer)) { - $responses[$key]->answer = $realanswer; - } else { - $responses[$key]->answer = explode(",", $realanswer); - } + if (isset($grades[$qid])) { + $questions[$qid]->maxgrade = $grades[$qid]->grade; + } else { + $questions[$qid]->maxgrade = 0.0; + } - } else if ($response->qtype == NUMERICAL or $response->qtype == SHORTANSWER) { - $responses[$key]->answer = array($response->answer); + if (isset($rawresponses[$qid])) { + $questions[$qid]->response = $QUIZ_QTYPES[$question->qtype] + ->extract_response($rawresponses[$qid], + quiz_qtype_nameprefix($question)); + $questions[$qid]->recentlyadded = false; } else { - $responses[$key]->answer = explode(",",$response->answer); + $questions[$qid]->response = array(); + $questions[$qid]->recentlyadded = !empty($rawresponses); } } - foreach ($responses as $key => $response) { - if (!empty($response->delete)) { - unset($responses[$key]); + + if ($attempting) { + /// Questions are requested for a test attempt that is + /// about to start and there are no responses to reuse + /// for current question, so we need to create new ones... + + /// For the case of wrapping question types that can + /// wrap other arbitrary questions, there is a need + /// to make sure that no question will appear twice + /// in the quiz attempt: + + $questionsinuse = $quiz->questions; + foreach ($questions as $question) { + if ($wrapped = $QUIZ_QTYPES[$question->qtype]->wrapped_questions + ($question, quiz_qtype_nameprefix($question))) { + $questionsinuse .= ",$wrapped"; + } + } + + /// Make sure all the questions will have responses: + foreach ($questions as $question) { + if (empty($question->response)) { + $nameprefix = quiz_qtype_nameprefix($question); + $questions[$question->id]->response = + $QUIZ_QTYPES[$question->qtype]->create_response + ($question, $nameprefix, $questionsinuse); + + ////////////////////////////////////////////////// + // In the future, a nice feature could be to save + // the created response right here, so that if a + // student quits the quiz without saving, the + // student will have the oppertunity to go back + // to same quiz if he/she restarts the attempt. + // Today, the student gets new RANDOM questions + // whenever he/she restarts the quiz attempt. + ////////////////////////////////////////////////// + // The above would also open the door for a new + // quiz feature that allows the student to save + // all responses if he/she needs to switch computer + // or have any other break in the middle of the quiz. + // (Or simply because the student feels more secure + // if he/she has the chance to save the responses + // a number of times during the quiz.) + ////////////////////////////////////////////////// + + /// Catch any additional wrapped questions: + if ($wrapped = $QUIZ_QTYPES[$question->qtype] + ->wrapped_questions($questions[$question->id], + $nameprefix)) { + $questionsinuse .= ",$wrapped"; + } + } } } - return $responses; + return $questions; } @@ -596,6 +856,18 @@ function get_list_of_questions($questionlist) { /// Any other quiz functions go here. Each of them must have a name that /// starts with quiz_ +function quiz_qtype_nameprefix($question, $prefixstart='question') { + global $QUIZ_QTYPES; + return $prefixstart.$question->id.$QUIZ_QTYPES[$question->qtype]->name(); +} +function quiz_extract_posted_id($name, $nameprefix='question') { + if (ereg("^$nameprefix([0-9]+)", $name, $regs)) { + return $regs[1]; + } else { + return false; + } +} + function quiz_print_comment($text) { global $THEME; @@ -612,46 +884,21 @@ 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_QUESTION_TYPE[$question->qtype]."\">"; - } - switch ($question->qtype) { - case SHORTANSWER: - echo ''; - break; - case TRUEFALSE: - echo ''; - break; - case MULTICHOICE: - echo ''; - break; - case RANDOM: - echo ''; - break; - case MATCH: - echo ''; - break; - case RANDOMSAMATCH: - echo ''; - break; - case DESCRIPTION: - echo ''; - break; - case NUMERICAL: - echo ''; - break; - case MULTIANSWER: - echo ''; - break; + echo "id\" title=\"" + .$QUIZ_QUESTION_TYPE[$question->qtype]."\">"; } + echo ''; if ($editlink) { echo "\n"; } } function quiz_print_possible_question_image($quizid, $question) { -// Includes the question image is there is one +// Includes the question image if there is one global $CFG; @@ -672,445 +919,17 @@ function quiz_print_possible_question_image($quizid, $question) { } } -function quiz_print_question($number, $question, $grade, $quizid, - $feedback=NULL, $response=NULL, $actualgrade=NULL, $correct=NULL, - $realquestion=NULL, $shuffleanswers=false, $showgrades=true, $courseid=0) { - -/// Prints a quiz question, any format -/// $question is provided as an object - - global $CFG, $THEME; - - $question->questiontextformat = isset($question->questiontextformat) ? $question->questiontextformat : NULL; - - if ($question->qtype == DESCRIPTION) { // Special case question - has no answers etc - echo '

'; - echo format_text($question->questiontext, $question->questiontextformat, NULL, $courseid); - quiz_print_possible_question_image($quizid, $question); - echo '

'; - return true; - } - - if (empty($actualgrade)) { - $actualgrade = 0; - } - - $stranswer = get_string("answer", "quiz"); - $strmarks = get_string("marks", "quiz"); - - echo ''; - echo '
'; - echo '

' . $number . '

'; - if ($showgrades) { - if ($feedback or $response) { - echo "

$strmarks: $actualgrade/$grade

"; - } else { - echo "

$grade $strmarks

"; - } - } - print_spacer(1,100); - - if (isset($question->recentlyadded) and $question->recentlyadded) { - echo '
'; - // Notify the user of this recently added question - echo ''; - echo get_string('recentlyaddedquestion', 'quiz'); - echo ''; - echo '
'; - - } else { // The normal case - echo ''; - } - - - if (empty($realquestion)) { - $realquestion->id = $question->id; - } else { // Add a marker to connect this question to the actual random parent - echo "id}rq$question->id\" value=\"x\" />\n"; - } - - switch ($question->qtype) { - - case SHORTANSWER: - case NUMERICAL: - echo format_text($question->questiontext, $question->questiontextformat, NULL, $courseid); - quiz_print_possible_question_image($quizid, $question); - if ($response) { - $value = 'value="'.s($response[0]).'"'; - } else { - $value = ''; - } - echo "

$stranswer: id\" size=\"80\" $value />

"; - if ($feedback) { - quiz_print_comment("

$feedback[0]

"); - } - if ($correct) { - $correctanswers = implode(", ", $correct); - quiz_print_correctanswer($correctanswers); - } - break; - - case TRUEFALSE: - if (!$options = get_record("quiz_truefalse", "question", $question->id)) { - notify("Error: Missing question options!"); - } - if (!$true = get_record("quiz_answers", "id", $options->trueanswer)) { - notify("Error: Missing question answers!"); - } - if (!$false = get_record("quiz_answers", "id", $options->falseanswer)) { - notify("Error: Missing question answers!"); - } - if (!$true->answer) { - $true->answer = get_string("true", "quiz"); - } - if (!$false->answer) { - $false->answer = get_string("false", "quiz"); - } - echo format_text($question->questiontext, $question->questiontextformat, NULL, $courseid); - quiz_print_possible_question_image($quizid, $question); - - $truechecked = ""; - $falsechecked = ""; - - if (!empty($response[$true->id])) { - $truechecked = 'checked="checked"'; - $feedbackid = $true->id; - } else if (!empty($response[$false->id])) { - $falsechecked = 'checked="checked"'; - $feedbackid = $false->id; - } - - $truecorrect = ""; - $falsecorrect = ""; - if ($correct) { - if (!empty($correct[$true->id])) { - $truecorrect = 'class="highlight"'; - } - if (!empty($correct[$false->id])) { - $falsecorrect = 'class="highlight"'; - } - } - echo "
$stranswer:  "; - echo ""; - echo "id\" value=\"$true->id\" />$true->answer"; - echo ""; - echo "id\" value=\"$false->id\" />$false->answer"; - echo "

";// changed from CLEAR=ALL jm - if ($feedback) { - quiz_print_comment("

$feedback[$feedbackid]

"); - } - - break; - - case MULTICHOICE: - if (!$options = get_record("quiz_multichoice", "question", $question->id)) { - notify("Error: Missing question options!"); - } - if (!$answers = get_records_list("quiz_answers", "id", $options->answers)) { - notify("Error: Missing question answers!"); - } - echo format_text($question->questiontext, $question->questiontextformat, NULL, $courseid); - quiz_print_possible_question_image($quizid, $question); - echo ""; - echo "
$stranswer:  "; - echo ""; - $answerids = explode(",", $options->answers); - - if ($shuffleanswers) { - $answerids = swapshuffle($answerids); - } - - foreach ($answerids as $key => $answerid) { - $answer = $answers[$answerid]; - $qnumchar = chr(ord('a') + $key); - - if (empty($response[$answerid])) { - $checked = ""; - } else { - $checked = 'checked="checked"'; - } - echo '"; - if (empty($feedback) or empty($correct[$answer->id])) { - echo ''; - } else { - echo ''; - } - if (!empty($feedback)) { - echo ""; - } - echo ""; - } - echo "
'; - if ($options->single) { - echo "id\" value=\"$answer->id\" />"; - } else { - echo "id"."a$answer->id\" value=\"$answer->id\" />"; - } - echo "'.format_text("$qnumchar. $answer->answer").''.format_text("$qnumchar. $answer->answer").' "; - if (!empty($response[$answerid])) { - quiz_print_comment($feedback[$answerid]); - } - echo "
"; - echo "
"; - break; - - case MATCH: - if (!$options = get_record("quiz_match", "question", $question->id)) { - notify("Error: Missing question options!"); - } - if (!$subquestions = get_records_list("quiz_match_sub", "id", $options->subquestions)) { - notify("Error: Missing subquestions for this question!"); - } - if (!empty($question->questiontext)) { - echo format_text($question->questiontext, $question->questiontextformat, NULL, $courseid); - } - quiz_print_possible_question_image($quizid, $question); - - if ($shuffleanswers) { - $subquestions = draw_rand_array($subquestions, count($subquestions)); - } - foreach ($subquestions as $subquestion) { - $answers[$subquestion->id] = $subquestion->answertext; - } - - $answers = draw_rand_array($answers, count($answers)); - - echo ''; - foreach ($subquestions as $key => $subquestion) { - echo ''; - if (empty($response)) { - echo ''; - } - echo '
'; - echo $subquestion->questiontext; - echo ''; - choose_from_menu($answers, "q$realquestion->id"."r$subquestion->id"); - } else { - if (empty($response[$key])) { - echo ''; - choose_from_menu($answers, "q$realquestion->id"."r$subquestion->id"); - } else { - if ($response[$key] == $correct[$key]) { - echo ''; - choose_from_menu($answers, "q$realquestion->id"."r$subquestion->id", $response[$key]); - } else { - echo ''; - choose_from_menu($answers, "q$realquestion->id"."r$subquestion->id", $response[$key]); - } - } - - if (!empty($feedback[$key])) { - quiz_print_comment($feedback[$key]); - } - } - echo '
'; - - break; - - case RANDOMSAMATCH: - if (!$options = get_record("quiz_randomsamatch", "question", $question->id)) { - notify("Error: Missing question options!"); - } - echo format_text($question->questiontext, $question->questiontextformat, NULL, $courseid); - quiz_print_possible_question_image($quizid, $question); - - /// First, get all the questions available - - $allquestions = get_records_select("quiz_questions", - "category = $question->category AND qtype = ".SHORTANSWER); - if (count($allquestions) < $options->choose) { - notify("Error: could not find enough Short Answer questions in the database!"); - notify("Found ".count($allquestions).", need $options->choose."); - break; - } - - if (empty($response)) { // Randomly pick the questions - if (!$randomquestions = draw_rand_array($allquestions, $options->choose)) { - notify("Error choosing $options->choose random questions"); - break; - } - } else { // Use existing questions - $randomquestions = array(); - foreach ($response as $key => $rrr) { - $rrr = explode("-", $rrr); - $randomquestions[$key] = $allquestions[$key]; - $responseanswer[$key] = $rrr[1]; - } - } - - /// For each selected, find the best matching answers - - foreach ($randomquestions as $randomquestion) { - $shortanswerquestion = get_record("quiz_shortanswer", "question", $randomquestion->id); - $questionanswers = get_records_list("quiz_answers", "id", $shortanswerquestion->answers); - $bestfraction = 0; - $bestanswer = NULL; - foreach ($questionanswers as $questionanswer) { - if ($questionanswer->fraction > $bestfraction) { - $bestanswer = $questionanswer; - $bestfraction = $questionanswer->fraction; - } - } - if (empty($bestanswer)) { - notify("Error: Could not find the best answer for question: ".$randomquestions->name); - break; - } - $randomanswers[$bestanswer->id] = trim($bestanswer->answer); - } - - if (!$randomanswers = draw_rand_array($randomanswers, $options->choose)) { // Mix them up - notify("Error randomising answers!"); - break; - } - - echo ''; - foreach ($randomquestions as $key => $randomquestion) { - echo ''; - echo ''; - } - echo '
'; - echo $randomquestion->questiontext; - echo ''; - if (empty($response)) { - choose_from_menu($randomanswers, "q$realquestion->id"."r$randomquestion->id"); - } else { - if (!empty($correct[$key])) { - if ($randomanswers[$responseanswer[$key]] == $correct[$key]) { - echo ''; - choose_from_menu($randomanswers, "q$realquestion->id"."r$randomquestion->id", $responseanswer[$key]); - echo '
'; - } else { - choose_from_menu($randomanswers, "q$realquestion->id"."r$randomquestion->id", $responseanswer[$key]); - quiz_print_correctanswer($correct[$key]); - } - } else { - choose_from_menu($randomanswers, "q$realquestion->id"."r$randomquestion->id", $responseanswer[$key]); - } - if (!empty($feedback[$key])) { - quiz_print_comment($feedback[$key]); - } - } - echo '
'; - break; - - case MULTIANSWER: - // For this question type, we better print the image on top: - quiz_print_possible_question_image($quizid, $question); - - $qtextremaining = format_text($question->questiontext, $question->questiontextformat, NULL, $courseid); - - // The regex will recognize text snippets of type {#X} - // where the X can be any text not containg } or white-space characters. - - $strfeedback = get_string('feedback', 'quiz'); - - while (ereg('\{#([^[:space:]}]*)}', $qtextremaining, $regs)) { - $qtextsplits = explode($regs[0], $qtextremaining, 2); - echo $qtextsplits[0]; - $qtextremaining = $qtextsplits[1]; - - $multianswer = get_record('quiz_multianswers', 'question', $question->id, 'positionkey', $regs[1]); - - $inputname= " name=\"q{$realquestion->id}ma$multianswer->id\" "; - - if (!empty($response) && ereg('(.[^-]*)-(.+)', array_shift($response), $responseitems)) { - $responsefractiongrade = (float)$responseitems[1]; - $actualresponse = $responseitems[2]; - - if (1.0 == $responsefractiongrade) { - $style = 'style="background-color:lime"'; - } else if (0.0 < $responsefractiongrade) { - $style = 'style="background-color:yellow"'; - } else if ('' != $actualresponse) { - // The response must have been totally wrong: - $style = 'style="background-color:red"'; - } else { - // There was no response given - $style = ''; - } - } else { - $responsefractiongrade = 0.0; - $actualresponse = ''; - $style = ''; - } - - $feedbackitem = ''; - switch ($multianswer->answertype) { - case SHORTANSWER: - case NUMERICAL: - if (isset($feedback[$regs[1]-1])) { - $title = str_replace("'", "\\'", $feedback[$regs[1]-1] ); - $popup = " onmouseover=\"return overlib('$title', CAPTION, '$strfeedback', FGCOLOR, '$THEME->cellcontent');\" ". - " onmouseout=\"return nd();\" "; - } else { - $popup = ''; - } - echo " "; - break; - case MULTICHOICE: - $outputoptions = ''; - $answers = get_records_list("quiz_answers", "id", $multianswer->answers); - $outputoptions .= ''; // Default empty option - foreach ($answers as $answer) { - if ($answer->id == $actualresponse) { - $selected = 'selected'; - $feedbackitem = $answer->feedback; - } else { - $selected = ''; - } - $outputoptions .= ""; - } - if ($feedbackitem) { - $title = str_replace("'", "\\'", $feedbackitem); - $popup = " onmouseover=\"return overlib('$title', CAPTION, '$strfeedback', FGCOLOR, '$THEME->cellcontent');\" ". - " onmouseout=\"return nd();\" "; - } else { - $popup = ''; - } - echo "'; - break; - default: - error("Unable to recognized answertype $answer->answertype"); - break; - } - } - - // Print the final piece of question text: - echo $qtextremaining; - break; - - case RANDOM: - // This can only happen if it is a recently added question - - echo '

' . get_string('random', 'quiz') . '

'; - break; - - default: - notify("Error: Unknown question type!"); - } - - echo "
"; -} - - - -function quiz_print_quiz_questions($quiz, $results=NULL, $questions=NULL, $shuffleorder=NULL) { +function quiz_print_quiz_questions($quiz, $questions, + $results=NULL, $shuffleorder=NULL) { // Prints a whole quiz on one page. - /// Get the questions + global $QUIZ_QTYPES; - if (!$questions) { - if (empty($quiz->questions)) { - notify("No questions have been defined!"); - return false; - } + /// Check arguments - if (!$questions = get_records_list("quiz_questions", "id", $quiz->questions, "")) { - notify("Error when reading questions from the database!"); - return false; - } + if (empty($questions)) { + notify("No questions have been defined!"); + return false; } if (!$shuffleorder) { @@ -1121,53 +940,11 @@ function quiz_print_quiz_questions($quiz, $results=NULL, $questions=NULL, $shuff } } - if ($shuffleorder) { // Order has been defined, so reorder questions + if ($shuffleorder) { // Order has been defined, so reorder questions $oldquestions = $questions; $questions = array(); foreach ($shuffleorder as $key) { - if (empty($oldquestions[$key])) { // Check for recently added questions - if ($recentlyaddedquestion = - get_record("quiz_questions", "id", $key)) { - $recentlyaddedquestion->recentlyadded = true; - $questions[] = $recentlyaddedquestion; - } - } else { - $questions[] = $oldquestions[$key]; // This loses the index key, but doesn't matter - } - } - } - - if (!$grades = get_records_list("quiz_question_grades", "question", $quiz->questions, "", "question,grade")) { - notify("No grades were found for these questions!"); - return false; - } - - - /// Examine the set of questions for random questions, and retrieve them - - if (empty($results)) { // Choose some new random questions - if ($randomcats = quiz_get_random_categories($quiz->questions)) { - foreach ($randomcats as $randomcat => $randomdraw) { - /// Get the appropriate amount of random questions from this category - if (!$catquestions[$randomcat] = quiz_choose_random_questions($randomcat, $randomdraw, $quiz->questions)) { - notify(get_string("toomanyrandom", "quiz", $randomcat)); - return false; - } - } - } - } else { // Get the previously chosen questions - $chosen = array(); - foreach ($questions as $question) { - if (isset($question->random)) { - $chosen[] = $question->random; - } - } - if ($chosen) { - $chosenlist = implode(",", $chosen); - if (!$chosen = get_records_list("quiz_questions", "id", $chosenlist, "")) { - notify("Error when reading questions from the database!"); - return false; - } + $questions[] = $oldquestions[$key]; // This loses the index key, but doesn't matter } } @@ -1196,62 +973,33 @@ function quiz_print_quiz_questions($quiz, $results=NULL, $questions=NULL, $shuff // END EDIT echo "id\" />\n"; - $count = 0; + // $count = 0; + $nextquestionnumber = 1; $questionorder = array(); - foreach ($questions as $question) { + // $readonly determines if it is an attempt or an review, + // The condition used here is unfortunatelly somewhat confusing... + $readonly = !empty($results) && !isset($results->attemptbuildsonthelast) + ? ' readonly="readonly" ' : ''; - if ($question->qtype != DESCRIPTION) { // Description questions are not counted - $count++; - } + foreach ($questions as $question) { $questionorder[] = $question->id; - $feedback = NULL; - $response = NULL; - $actualgrades = NULL; - $correct = NULL; - $randomquestion = NULL; - - if (empty($results)) { - if ($question->qtype == RANDOM ) { // Set up random questions - $randomquestion = $question; - $question = array_pop($catquestions[$randomquestion->category]); - $grades[$question->id]->grade = $grades[$randomquestion->id]->grade; - } + if ($results && isset($results->details[$question->id])) { + $details = $results->details[$question->id]; } else { - if (!empty($results->feedback[$question->id])) { - $feedback = $results->feedback[$question->id]; - } - if (!empty($results->response[$question->id])) { - $response = $results->response[$question->id]; - } - if (!empty($results->grades[$question->id])) { - $actualgrades = $results->grades[$question->id]; - } - if ($quiz->correctanswers) { - if (!empty($results->correct[$question->id])) { - $correct = $results->correct[$question->id]; - } - } - if (!empty($question->random)) { - $randomquestion = $question; - $question = $chosen[$question->random]; - $grades[$question->id]->grade = $grades[$randomquestion->id]->grade; - } + $details = false; } print_simple_box_start("center", "90%"); - quiz_print_question($count, $question, $grades[$question->id]->grade, $quiz->id, - $feedback, $response, $actualgrades, $correct, - $randomquestion, $quiz->shuffleanswers, $quiz->grade, $quiz->course); + $nextquestionnumber = $QUIZ_QTYPES[$question->qtype]->print_question + ($nextquestionnumber, $quiz, $question, $readonly, $details); print_simple_box_end(); echo "
"; } - $attemptbuildsonthelast = isset($results->attemptbuildsonthelast) ? $results->attemptbuildsonthelast : NULL; - - if (empty($results) || $attemptbuildsonthelast) { + if (empty($readonly)) { if (!empty($quiz->shufflequestions)) { // Things have been mixed up, so pass the question order $shuffleorder = implode(',', $questionorder); echo "\n"; @@ -1379,33 +1127,6 @@ function quiz_get_category_coursename($category) { return $cname; } -function quiz_choose_random_questions($category, $draws, $excluded=0) { -/// Given a question category and a number of draws, this function -/// creates a random subset of that size - returned as an array of questions - - if (!$pool = get_records_select_menu("quiz_questions", - "category = '$category' AND id NOT IN ($excluded) - AND qtype <> ".RANDOM." - AND qtype <> ".DESCRIPTION, - "", "id,qtype")) { - return false; - } - - $countpool = count($pool); - - if ($countpool == $draws) { - $chosen = $pool; - } else if ($countpool < $draws) { - return false; - } else { - $chosen = draw_rand_array($pool, $draws); - } - - $chosenlist = implode(",", array_keys($chosen)); - return get_records_list("quiz_questions", "id", $chosenlist); -} - - function quiz_get_all_question_grades($questionlist, $quizid) { // Given a list of question IDs, finds grades or invents them to // create an array of matching grades @@ -1694,8 +1415,8 @@ function quiz_start_attempt($quizid, $userid, $numattempt) { $attempt->timestart = time(); $attempt->timefinish = 0; $attempt->timemodified = time(); - - return insert_record("quiz_attempts", $attempt); + $attempt->id = insert_record("quiz_attempts", $attempt); + return $attempt; } function quiz_get_user_attempt_unfinished($quizid, $userid) { @@ -1845,6 +1566,7 @@ function quiz_save_attempt($quiz, $questions, $result, $attemptnum) { /// if necessary. global $USER; + global $QUIZ_QTYPES; // First find the attempt in the database (start of attempt) @@ -1873,29 +1595,27 @@ function quiz_save_attempt($quiz, $questions, $result, $attemptnum) { foreach ($questions as $question) { $response->attempt = $attempt->id; + $response->grade = $result->details[$question->id]->grade; $response->question = $question->id; - $response->grade = $result->grades[$question->id]; - - if (!empty($question->random)) { - // First save the response of the random question - // the answer is the id of the REAL response - $response->answer = $question->random; - if (!insert_record("quiz_responses", $response)) { - notify("Error while saving response"); - return false; - } - $response->question = $question->random; - } - if (!empty($question->answer)) { - if (is_array($question->answer)) - { - $response->answer = implode(",",$question->answer); - } - else - { - $response->answer=$question->answer; - } + if (!empty($question->response)) { + $response->answer = $QUIZ_QTYPES[$question->qtype] + ->convert_to_response_answer_field($question->response); + + /////////////////////////////////////////// + // WORKAROUND for question type RANDOM: + /////////////////////////////////////////// + if ($question->qtype == RANDOM and + ereg('^random([0-9]+)-(.*)$', $response->answer, $afields)) { + $response->answer = $afields[1]; + if (!insert_record("quiz_responses", $response)) { + notify("Error while saving response"); + return false; + } + $response->question = $response->answer; + $response->answer = $afields[2]; + } /// End of WORKAROUND ////////////////////// + } else { $response->answer = ""; } @@ -1907,284 +1627,80 @@ function quiz_save_attempt($quiz, $questions, $result, $attemptnum) { return $attempt; } -function quiz_grade_attempt_question_result($question, - $answers, - $gradecanbenegative= false) -{ - $grade = 0; // default - $correct = array(); - $feedback = array(); - $response = array(); - - switch ($question->qtype) { - case SHORTANSWER: - if ($question->answer) { - $question->answer = trim(stripslashes($question->answer[0])); - } else { - $question->answer = ""; - } - $response[0] = $question->answer; - $feedback[0] = ''; // Default - foreach ($answers as $answer) { // There might be multiple right answers - - $answer->answer = trim($answer->answer); // Just in case - - if ($answer->fraction >= 1.0) { - $correct[] = $answer->answer; - } - if (!$answer->usecase) { // Don't compare case - $answer->answer = strtolower($answer->answer); - $question->answer = strtolower($question->answer); - } - - $potentialgrade = (float)$answer->fraction * $question->grade; - - if ($potentialgrade >= $grade and (strpos(' '.$answer->answer, '*'))) { - $answer->answer = str_replace('\*','@@@@@@',$answer->answer); - $answer->answer = str_replace('*','.*',$answer->answer); - $answer->answer = str_replace('@@@@@@', '\*',$answer->answer); - $answer->answer = str_replace('+', '\+',$answer->answer); - if (eregi('^'.$answer->answer.'$', $question->answer)) { - $feedback[0] = $answer->feedback; - $grade = $potentialgrade; - } - - } else if ($answer->answer == $question->answer) { - $feedback[0] = $answer->feedback; - $grade = $potentialgrade; - } - } - - break; - - case NUMERICAL: - if ($question->answer) { - $question->answer = trim(stripslashes($question->answer[0])); - } else { - $question->answer = ""; - } - $response[0] = $question->answer; - $bestshortanswer = 0; - foreach ($answers as $answer) { // There might be multiple right answers - if ($answer->fraction > $bestshortanswer) { - $correct[$answer->id] = $answer->answer; - $bestshortanswer = $answer->fraction; - $feedback[0] = $answer->feedback; // Show feedback for best answer - } - if ('' != $question->answer // Must not be mixed up with zero! - && (float)$answer->fraction > (float)$grade // Do we need to bother? - and // and has lower procedence than && and ||. - strtolower($question->answer) == strtolower($answer->answer) - || '' != trim($answer->min) - && ((float)$question->answer >= (float)$answer->min) - && ((float)$question->answer <= (float)$answer->max)) - { - //$feedback[0] = $answer->feedback; No feedback was shown for wrong answers - $grade = (float)$answer->fraction; - } - } - $grade *= $question->grade; // Normalize to correct weight - break; - - case TRUEFALSE: - if ($question->answer) { - $question->answer = $question->answer[0]; - } else { - $question->answer = NULL; - } - foreach($answers as $answer) { // There should be two answers (true and false) - $feedback[$answer->id] = $answer->feedback; - if ($answer->fraction > 0) { - $correct[$answer->id] = true; - } - if ($question->answer == $answer->id) { - $grade = (float)$answer->fraction * $question->grade; - $response[$answer->id] = true; - } - } - break; - - - case MULTICHOICE: - foreach($answers as $answer) { // There will be multiple answers, perhaps more than one is right - $feedback[$answer->id] = $answer->feedback; - if ($answer->fraction > 0) { - $correct[$answer->id] = true; - } - if (!empty($question->answer)) { - foreach ($question->answer as $questionanswer) { - if ($questionanswer == $answer->id) { - $response[$answer->id] = true; - if ($answer->single) { - $grade = (float)$answer->fraction * $question->grade; - continue; - } else { - $grade += (float)$answer->fraction * $question->grade; - } - } - } - } - } - break; - - case MATCH: - $matchcount = $totalcount = 0; - - foreach ($question->answer as $questionanswer) { // Each answer is "subquestionid-answerid" - $totalcount++; - $qarr = explode('-', $questionanswer); // Extract subquestion/answer. - $subquestionid = $qarr[0]; - $subanswerid = $qarr[1]; - if ($subquestionid and $subanswerid and (($subquestionid == $subanswerid) or - ($answers[$subquestionid]->answertext == $answers[$subanswerid]->answertext))) { - // Either the ids match exactly, or the answertexts match exactly - // (in case two subquestions had the same answer) - $matchcount++; - $correct[$subquestionid] = true; - } else { - $correct[$subquestionid] = false; - } - $response[$subquestionid] = $subanswerid; - } - - $grade = $question->grade * $matchcount / $totalcount; - - break; - - case RANDOMSAMATCH: - $bestanswer = array(); - foreach ($answers as $answer) { // Loop through them all looking for correct answers - if (empty($bestanswer[$answer->question])) { - $bestanswer[$answer->question] = 0; - $correct[$answer->question] = ""; - } - if ($answer->fraction > $bestanswer[$answer->question]) { - $bestanswer[$answer->question] = $answer->fraction; - $correct[$answer->question] = $answer->answer; - } - } - $answerfraction = 1.0 / (float) count($question->answer); - foreach ($question->answer as $questionanswer) { // For each random answered question - $rqarr = explode('-', $questionanswer); // Extract question/answer. - $rquestion = $rqarr[0]; - $ranswer = $rqarr[1]; - $response[$rquestion] = $questionanswer; - if (isset($answers[$ranswer])) { // If the answer exists in the list - $answer = $answers[$ranswer]; - $feedback[$rquestion] = $answer->feedback; - if ($answer->question == $rquestion) { // Check that this answer matches the question - $grade += (float)$answer->fraction * $question->grade * $answerfraction; - } - } - } - break; - - case MULTIANSWER: - // Default setting that avoids a possible divide by zero: - $subquestion->grade = 1.0; - - foreach ($question->answer as $questionanswer) { - - // Resetting default values for subresult: - $subresult->grade = 0.0; - $subresult->correct = array(); - $subresult->feedback = array(); - - // Resetting subquestion responses: - $subquestion->answer = array(); - - $qarr = explode('-', $questionanswer, 2); - $subquestion->answer[] = $qarr[1]; // Always single answer for subquestions - foreach ($answers as $multianswer) { - if ($multianswer->id == $qarr[0]) { - $subquestion->qtype = $multianswer->answertype; - $subquestion->grade = $multianswer->norm; - $subresult = quiz_grade_attempt_question_result($subquestion, $multianswer->subanswers, true); - break; - } - } - - - // Summarize subquestion results: - $grade += $subresult->grade; - $feedback[] = $subresult->feedback[0]; - $correct[] = $subresult->correct[0]; - - // Each response instance also contains the partial - // fraction grade for the response: - $response[] = $subresult->grade/$subquestion->grade - . '-' . $subquestion->answer[0]; - } - // Normalize grade: - $grade *= $question->grade/($question->defaultgrade); - break; - - case DESCRIPTION: // Descriptions are not graded. - break; - - case RANDOM: // Returns a recursive call with the real question - $realquestion = get_record - ('quiz_questions', 'id', $question->random); - $realquestion->answer = $question->answer; - $realquestion->grade = $question->grade; - return quiz_grade_attempt_question_result($realquestion, $answers); +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; + } } - - $result->grade = - $gradecanbenegative ? $grade // Grade can be negative - : max(0.0, $grade); // Grade must not be negative - $result->correct = $correct; - $result->feedback = $feedback; - $result->response = $response; - return $result; + return $correctanswers; } -function quiz_grade_attempt_results($quiz, $questions) { -/// Given a list of questions (including answers for each one) -/// this function does all the hard work of calculating the -/// grades for each question, as well as a total grade for -/// for the whole quiz. It returns everything in a structure -/// that looks like: -/// $result->sumgrades (sum of all grades for all questions) -/// $result->percentage (Percentage of grades that were correct) -/// $result->grade (final grade result for the whole quiz) -/// $result->grades[] (array of grades, indexed by question id) -/// $result->response[] (array of response arrays, indexed by question id) -/// $result->feedback[] (array of feedback arrays, indexed by question id) -/// $result->correct[] (array of feedback arrays, indexed by question id) +function quiz_grade_responses($quiz, $questions) { +/// 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 +/// 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 specifik question) +/// []->answers[] (result answer records for the question response(s)) +/// []->correctanswers[] (answer records if question response(s) had been correct) +/// The array ->answers[] is indexed like ->respoonse[] 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 concatinated with the ->id +/// of its answer record. + + global $QUIZ_QTYPES; if (!$questions) { error("No questions!"); } - if (!$grades = get_records_menu("quiz_question_grades", "quiz", $quiz->id, "", "question,grade")) { - error("No grades defined for these quiz questions!"); - } + $result->sumgrades = 0.0; + foreach ($questions as $qid => $question) { - $result->sumgrades = 0; + $resultdetails = $QUIZ_QTYPES[$question->qtype]->grade_response + ($question, quiz_qtype_nameprefix($question)); - foreach ($questions as $question) { + // Negative grades will not do: + if (((float)($resultdetails->grade)) <= 0.0) { + $resultdetails->grade = 0.0; - $question->grade = $grades[$question->id]; - - if (!$answers = quiz_get_answers($question)) { - error("No answers defined for question id $question->id!"); + // Neither will extra credit: + } else if (((float)($resultdetails->grade)) >= 1.0) { + $resultdetails->grade = $question->maxgrade; + + } else { + $resultdetails->grade *= $question->maxgrade; } - $questionresult = quiz_grade_attempt_question_result($question, - $answers); // if time limit is enabled and exceeded, return zero grades - if($quiz->timelimit > 0) { - if(($quiz->timelimit + 60) <= $quiz->timesincestart) { - $questionresult->grade = 0; + if ($quiz->timelimit > 0) { + if (($quiz->timelimit + 60) <= $quiz->timesincestart) { + $resultdetails->grade = 0; } } - $result->grades[$question->id] = round($questionresult->grade, 2); - $result->sumgrades += $questionresult->grade; - $result->feedback[$question->id] = $questionresult->feedback; - $result->response[$question->id] = $questionresult->response; - $result->correct[$question->id] = $questionresult->correct; + $result->sumgrades += $resultdetails->grade; + $resultdetails->grade = round($resultdetails->grade, 2); + $result->details[$qid] = $resultdetails; } $fraction = (float)($result->sumgrades / $quiz->sumgrades); @@ -2195,629 +1711,6 @@ function quiz_grade_attempt_results($quiz, $questions) { return $result; } - -function quiz_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 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 - - switch ($question->qtype) { - case SHORTANSWER: - - if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { - $oldanswers = array(); - } - - $answers = array(); - $maxfraction = -1; - - // Insert all the new answers - foreach ($question->answer as $key => $dataanswer) { - if ($dataanswer != "") { - if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it - $answer = $oldanswer; - $answer->answer = trim($dataanswer); - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!update_record("quiz_answers", $answer)) { - $result->error = "Could not update quiz answer! (id=$answer->id)"; - return $result; - } - } else { // This is a completely new answer - unset($answer); - $answer->answer = trim($dataanswer); - $answer->question = $question->id; - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!$answer->id = insert_record("quiz_answers", $answer)) { - $result->error = "Could not insert quiz answer!"; - return $result; - } - } - $answers[] = $answer->id; - if ($question->fraction[$key] > $maxfraction) { - $maxfraction = $question->fraction[$key]; - } - } - } - - if ($options = get_record("quiz_shortanswer", "question", $question->id)) { - $options->answers = implode(",",$answers); - $options->usecase = $question->usecase; - if (!update_record("quiz_shortanswer", $options)) { - $result->error = "Could not update quiz shortanswer options! (id=$options->id)"; - return $result; - } - } else { - unset($options); - $options->question = $question->id; - $options->answers = implode(",",$answers); - $options->usecase = $question->usecase; - if (!insert_record("quiz_shortanswer", $options)) { - $result->error = "Could not insert quiz shortanswer options!"; - return $result; - } - } - - /// Perform sanity checks on fractional grades - if ($maxfraction != 1) { - $maxfraction = $maxfraction * 100; - $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction); - return $result; - } - break; - - case NUMERICAL: // Note similarities to SHORTANSWER - - if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { - $oldanswers = array(); - } - - $answers = array(); - $maxfraction = -1; - - // Insert all the new answers - foreach ($question->answer as $key => $dataanswer) { - if ($dataanswer != "") { - if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it - $answer = $oldanswer; - $answer->answer = $dataanswer; - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!update_record("quiz_answers", $answer)) { - $result->error = "Could not update quiz answer! (id=$answer->id)"; - return $result; - } - } else { // This is a completely new answer - unset($answer); - $answer->answer = $dataanswer; - $answer->question = $question->id; - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!$answer->id = insert_record("quiz_answers", $answer)) { - $result->error = "Could not insert quiz answer!"; - return $result; - } - } - $answers[] = $answer->id; - if ($question->fraction[$key] > $maxfraction) { - $maxfraction = $question->fraction[$key]; - } - - if ($options = get_record("quiz_numerical", "answer", $answer->id)) { - $options->min= $question->min[$key]; - $options->max= $question->max[$key]; - if (!update_record("quiz_numerical", $options)) { - $result->error = "Could not update quiz numerical options! (id=$options->id)"; - return $result; - } - } else { // completely new answer - unset($options); - $options->question = $question->id; - $options->answer = $answer->id; - $options->min = $question->min[$key]; - $options->max = $question->max[$key]; - if (!insert_record("quiz_numerical", $options)) { - $result->error = "Could not insert quiz numerical options!"; - return $result; - } - } - } - } - - /// Perform sanity checks on fractional grades - if ($maxfraction != 1) { - $maxfraction = $maxfraction * 100; - $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction); - return $result; - } - break; - - - case TRUEFALSE: - - if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { - $oldanswers = array(); - } - - if ($true = array_shift($oldanswers)) { // Existing answer, so reuse it - $true->answer = get_string("true", "quiz"); - $true->fraction = $question->answer; - $true->feedback = $question->feedbacktrue; - if (!update_record("quiz_answers", $true)) { - $result->error = "Could not update quiz answer \"true\")!"; - return $result; - } - } else { - unset($true); - $true->answer = get_string("true", "quiz"); - $true->question = $question->id; - $true->fraction = $question->answer; - $true->feedback = $question->feedbacktrue; - if (!$true->id = insert_record("quiz_answers", $true)) { - $result->error = "Could not insert quiz answer \"true\")!"; - return $result; - } - } - - if ($false = array_shift($oldanswers)) { // Existing answer, so reuse it - $false->answer = get_string("false", "quiz"); - $false->fraction = 1 - (int)$question->answer; - $false->feedback = $question->feedbackfalse; - if (!update_record("quiz_answers", $false)) { - $result->error = "Could not insert quiz answer \"false\")!"; - return $result; - } - } else { - unset($false); - $false->answer = get_string("false", "quiz"); - $false->question = $question->id; - $false->fraction = 1 - (int)$question->answer; - $false->feedback = $question->feedbackfalse; - if (!$false->id = insert_record("quiz_answers", $false)) { - $result->error = "Could not insert quiz answer \"false\")!"; - return $result; - } - } - - if ($options = get_record("quiz_truefalse", "question", $question->id)) { - // No need to do anything, since the answer IDs won't have changed - // But we'll do it anyway, just for robustness - $options->trueanswer = $true->id; - $options->falseanswer = $false->id; - if (!update_record("quiz_truefalse", $options)) { - $result->error = "Could not update quiz truefalse options! (id=$options->id)"; - return $result; - } - } else { - unset($options); - $options->question = $question->id; - $options->trueanswer = $true->id; - $options->falseanswer = $false->id; - if (!insert_record("quiz_truefalse", $options)) { - $result->error = "Could not insert quiz truefalse options!"; - return $result; - } - } - break; - - - case MULTICHOICE: - - if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { - $oldanswers = array(); - } - - - // following hack to check at least two answers exist - $answercount = 0; - foreach ($question->answer as $key=>$dataanswer) { - if ($dataanswer != "") { - $answercount++; - } - } - $answercount += count($oldanswers); - if ($answercount < 2) { // check there are at lest 2 answers for multiple choice - $result->notice = get_string("notenoughanswers", "quiz", "2"); - return $result; - } - - - - // Insert all the new answers - - $totalfraction = 0; - $maxfraction = -1; - - $answers = array(); - - foreach ($question->answer as $key => $dataanswer) { - if ($dataanswer != "") { - if ($answer = array_shift($oldanswers)) { // Existing answer, so reuse it - $answer->answer = $dataanswer; - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!update_record("quiz_answers", $answer)) { - $result->error = "Could not update quiz answer! (id=$answer->id)"; - return $result; - } - } else { - unset($answer); - $answer->answer = $dataanswer; - $answer->question = $question->id; - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!$answer->id = insert_record("quiz_answers", $answer)) { - $result->error = "Could not insert quiz answer! "; - return $result; - } - } - $answers[] = $answer->id; - - if ($question->fraction[$key] > 0) { // Sanity checks - $totalfraction += $question->fraction[$key]; - } - if ($question->fraction[$key] > $maxfraction) { - $maxfraction = $question->fraction[$key]; - } - } - } - - if ($options = get_record("quiz_multichoice", "question", $question->id)) { - $options->answers = implode(",",$answers); - $options->single = $question->single; - if (!update_record("quiz_multichoice", $options)) { - $result->error = "Could not update quiz multichoice options! (id=$options->id)"; - return $result; - } - } else { - unset($options); - $options->question = $question->id; - $options->answers = implode(",",$answers); - $options->single = $question->single; - if (!insert_record("quiz_multichoice", $options)) { - $result->error = "Could not insert quiz multichoice options!"; - return $result; - } - } - - /// Perform sanity checks on fractional grades - if ($options->single) { - if ($maxfraction != 1) { - $maxfraction = $maxfraction * 100; - $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction); - return $result; - } - } else { - $totalfraction = round($totalfraction,2); - if ($totalfraction != 1) { - $totalfraction = $totalfraction * 100; - $result->noticeyesno = get_string("fractionsaddwrong", "quiz", $totalfraction); - return $result; - } - } - break; - - case MATCH: - - if (!$oldsubquestions = get_records("quiz_match_sub", "question", $question->id, "id ASC")) { - $oldsubquestions = array(); - } - - - // following hack to check at least three answers exist - $answercount = 0; - foreach ($question->subquestions as $key=>$questiontext) { - $answertext = $question->subanswers[$key]; - if (!empty($questiontext) and !empty($answertext)) { - $answercount++; - } - } - $answercount += count($oldsubquestions); - if ($answercount < 3) { // check there are at lest 3 answers for matching type questions - $result->notice = get_string("notenoughanswers", "quiz", "3"); - return $result; - } - - - - $subquestions = array(); - - // Insert all the new question+answer pairs - foreach ($question->subquestions as $key => $questiontext) { - $answertext = $question->subanswers[$key]; - if (!empty($questiontext) and !empty($answertext)) { - if ($subquestion = array_shift($oldsubquestions)) { // Existing answer, so reuse it - $subquestion->questiontext = $questiontext; - $subquestion->answertext = $answertext; - if (!update_record("quiz_match_sub", $subquestion)) { - $result->error = "Could not insert quiz match subquestion! (id=$subquestion->id)"; - return $result; - } - } else { - unset($subquestion); - $subquestion->question = $question->id; - $subquestion->questiontext = $questiontext; - $subquestion->answertext = $answertext; - if (!$subquestion->id = insert_record("quiz_match_sub", $subquestion)) { - $result->error = "Could not insert quiz match subquestion!"; - return $result; - } - } - $subquestions[] = $subquestion->id; - } - } - - if (count($subquestions) < 3) { - $result->noticeyesno = get_string("notenoughsubquestions", "quiz"); - return $result; - } - - if ($options = get_record("quiz_match", "question", $question->id)) { - $options->subquestions = implode(",",$subquestions); - if (!update_record("quiz_match", $options)) { - $result->error = "Could not update quiz match options! (id=$options->id)"; - return $result; - } - } else { - unset($options); - $options->question = $question->id; - $options->subquestions = implode(",",$subquestions); - if (!insert_record("quiz_match", $options)) { - $result->error = "Could not insert quiz match options!"; - return $result; - } - } - - break; - - - case RANDOMSAMATCH: - $options->question = $question->id; - $options->choose = $question->choose; - if ($existing = get_record("quiz_randomsamatch", "question", $options->question)) { - $options->id = $existing->id; - if (!update_record("quiz_randomsamatch", $options)) { - $result->error = "Could not update quiz randomsamatch options!"; - return $result; - } - } else { - if (!insert_record("quiz_randomsamatch", $options)) { - $result->error = "Could not insert quiz randomsamatch options!"; - return $result; - } - } - break; - - case MULTIANSWER: - if (!$oldmultianswers = get_records("quiz_multianswers", "question", $question->id, "id ASC")) { - $oldmultianswers = array(); - } - - // Insert all the new multi answers - foreach ($question->answers as $dataanswer) { - if ($oldmultianswer = array_shift($oldmultianswers)) { // Existing answer, so reuse it - $multianswer = $oldmultianswer; - $multianswer->positionkey = $dataanswer->positionkey; - $multianswer->norm = $dataanswer->norm; - $multianswer->answertype = $dataanswer->answertype; - - if (! $multianswer->answers = quiz_save_multianswer_alternatives - ($question->id, $dataanswer->answertype, - $dataanswer->alternatives, $oldmultianswer->answers)) - { - $result->error = "Could not update multianswer alternatives! (id=$multianswer->id)"; - return $result; - } - if (!update_record("quiz_multianswers", $multianswer)) { - $result->error = "Could not update quiz multianswer! (id=$multianswer->id)"; - return $result; - } - } else { // This is a completely new answer - unset($multianswer); - $multianswer->question = $question->id; - $multianswer->positionkey = $dataanswer->positionkey; - $multianswer->norm = $dataanswer->norm; - $multianswer->answertype = $dataanswer->answertype; - - if (! $multianswer->answers = quiz_save_multianswer_alternatives - ($question->id, $dataanswer->answertype, - $dataanswer->alternatives)) - { - $result->error = "Could not insert multianswer alternatives! (questionid=$question->id)"; - return $result; - } - if (!insert_record("quiz_multianswers", $multianswer)) { - $result->error = "Could not insert quiz multianswer!"; - return $result; - } - } - } - break; - - case RANDOM: - break; - - case DESCRIPTION: - break; - - default: - $result->error = "Unsupported question type ($question->qtype)!"; - return $result; - break; - } - return true; -} - - -function quiz_remove_unwanted_questions(&$questions, $quiz) { -/// Given an array of questions, and a list of question IDs, -/// this function removes unwanted questions from the array -/// Used by review.php and attempt.php to counter changing quizzes - - $quizquestions = array(); - $quizids = explode(",", $quiz->questions); - foreach ($quizids as $quizid) { - $quizquestions[$quizid] = true; - } - foreach ($questions as $key => $question) { - if (!isset($quizquestions[$question->id])) { - unset($questions[$key]); - } - } -} - -function quiz_save_multianswer_alternatives - ($questionid, $answertype, $alternatives, $oldalternativeids= NULL) -{ -// Returns false if something goes wrong, -// otherwise the ids of the answers. - - if (empty($oldalternativeids) - or !($oldalternatives = - get_records_list('quiz_answers', 'id', $oldalternativeids))) - { - $oldalternatives = array(); - } - - $alternativeids = array(); - - foreach ($alternatives as $altdata) { - - if ($altold = array_shift($oldalternatives)) { // Use existing one... - $alt = $altold; - $alt->answer = $altdata->answer; - $alt->fraction = $altdata->fraction; - $alt->feedback = $altdata->feedback; - if (!update_record("quiz_answers", $alt)) { - return false; - } - - } else { // Completely new one - unset($alt); - $alt->question= $questionid; - $alt->answer = $altdata->answer; - $alt->fraction = $altdata->fraction; - $alt->feedback = $altdata->feedback; - if (!($alt->id = insert_record("quiz_answers", $alt))) { - return false; - } - } - - // For the answer type numerical, each alternative has individual options: - if ($answertype == NUMERICAL) { - if ($numericaloptions = - get_record('quiz_numerical', 'answer', $alt->id)) - { - // Reuse existing numerical options - $numericaloptions->min = $altdata->min; - $numericaloptions->max = $altdata->max; - if (!update_record('quiz_numerical', $numericaloptions)) { - return false; - } - } else { - // New numerical options - $numericaloptions->answer = $alt->id; - $numericaloptions->question = $questionid; - $numericaloptions->min = $altdata->min; - $numericaloptions->max = $altdata->max; - if (!insert_record("quiz_numerical", $numericaloptions)) { - return false; - } - } - } else { // Delete obsolete numerical options - delete_records('quiz_numerical', 'answer', $alt->id); - } // end if NUMERICAL - - $alternativeids[] = $alt->id; - } // end foreach $alternatives - $answers = implode(',', $alternativeids); - - // Removal of obsolete alternatives from answers and quiz_numerical: - while ($altobsolete = array_shift($oldalternatives)) { - delete_records("quiz_answers", "id", $altobsolete->id); - - // Possibly obsolute numerical options are also to be deleted: - delete_records("quiz_numerical", 'answer', $altobsolete->id); - } - - // Common alternative options and removal of obsolete options - switch ($answertype) { - case NUMERICAL: - if (!empty($oldalternativeids)) { - delete_records('quiz_shortanswer', 'answers', -$oldalternativeids); - delete_records('quiz_multichoice', 'answers', -$oldalternativeids); - } - break; - case SHORTANSWER: - if (!empty($oldalternativeids)) { - delete_records('quiz_multichoice', 'answers', -$oldalternativeids); - $options = get_record('quiz_shortanswer', - 'answers', $oldalternativeids); - } else { - unset($options); - } - if (empty($options)) { - // Create new shortanswer options - $options->question = $questionid; - $options->usecase = 0; - $options->answers = $answers; - if (!insert_record('quiz_shortanswer', $options)) { - return false; - } - } else if ($answers != $oldalternativeids) { - // Shortanswer options needs update: - $options->answers = $answers; - if (!update_record('quiz_shortanswer', $options)) { - return false; - } - } - break; - case MULTICHOICE: - if (!empty($oldalternativeids)) { - delete_records('quiz_shortanswer', 'answers', -$oldalternativeids); - $options = get_record('quiz_multichoice', - 'answers', $oldalternativeids); - } else { - unset($options); - } - if (empty($options)) { - // Create new multichoice options - $options->question = $questionid; - $options->layout = 0; - $options->single = 1; - $options->answers = $answers; - if (!insert_record('quiz_multichoice', $options)) { - return false; - } - } else if ($answers != $oldalternativeids) { - // Multichoice options needs update: - $options->answers = $answers; - if (!update_record('quiz_multichoice', $options)) { - return false; - } - } - break; - default: - return false; - } - return $answers; -} - function quiz_get_recent_mod_activity(&$activities, &$index, $sincetime, $courseid, $quiz="0", $user="", $groupid="") { // Returns all quizzes since a given time. If quiz is specified then // this restricts the results diff --git a/mod/quiz/question.php b/mod/quiz/question.php index 6b4a4aeb0e..c37982c244 100644 --- a/mod/quiz/question.php +++ b/mod/quiz/question.php @@ -98,66 +98,9 @@ } } - if ($form = data_submitted()) { - - // First, save the basic question itself - - $question->name = $form->name; - $question->questiontext = $form->questiontext; - $question->questiontextformat = $form->questiontextformat; - - if (empty($form->image)) { - $question->image = ""; - } else { - $question->image = $form->image; - } - - if (isset($form->defaultgrade)) { - $question->defaultgrade = $form->defaultgrade; - } - - if ($err = formcheck($question)) { - notify(get_string("someerrorswerefound")); - - } else { - - 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 = quiz_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"); - } + if ($form = data_submitted()) { + $question = $QUIZ_QTYPES[$qtype]->save_question($question, + $form, $course); } $grades = array(1,0.9,0.8,0.75,0.70,0.66666,0.60,0.50,0.40,0.33333,0.30,0.25,0.20,0.16666,0.10,0.05,0); @@ -210,160 +153,7 @@ $onsubmit = ""; } - switch ($qtype) { - case SHORTANSWER: - if (!empty($question->id)) { - $options = get_record("quiz_shortanswer", "question", $question->id); - } else { - $options->usecase = 0; - } - if (!empty($options->answers)) { - $answersraw = get_records_list("quiz_answers", "id", $options->answers); - } - for ($i=0; $iid)) { - $options = get_record("quiz_truefalse", "question", "$question->id"); - } - if (!empty($options->trueanswer)) { - $true = get_record("quiz_answers", "id", $options->trueanswer); - } else { - $true->fraction = 1; - $true->feedback = ""; - } - if (!empty($options->falseanswer)) { - $false = get_record("quiz_answers", "id", "$options->falseanswer"); - } else { - $false->fraction = 0; - $false->feedback = ""; - } - - if ($true->fraction > $false->fraction) { - $question->answer = 1; - } else { - $question->answer = 0; - } - - print_heading_with_help(get_string("editingtruefalse", "quiz"), "truefalse", "quiz"); - require("truefalse.html"); - break; - - case MULTICHOICE: - if (!empty($question->id)) { - $options = get_record("quiz_multichoice", "question", $question->id); - } else { - $options->single = 1; - } - if (!empty($options->answers)) { - $answersraw = get_records_list("quiz_answers", "id", $options->answers); - } - for ($i=0; $iid)) { - $options = get_record("quiz_match", "question", $question->id); - if (!empty($options->subquestions)) { - $oldsubquestions = get_records_list("quiz_match_sub", "id", $options->subquestions); - } - } - if (empty($subquestions) and empty($subanswers)) { - for ($i=0; $iquestiontext; // insert questions into slots - $subanswers[$i] = $oldsubquestion->answertext; // insert answers into slots - $i++; - } - } - } - print_heading_with_help(get_string("editingmatch", "quiz"), "match", "quiz"); - require("match.html"); - break; - - case RANDOMSAMATCH: - if (!empty($question->id)) { - $options = get_record("quiz_randomsamatch", "question", $question->id); - } else { - $options->choose = ""; - } - $numberavailable = count_records("quiz_questions", "category", $category->id, "qtype", SHORTANSWER); - print_heading_with_help(get_string("editingrandomsamatch", "quiz"), "randomsamatch", "quiz"); - require("randomsamatch.html"); - break; - - case RANDOM: - print_heading_with_help(get_string("editingrandom", "quiz"), "random", "quiz"); - require("random.html"); - break; - - case DESCRIPTION: - print_heading_with_help(get_string("editingdescription", "quiz"), "description", "quiz"); - require("description.html"); - break; - - case MULTIANSWER: - print_heading_with_help(get_string("editingmultianswer", "quiz"), "multianswer", "quiz"); - require("editmultianswer.php"); - break; - - case NUMERICAL: - // This will only support one answer of the type NUMERICAL - // However, lib.php has support for multiple answers - if (!empty($question->id)) { - $answersraw= quiz_get_answers($question); - } - $answers= array(); - for ($i=0; $i<6; $i++) { - $answers[$i]->answer = ""; // Make answer slots, default as blank... - $answers[$i]->min = ""; - $answers[$i]->max = ""; - $answers[$i]->feedback = ""; - } - if (!empty($answersraw)) { - $i=0; - foreach ($answersraw as $answer) { - $answers[$i] = $answer; - $i++; - } - } - print_heading_with_help(get_string("editingnumerical", "quiz"), "numerical", "quiz"); - require("numerical.html"); - break; - - - default: - error("Invalid question type"); - break; - } + require('questiontypes/'.$QUIZ_QTYPES[$qtype]->name().'/editquestion.php'); print_footer($course); diff --git a/mod/quiz/questiontypes/description/description.html b/mod/quiz/questiontypes/description/description.html new file mode 100644 index 0000000000..38188b5bc5 --- /dev/null +++ b/mod/quiz/questiontypes/description/description.html @@ -0,0 +1,80 @@ +
action="question.php"> +
+ + + + + + + + + + + + + + + + + +

:

+ id, true, true); ?> +

:

+ + +

:

+
+
+
+

+ +

+
+ "; + } + + print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); + + if ($usehtmleditor) { /// Trying this out for a while + echo ''; + } else { + echo "
"; + print_string("formattexttype"); + echo ": "; + if (!isset($question->questiontextformat)) { + $question->questiontextformat = FORMAT_MOODLE; + } + choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, ""); + helpbutton("textformat", get_string("helpformatting")); + echo "
"; + } + ?> +

:

+ image", get_string("none"),"",""); + } + ?> +
+ + + + +"> + +
+
+ diff --git a/mod/quiz/questiontypes/description/editquestion.php b/mod/quiz/questiontypes/description/editquestion.php new file mode 100644 index 0000000000..cd5a89c1b0 --- /dev/null +++ b/mod/quiz/questiontypes/description/editquestion.php @@ -0,0 +1,6 @@ + diff --git a/mod/quiz/questiontypes/description/icon.gif b/mod/quiz/questiontypes/description/icon.gif new file mode 100644 index 0000000000..9c6ec299d1 Binary files /dev/null and b/mod/quiz/questiontypes/description/icon.gif differ diff --git a/mod/quiz/questiontypes/description/questiontype.php b/mod/quiz/questiontypes/description/questiontype.php new file mode 100644 index 0000000000..dba6997ea6 --- /dev/null +++ b/mod/quiz/questiontypes/description/questiontype.php @@ -0,0 +1,57 @@ +'; + echo format_text($question->questiontext, + $question->questiontextformat, + NULL, $quiz->course); + quiz_print_possible_question_image($quiz->id, $question); + echo '

'; + return $currentnumber; + } + + function grade_response($question, $nameprefix) { + $result->grade = 0.0; + $result->answers = array(); + $result->correctanswers = array(); + return $result; + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[DESCRIPTION]= new quiz_description_qtype(); + +?> diff --git a/mod/quiz/questiontypes/match/editquestion.php b/mod/quiz/questiontypes/match/editquestion.php new file mode 100644 index 0000000000..c10475181f --- /dev/null +++ b/mod/quiz/questiontypes/match/editquestion.php @@ -0,0 +1,25 @@ +id)) { + $options = get_record("quiz_match", "question", $question->id); + if (!empty($options->subquestions)) { + $oldsubquestions = get_records_list("quiz_match_sub", "id", $options->subquestions); + } + } + if (empty($subquestions) and empty($subanswers)) { + for ($i=0; $iquestiontext; // insert questions into slots + $subanswers[$i] = $oldsubquestion->answertext; // insert answers into slots + $i++; + } + } + } + print_heading_with_help(get_string("editingmatch", "quiz"), "match", "quiz"); + require("match.html"); + +?> diff --git a/mod/quiz/questiontypes/match/icon.gif b/mod/quiz/questiontypes/match/icon.gif new file mode 100644 index 0000000000..fa1ed50978 Binary files /dev/null and b/mod/quiz/questiontypes/match/icon.gif differ diff --git a/mod/quiz/questiontypes/match/match.html b/mod/quiz/questiontypes/match/match.html new file mode 100644 index 0000000000..37327f015c --- /dev/null +++ b/mod/quiz/questiontypes/match/match.html @@ -0,0 +1,104 @@ +
action="question.php"> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

:

+ id, true, true); ?> +

:

+ + +

:

+
+
+
+

+ +

+
+ "; + } + + print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); + + if ($usehtmleditor) { /// Trying this out for a while + echo ''; + } else { + echo "
"; + print_string("formattexttype"); + echo ": "; + if (!isset($question->questiontextformat)) { + $question->questiontextformat = FORMAT_MOODLE; + } + choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, ""); + helpbutton("textformat", get_string("helpformatting")); + echo "
"; + } + ?> +

:

+ image", get_string("none"),"",""); + } + ?> +

:

+

:

+ +
+    + +
+ + + +"> + +
+
+ diff --git a/mod/quiz/questiontypes/match/questiontype.php b/mod/quiz/questiontypes/match/questiontype.php new file mode 100644 index 0000000000..419e612241 --- /dev/null +++ b/mod/quiz/questiontypes/match/questiontype.php @@ -0,0 +1,247 @@ +id, "id ASC")) { + $oldsubquestions = array(); + } + + // following hack to check at least three answers exist + $answercount = 0; + foreach ($question->subquestions as $key=>$questiontext) { + $answertext = $question->subanswers[$key]; + if (!empty($questiontext) and !empty($answertext)) { + $answercount++; + } + } + $answercount += count($oldsubquestions); + if ($answercount < 3) { // check there are at lest 3 answers for matching type questions + $result->notice = get_string("notenoughanswers", "quiz", "3"); + return $result; + } + + $subquestions = array(); + + // Insert all the new question+answer pairs + foreach ($question->subquestions as $key => $questiontext) { + $answertext = $question->subanswers[$key]; + if (!empty($questiontext) and !empty($answertext)) { + if ($subquestion = array_shift($oldsubquestions)) { // Existing answer, so reuse it + $subquestion->questiontext = $questiontext; + $subquestion->answertext = $answertext; + if (!update_record("quiz_match_sub", $subquestion)) { + $result->error = "Could not insert quiz match subquestion! (id=$subquestion->id)"; + return $result; + } + } else { + unset($subquestion); + $subquestion->question = $question->id; + $subquestion->questiontext = $questiontext; + $subquestion->answertext = $answertext; + if (!$subquestion->id = insert_record("quiz_match_sub", $subquestion)) { + $result->error = "Could not insert quiz match subquestion!"; + return $result; + } + } + $subquestions[] = $subquestion->id; + } + } + + if (count($subquestions) < 3) { + $result->noticeyesno = get_string("notenoughsubquestions", "quiz"); + return $result; + } + + if ($options = get_record("quiz_match", "question", $question->id)) { + $options->subquestions = implode(",",$subquestions); + if (!update_record("quiz_match", $options)) { + $result->error = "Could not update quiz match options! (id=$options->id)"; + return $result; + } + } else { + unset($options); + $options->question = $question->id; + $options->subquestions = implode(",",$subquestions); + if (!insert_record("quiz_match", $options)) { + $result->error = "Could not insert quiz match options!"; + return $result; + } + } + return true; + } + + function convert_to_response_answer_field($questionresponse) { + /// This method, together with extract_response, should be + /// obsolete as soon as we get a better response storage + + $delimiter = ''; + $responseanswerfield = ''; + foreach ($questionresponse as $key => $value) { + if ($matchid = $this->extract_response_id($key)) { + $responseanswerfield .= "$delimiter$matchid-$value"; + $delimiter = ','; + } else { + notify("Error: Illegal match key $key detected"); + } + } + return $responseanswerfield; + } + + function extract_response($rawresponse, $nameprefix) { + if (!($options = get_record("quiz_match", + "question", $rawresponse->question))) { + notify("Error: Missing question options!"); + return array(); + } + $subids = explode(',', $options->subquestions); + foreach ($subids as $subid) { + $response[$nameprefix.$subid] = + ereg("(^|,)$subid-([^,]+)", $rawresponse->answer, $regs) + ? $regs[2] + : ''; + } + return $response; + } + + function print_question_formulation_and_controls($question, + $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + + // Print question text and possible image + if (!empty($question->questiontext)) { + echo format_text($question->questiontext, + $question->questiontextformat, + NULL, $quiz->course); + } + quiz_print_possible_question_image($quiz->id, $question); + + // It so happens to be that $correctanswers for this question type also + // contains the subqustions, which we need to make sure we have: + if (empty($correctanswers)) { + $options = get_record('quiz_match', 'question', $question->id) + and $subquestions = get_records_list('quiz_match_sub', 'id', + $options->subquestions); + } else { + $subquestions = $correctanswers; + } + + /// Check whether everything turned out alright: + if (empty($subquestions)) { + notify("Error: Missing subquestions for this question!"); + + } else { + /// Everything is fine - + /// Set up $subquestions and $answers and do the shuffling: + + if ($quiz->shuffleanswers) { + $subquestions = draw_rand_array($subquestions, + count($subquestions)); + } + foreach ($subquestions as $key => $subquestion) { + unset($answers[$key]); + $answers[$subquestion->id] = $subquestion->answertext; + } + $answers = draw_rand_array($answers, count($answers)); + } + + ///// Ptint the input controls ////// + + echo ''; + foreach ($subquestions as $subquestion) { + + /// Subquestion text: + echo ''; + + /// Drop-down list: + $menuname = $nameprefix.$subquestion->id; + $response = isset($question->response[$menuname]) + ? $question->response[$menuname] : '0'; + if (isset($correctanswers[$menuname]) + && $correctanswers[$menuname]->id + == $response) { + $class = ' class="highlight" '; + } else { + $class = ''; + } + echo "'; + } + echo '
'; + echo $subquestion->questiontext; + echo '"; + choose_from_menu($answers, $menuname, $response); + if ($quiz->feedback && isset($answers[$menuname]) + && $answers[$menuname]->feedback) { + quiz_print_comment($answers[$menuname]->feedback); + } + echo '
'; + } + + function grade_response($question, $nameprefix) { + /// This question type does not use the table quiz_answers + /// but we will take some measures to emulate that record anyway. + + $result->grade = 0.0; + $result->answers = array(); + $result->correctanswers = array(); + + if (!($options = get_record('quiz_match', 'question', $question->id) + and $subquestions = get_records_list('quiz_match_sub', + 'id', $options->subquestions))) { + notify("Error: Cannot find match options and subquestions + for question $question->id"); + return $result; + } + + $fraction = 1.0 / count($subquestions); + + /// Populate correctanswers arrays: + foreach ($subquestions as $subquestion) { + $subquestion->fraction = $fraction; + $subquestion->answer = $subquestion->answertext; + $subquestion->feedback = ''; + $result->correctanswers[$nameprefix.$subquestion->id] = + $subquestion; + } + + foreach ($question->response as $responsekey => $answerid) { + + if ($answerid and $answer = + $result->correctanswers[$nameprefix.$answerid]) { + + if ($result->correctanswers[$responsekey]->answer + == $answer->answer) { + + /// The response was correct! + $result->answers[$responsekey] = + $result->correctanswers[$responsekey]; + $result->grade += $fraction; + + } else { + /// The response was incorrect: + $answer->fraction = 0.0; + $result->answers[$responsekey] = $answer; + } + + } + } + return $result; + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[MATCH]= new quiz_match_qtype(); + +?> diff --git a/mod/quiz/questiontypes/multianswer/editquestion.php b/mod/quiz/questiontypes/multianswer/editquestion.php new file mode 100644 index 0000000000..0e58937b52 --- /dev/null +++ b/mod/quiz/questiontypes/multianswer/editquestion.php @@ -0,0 +1,46 @@ +questiontext and $question->id) { + $answers = quiz_get_answers($question); + + foreach ($answers as $multianswer) { + $parsableanswerdef = '{' . $multianswer->norm . ':'; + switch ($multianswer->answertype) { + case MULTICHOICE: + $parsableanswerdef .= 'MULTICHOICE:'; + break; + case SHORTANSWER: + $parsableanswerdef .= 'SHORTANSWER:'; + break; + case NUMERICAL: + $parsableanswerdef .= 'NUMERICAL:'; + break; + default: + error("answertype $multianswer->answertype not recognized"); + } + $separator= ''; + foreach ($multianswer->subanswers as $subanswer) { + $parsableanswerdef .= $separator + . '%' . round(100*$subanswer->fraction) . '%'; + $parsableanswerdef .= $subanswer->answer; + if (isset($subanswer->min) && isset($subanswer->max) + and $subanswer->min || $subanswer->max) { + // Special for numerical answers: + $errormargin = $subanswer->answer - $subanswer->min; + $parsableanswerdef .= ":$errormargin"; + } + if ($subanswer->feedback) { + $parsableanswerdef .= "#$subanswer->feedback"; + } + $separator = '~'; + } + $parsableanswerdef .= '}'; + $question->questiontext = str_replace + ("{#$multianswer->positionkey}", $parsableanswerdef, + $question->questiontext); + } + } + print_heading_with_help(get_string('editingmultianswer', 'quiz'), + 'multianswer', 'quiz'); + require('multianswer.html'); + +?> diff --git a/mod/quiz/questiontypes/multianswer/icon.gif b/mod/quiz/questiontypes/multianswer/icon.gif new file mode 100644 index 0000000000..edaa24193d Binary files /dev/null and b/mod/quiz/questiontypes/multianswer/icon.gif differ diff --git a/mod/quiz/questiontypes/multianswer/multianswer.html b/mod/quiz/questiontypes/multianswer/multianswer.html new file mode 100644 index 0000000000..4c3da4bbd2 --- /dev/null +++ b/mod/quiz/questiontypes/multianswer/multianswer.html @@ -0,0 +1,114 @@ +
action="question.php"> + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

:

+ + id, true, true ); ?> + +

:

+ + + + + +

:

+ + "; + + } + + print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); + + if ($usehtmleditor) { + + helpbutton("richtext", get_string("helprichtext"), "moodle"); + + } else { + + helpbutton("text", get_string("helptext"), "moodle"); + + } + + ?> + +

:

+ + image", get_string("none"),"",""); + + } + + ?> + +
+ + + + + + + + + +"> + + + +
+ +
+ + + diff --git a/mod/quiz/questiontypes/multianswer/questiontype.php b/mod/quiz/questiontypes/multianswer/questiontype.php new file mode 100644 index 0000000000..550002d14d --- /dev/null +++ b/mod/quiz/questiontypes/multianswer/questiontype.php @@ -0,0 +1,623 @@ +id = $question->id; + + if ($multianswers = get_records('quiz_multianswers', 'question', $question->id)) { + foreach ($multianswers as $multianswer) { + $virtualquestion->qtype = $multianswer->answertype; + // Call to other question type for subanswers + $addedcondition = " AND a.id IN ($multianswer->answers) "; + $multianswer->subanswers = + $QUIZ_QTYPES[$multianswer->answertype] + ->get_answers($virtualquestion, $addedcondition); + $answers[] = $multianswer; + } + } + return $answers; + } + + function name() { + return 'multianswer'; + } + + function save_question_options($question) { + if (!$oldmultianswers = get_records("quiz_multianswers", "question", $question->id, "id ASC")) { + $oldmultianswers = array(); + } + + // Insert all the new multi answers + foreach ($question->answers as $dataanswer) { + if ($oldmultianswer = array_shift($oldmultianswers)) { // Existing answer, so reuse it + $multianswer = $oldmultianswer; + $multianswer->positionkey = $dataanswer->positionkey; + $multianswer->norm = $dataanswer->norm; + $multianswer->answertype = $dataanswer->answertype; + + if (! $multianswer->answers = + quiz_qtype_multianswer_save_alternatives + ($question->id, $dataanswer->answertype, + $dataanswer->alternatives, $oldmultianswer->answers)) + { + $result->error = "Could not update multianswer alternatives! (id=$multianswer->id)"; + return $result; + } + if (!update_record("quiz_multianswers", $multianswer)) { + $result->error = "Could not update quiz multianswer! (id=$multianswer->id)"; + return $result; + } + } else { // This is a completely new answer + unset($multianswer); + $multianswer->question = $question->id; + $multianswer->positionkey = $dataanswer->positionkey; + $multianswer->norm = $dataanswer->norm; + $multianswer->answertype = $dataanswer->answertype; + + if (! $multianswer->answers = + quiz_qtype_multianswer_save_alternatives + ($question->id, $dataanswer->answertype, + $dataanswer->alternatives)) + { + $result->error = "Could not insert multianswer alternatives! (questionid=$question->id)"; + return $result; + } + if (!insert_record("quiz_multianswers", $multianswer)) { + $result->error = "Could not insert quiz multianswer!"; + return $result; + } + } + } + } + + function save_question($authorizedquestion, $form, $course) { + + $question = quiz_qtype_multianswer_extract_question + ($form->questiontext); + $question->id = $authorizedquestion->id; + $question->qtype = $authorizedquestion->qtype; + $question->category = $authorizedquestion->category; + + $question->name = $form->name; + if (empty($form->image)) { + $question->image = ""; + } else { + $question->image = $form->image; + } + + // Formcheck + $err = array(); + if (empty($question->name)) { + $err["name"] = get_string("missingname", "quiz"); + } + if (empty($question->questiontext)) { + $err["questiontext"] = get_string("missingquestiontext", "quiz"); + } + if ($err) { // Formcheck failed + notify(get_string("someerrorswerefound")); + + } else { + + if (!empty($question->id)) { // Question already exists + 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) + if (!$question->id = insert_record("quiz_questions", $question)) { + error("Could not insert new question!"); + } + } + + // Now to save all the answers and type-specific options + $result = $this->save_question_options($question); + + if (!empty($result->error)) { + error($result->error); + } + + if (!empty($result->notice)) { + notice_yesno($result->notice, "question.php?id=$question->id", "edit.php"); + print_footer($course); + exit; + } + + redirect("edit.php"); + } + } + + function convert_to_response_answer_field($questionresponse) { + /// This method, together with extract_response, should be + /// obsolete as soon as we get a better response storage + + $delimiter = ''; + $responseanswerfield = ''; + foreach ($questionresponse as $key => $value) { + if ($multianswerid = $this->extract_response_id($key)) { + $responseanswerfield .= "$delimiter$multianswerid-$value"; + $delimiter = ','; + } else { + notify("Error: Illegal match key $key detected"); + } + } + return $responseanswerfield; + } + + function extract_response($rawresponse, $nameprefix) { + /// A temporary fix for bug #647 has accidently been enforced here + /// because of the odd circumstances during the refactoring + + $multianswers = get_records('quiz_multianswers', + 'question', $rawresponse->question); + $response = array(); + foreach ($multianswers as $maid => $multianswer) { + if (ereg("(^|,)$maid-(.*)", $rawresponse->answer, $regs)) { + $splits = split(',[0-9]+-', $regs[2], 2); + $response[$nameprefix.$maid] = $splits[0]; + } else { + $response[$nameprefix.$maid] = ''; + } + } + return $response; + } + + function print_question_formulation_and_controls($question, + $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + global $THEME; + + // For this question type, we better print the image on top: + quiz_print_possible_question_image($quiz->id, $question); + + $qtextremaining = format_text($question->questiontext, + $question->questiontextformat, + NULL, $quiz->course); + + $strfeedback = get_string('feedback', 'quiz'); + + // The regex will recognize text snippets of type {#X} + // where the X can be any text not containg } or white-space characters. + + while (ereg('\{#([^[:space:]}]*)}', $qtextremaining, $regs)) { + $qtextsplits = explode($regs[0], $qtextremaining, 2); + echo $qtextsplits[0]; + $qtextremaining = $qtextsplits[1]; + + $multianswer = get_record('quiz_multianswers', 'question', + $question->id, 'positionkey', $regs[1]); + $inputname = $nameprefix.$multianswer->id; + $response = isset($question->response[$inputname]) + ? $question->response[$inputname] : ''; + + /// Determine style + if (!empty($correctanswers) && '' !== $response) { + + if (!isset($answers[$inputname]) + || $answers[$inputname]->fraction <= 0.0) { + // The response must have been totally wrong: + $style = ' style="background-color:red" '; + + } else if ($answers[$inputname]->fraction >= 1.0) { + // The response must was correct!! + $style = 'style="background-color:lime"'; + + } else { + // This response did at least give some credit: + $style = 'style="background-color:yellow"'; + } + } else { + // No colorish feedback is to be used + $style = ''; + } + + // Determine feedback popup if any + if ($quiz->feedback && isset($answers[$inputname]) + && '' !== $answers[$inputname]->feedback) { + $title = str_replace("'", "\\'", $answers[$inputname]->feedback); + $popup = " onmouseover=\"return overlib('$title', CAPTION, '$strfeedback', FGCOLOR, '$THEME->cellcontent');\" ". + " onmouseout=\"return nd();\" "; + } else { + $popup = ''; + } + + // Print the input control + switch ($multianswer->answertype) { + case SHORTANSWER: + case NUMERICAL: + echo " "; + break; + case MULTICHOICE: + $outputoptions = ''; // Default empty option + $mcanswers = get_records_list("quiz_answers", "id", $multianswer->answers); + foreach ($mcanswers as $mcanswer) { + $selected = $response == $mcanswer->id + ? ' selected="selected" ' : ''; + $outputoptions .= ""; + } + echo "'; + break; + default: + error("Unable to recognized answertype $answer->answertype"); + break; + } + } + + // Print the final piece of question text: + echo $qtextremaining; + } + + function grade_response($question, $nameprefix) { + + global $QUIZ_QTYPES; + + $result->grade = 0.0; + $result->answers = array(); + $result->correctanswers = array(); + + $multianswers = get_records('quiz_multianswers', + 'question', $question->id); + // Default settings: + $subquestion->id = $question->id; + $normsum = 0; + + // Grade each multianswer + foreach ($multianswers as $multianswer) { + $name = $nameprefix.$multianswer->id; + $subquestion->response[$nameprefix] = + isset($question->response[$name]) + ? $question->response[$name] : ''; + + $subresult = $QUIZ_QTYPES[$multianswer->answertype] + ->grade_response($subquestion, $nameprefix, + " AND a.id IN ($multianswer->answers) "); + + // Summarize subquestion results: + + if (isset($subresult->answers[$nameprefix])) { + + /// Answer was found: + $result->answers[$name] = $subresult->answers[$nameprefix]; + + if ($result->answers[$name]->fraction >= 1.0) { + // This is also the correct answer: + $result->correctanswers[$name] = $result->answers[$name]; + } + } + + if (!isset($result->correctanswers[$name])) { + // Pick the first correctanswer: + foreach ($subresult->correctanswers as $correctanswer) { + $result->correctanswers[$name] = $correctanswer; + break; + } + } + $result->grade += $multianswer->norm * $subresult->grade; + $normsum += $multianswer->norm; + } + $result->grade /= $normsum; + + return $result; + } +} +//// END OF CLASS //// + + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[MULTIANSWER]= new quiz_embedded_cloze_qtype(); + + +///////////////////////////////////////////////////////////// +//// ADDITIONAL FUNCTIONS +//// The functions below deal exclusivly with editing +//// of questions with question type MULTIANSWER. +//// Therefore they are kept in this file. +//// They are not in the class as they are not +//// likely to be subject for overriding. +///////////////////////////////////////////////////////////// + +function quiz_qtype_multianswer_extract_question($text) { + +//////////////////////////////////////////////// +//// Define some constants first. It is not the +//// pattern commonly used in quiz/questiontypes. +//// The reason is that it has been moved here from +//// quiz/format/multianswer/format.php +//////////////////////////////////////////////// + + // REGULAR EXPRESSION CONSTANTS + // I do not know any way to make this easier + // Regexes are always awkard when defined but more comprehensible + // when used as constants in the executive code + + // ANSWER_ALTERNATIVE regexes + + define("ANSWER_ALTERNATIVE_FRACTION_REGEX", + '=|%(-?[0-9]+)%'); + define("ANSWER_ALTERNATIVE_ANSWER_REGEX", + '[^~#}]+'); + define("ANSWER_ALTERNATIVE_FEEDBACK_REGEX", + '[^~}]*'); + define("ANSWER_ALTERNATIVE_REGEX", + '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' + . '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' + . '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?'); + + // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX + define("ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION", 2); + define("ANSWER_ALTERNATIVE_REGEX_FRACTION", 1); + define("ANSWER_ALTERNATIVE_REGEX_ANSWER", 3); + define("ANSWER_ALTERNATIVE_REGEX_FEEDBACK", 5); + + // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used + // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER + define("NUMBER_REGEX", + '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)'); + define("NUMERICAL_ALTERNATIVE_REGEX", + '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$'); + + // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX + define("NUMERICAL_CORRECT_ANSWER", 1); + define("NUMERICAL_ABS_ERROR_MARGIN", 6); + + // Remaining ANSWER regexes + define("ANSWER_TYPE_DEF_REGEX", + '(NUMERICAL|NM)|(MULTICHOICE|MC)|(SHORTANSWER|SA|MW)'); + define("ANSWER_START_REGEX", + '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):'); + + define("ANSWER_REGEX", + ANSWER_START_REGEX + . '(' . ANSWER_ALTERNATIVE_REGEX + . '(~' + . ANSWER_ALTERNATIVE_REGEX + . ')*)}' ); + + // Parenthesis positions for singulars in ANSWER_REGEX + define("ANSWER_REGEX_NORM", 1); + define("ANSWER_REGEX_ANSWER_TYPE_NUMERICAL", 3); + define("ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE", 4); + define("ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER", 5); + define("ANSWER_REGEX_ALTERNATIVES", 6); + +//////////////////////////////////////// +//// Start of the actual function +//////////////////////////////////////// + + $question = NULL; + $question->qtype= MULTIANSWER; + $question->questiontext= $text; + $question->answers= array(); + $question->defaultgrade = 0; // Will be increased for each answer norm + + for ($positionkey=1 + ; ereg(ANSWER_REGEX, $question->questiontext, $answerregs) + ; ++$positionkey ) + { + unset($multianswer); + + $multianswer->positionkey = $positionkey; + $multianswer->norm = $answerregs[ANSWER_REGEX_NORM] + or $multianswer->norm = '1'; + if ($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL]) { + $multianswer->answertype = NUMERICAL; + } else if($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER]) { + $multianswer->answertype = SHORTANSWER; + } else if($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE]){ + $multianswer->answertype = MULTICHOICE; + } else { + error("Cannot identify answertype $answerregs[2]"); + return false; + } + + $multianswer->alternatives= array(); + $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES]; + while (ereg(ANSWER_ALTERNATIVE_REGEX, $remainingalts, $altregs)) { + unset($alternative); + + if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) { + $alternative->fraction = '1'; + } else { + $alternative->fraction = .01 * + $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION] + or $alternative->fraction = '0'; + } + $alternative->feedback = $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK]; + if ($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL] + && ereg(NUMERICAL_ALTERNATIVE_REGEX, + $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], + $numregs) ) + { + $alternative->answer = $numregs[NUMERICAL_CORRECT_ANSWER]; + if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) { + $alternative->min = $numregs[NUMERICAL_CORRECT_ANSWER] + - $numregs[NUMERICAL_ABS_ERROR_MARGIN]; + $alternative->max = $numregs[NUMERICAL_CORRECT_ANSWER] + + $numregs[NUMERICAL_ABS_ERROR_MARGIN]; + } else { + $alternative->min = $numregs[NUMERICAL_CORRECT_ANSWER]; + $alternative->max = $numregs[NUMERICAL_CORRECT_ANSWER]; + } + } else { // Min and max must stay undefined... + $alternative->answer = + $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER]; + } + + $multianswer->alternatives[] = $alternative; + $tmp = explode($altregs[0], $remainingalts, 2); + $remainingalts = $tmp[1]; + } + + $question->defaultgrade += $multianswer->norm; + $question->answers[] = $multianswer; + $question->questiontext = implode("{#$positionkey}", + explode($answerregs[0], $question->questiontext, 2)); + } + return $question; +} + +function quiz_qtype_multianswer_save_alternatives($questionid, + $answertype, $alternatives, $oldalternativeids= NULL) { +// Returns false if something goes wrong, +// otherwise the ids of the answers. + + if (empty($oldalternativeids) + or !($oldalternatives = + get_records_list('quiz_answers', 'id', $oldalternativeids))) + { + $oldalternatives = array(); + } + + $alternativeids = array(); + + foreach ($alternatives as $altdata) { + + if ($altold = array_shift($oldalternatives)) { // Use existing one... + $alt = $altold; + $alt->answer = $altdata->answer; + $alt->fraction = $altdata->fraction; + $alt->feedback = $altdata->feedback; + if (!update_record("quiz_answers", $alt)) { + return false; + } + + } else { // Completely new one + unset($alt); + $alt->question= $questionid; + $alt->answer = $altdata->answer; + $alt->fraction = $altdata->fraction; + $alt->feedback = $altdata->feedback; + if (!($alt->id = insert_record("quiz_answers", $alt))) { + return false; + } + } + + // For the answer type numerical, each alternative has individual options: + if ($answertype == NUMERICAL) { + if ($numericaloptions = + get_record('quiz_numerical', 'answer', $alt->id)) + { + // Reuse existing numerical options + $numericaloptions->min = $altdata->min; + $numericaloptions->max = $altdata->max; + if (!update_record('quiz_numerical', $numericaloptions)) { + return false; + } + } else { + // New numerical options + $numericaloptions->answer = $alt->id; + $numericaloptions->question = $questionid; + $numericaloptions->min = $altdata->min; + $numericaloptions->max = $altdata->max; + if (!insert_record("quiz_numerical", $numericaloptions)) { + return false; + } + } + } else { // Delete obsolete numerical options + delete_records('quiz_numerical', 'answer', $alt->id); + } // end if NUMERICAL + + $alternativeids[] = $alt->id; + } // end foreach $alternatives + $answers = implode(',', $alternativeids); + + // Removal of obsolete alternatives from answers and quiz_numerical: + while ($altobsolete = array_shift($oldalternatives)) { + delete_records("quiz_answers", "id", $altobsolete->id); + + // Possibly obsolute numerical options are also to be deleted: + delete_records("quiz_numerical", 'answer', $altobsolete->id); + } + + // Common alternative options and removal of obsolete options + switch ($answertype) { + case NUMERICAL: + if (!empty($oldalternativeids)) { + delete_records('quiz_shortanswer', 'answers', +$oldalternativeids); + delete_records('quiz_multichoice', 'answers', +$oldalternativeids); + } + break; + case SHORTANSWER: + if (!empty($oldalternativeids)) { + delete_records('quiz_multichoice', 'answers', +$oldalternativeids); + $options = get_record('quiz_shortanswer', + 'answers', $oldalternativeids); + } else { + unset($options); + } + if (empty($options)) { + // Create new shortanswer options + $options->question = $questionid; + $options->usecase = 0; + $options->answers = $answers; + if (!insert_record('quiz_shortanswer', $options)) { + return false; + } + } else if ($answers != $oldalternativeids) { + // Shortanswer options needs update: + $options->answers = $answers; + if (!update_record('quiz_shortanswer', $options)) { + return false; + } + } + break; + case MULTICHOICE: + if (!empty($oldalternativeids)) { + delete_records('quiz_shortanswer', 'answers', +$oldalternativeids); + $options = get_record('quiz_multichoice', + 'answers', $oldalternativeids); + } else { + unset($options); + } + if (empty($options)) { + // Create new multichoice options + $options->question = $questionid; + $options->layout = 0; + $options->single = 1; + $options->answers = $answers; + if (!insert_record('quiz_multichoice', $options)) { + return false; + } + } else if ($answers != $oldalternativeids) { + // Multichoice options needs update: + $options->answers = $answers; + if (!update_record('quiz_multichoice', $options)) { + return false; + } + } + break; + default: + return false; + } + return $answers; +} + +?> diff --git a/mod/quiz/questiontypes/multichoice/editquestion.php b/mod/quiz/questiontypes/multichoice/editquestion.php new file mode 100644 index 0000000000..d313938c03 --- /dev/null +++ b/mod/quiz/questiontypes/multichoice/editquestion.php @@ -0,0 +1,23 @@ +id)) { + $options = get_record("quiz_multichoice", "question", $question->id); + } else { + $options->single = 1; + } + if (!empty($options->answers)) { + $answersraw = get_records_list("quiz_answers", "id", $options->answers); + } + for ($i=0; $i diff --git a/mod/quiz/questiontypes/multichoice/icon.gif b/mod/quiz/questiontypes/multichoice/icon.gif new file mode 100644 index 0000000000..d264c818ae Binary files /dev/null and b/mod/quiz/questiontypes/multichoice/icon.gif differ diff --git a/mod/quiz/questiontypes/multichoice/multichoice.html b/mod/quiz/questiontypes/multichoice/multichoice.html new file mode 100644 index 0000000000..5afb2566ba --- /dev/null +++ b/mod/quiz/questiontypes/multichoice/multichoice.html @@ -0,0 +1,124 @@ +
action="question.php"> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

:

+ id, true, true); ?> +

:

+ + +

:

+
+
+
+

+ +

+
+ "; + } + + print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); + + if ($usehtmleditor) { /// Trying this out for a while + echo ''; + } else { + echo "
"; + print_string("formattexttype"); + echo ": "; + if (!isset($question->questiontextformat)) { + $question->questiontextformat = FORMAT_MOODLE; + } + choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, ""); + helpbutton("textformat", get_string("helpformatting")); + echo "
"; + } + ?> +

:

+ image", get_string("none"),"",""); + } + ?> +

:

+ single", ""); + unset($menu); + ?> +

:

+

:

+    + fraction, ""); ?> +
+

:

+ +
 
+ + + +"> + +
+
+ diff --git a/mod/quiz/questiontypes/multichoice/questiontype.php b/mod/quiz/questiontypes/multichoice/questiontype.php new file mode 100644 index 0000000000..06c53cb2b6 --- /dev/null +++ b/mod/quiz/questiontypes/multichoice/questiontype.php @@ -0,0 +1,274 @@ +prefix}quiz_multichoice mc, + {$CFG->prefix}quiz_answers a + WHERE mc.question = '$question->id' + AND mc.question = a.question " + . $addedcondition); + } + + function name() { + return 'multichoice'; + } + + function save_question_options($question) { + + if (!$oldanswers = get_records("quiz_answers", "question", + $question->id, "id ASC")) { + $oldanswers = array(); + } + + // following hack to check at least two answers exist + $answercount = 0; + foreach ($question->answer as $key=>$dataanswer) { + if ($dataanswer != "") { + $answercount++; + } + } + $answercount += count($oldanswers); + if ($answercount < 2) { // check there are at lest 2 answers for multiple choice + $result->notice = get_string("notenoughanswers", "quiz", "2"); + return $result; + } + + + + // Insert all the new answers + + $totalfraction = 0; + $maxfraction = -1; + + $answers = array(); + + foreach ($question->answer as $key => $dataanswer) { + if ($dataanswer != "") { + if ($answer = array_shift($oldanswers)) { // Existing answer, so reuse it + $answer->answer = $dataanswer; + $answer->fraction = $question->fraction[$key]; + $answer->feedback = $question->feedback[$key]; + if (!update_record("quiz_answers", $answer)) { + $result->error = "Could not update quiz answer! (id=$answer->id)"; + return $result; + } + } else { + unset($answer); + $answer->answer = $dataanswer; + $answer->question = $question->id; + $answer->fraction = $question->fraction[$key]; + $answer->feedback = $question->feedback[$key]; + if (!$answer->id = insert_record("quiz_answers", $answer)) { + $result->error = "Could not insert quiz answer! "; + return $result; + } + } + $answers[] = $answer->id; + + if ($question->fraction[$key] > 0) { // Sanity checks + $totalfraction += $question->fraction[$key]; + } + if ($question->fraction[$key] > $maxfraction) { + $maxfraction = $question->fraction[$key]; + } + } + } + + if ($options = get_record("quiz_multichoice", "question", $question->id)) { + $options->answers = implode(",",$answers); + $options->single = $question->single; + if (!update_record("quiz_multichoice", $options)) { + $result->error = "Could not update quiz multichoice options! (id=$options->id)"; + return $result; + } + } else { + unset($options); + $options->question = $question->id; + $options->answers = implode(",",$answers); + $options->single = $question->single; + if (!insert_record("quiz_multichoice", $options)) { + $result->error = "Could not insert quiz multichoice options!"; + return $result; + } + } + + /// Perform sanity checks on fractional grades + if ($options->single) { + if ($maxfraction != 1) { + $maxfraction = $maxfraction * 100; + $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction); + return $result; + } + } else { + $totalfraction = round($totalfraction,2); + if ($totalfraction != 1) { + $totalfraction = $totalfraction * 100; + $result->noticeyesno = get_string("fractionsaddwrong", "quiz", $totalfraction); + return $result; + } + } + return true; + } + + function extract_response($rawresponse, $nameprefix) { + // Fetch additional details from the database... + if (!$options = get_record("quiz_multichoice", + "question", $rawresponse->question)) { + notify("Error: Missing question options!"); + } + + if ($options->single) { + return array($nameprefix => $rawresponse->answer); + + } else { + $response = array(); + $answerids = explode(',', $options->answers); + foreach ($answerids as $answerid) { + $response[$nameprefix.$answerid] = + ereg("(,|^)$answerid(,|$)", $rawresponse->answer) + ? $answerid + : ''; + } + return $response; + } + } + + function print_question_formulation_and_controls($question, + $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + + // Fetch additional details from the database... + if (!$options = get_record("quiz_multichoice", "question", $question->id)) { + notify("Error: Missing question options!"); + } + if (!$answers = get_records_list("quiz_answers", "id", $options->answers)) { + notify("Error: Missing question answers!"); + } + + // Print formulation + echo format_text($question->questiontext, + $question->questiontextformat, + NULL, $quiz->course); + quiz_print_possible_question_image($quiz->id, $question); + + // Print input controls and alternatives + echo ""; + $stranswer = get_string("answer", "quiz"); + echo "
$stranswer:  "; + echo ""; + $answerids = explode(",", $options->answers); + + if ($quiz->shuffleanswers) { + $answerids = swapshuffle($answerids); + } + + // Handle the case of unanswered single-choice questions: + if ($options->single) { + $singleresponse = isset($question->response[$nameprefix]) + ? $question->response[$nameprefix] : '0'; + } + + foreach ($answerids as $key => $aid) { + $answer = $answers[$aid]; + $qnumchar = chr(ord('a') + $key); + + echo '"; + if ($readonly and $quiz->correctanswers || $quiz->feedback + and !empty($correctanswers[$nameprefix.$aid])) { + echo ''; + } else { + echo ''; + } + if ($quiz->feedback) { + echo ""; + } + echo ""; + } + echo "
'; + + if ($options->single) { + $type = ' type="radio" '; + $name = " name=\"$nameprefix\" "; + $checked = $singleresponse == $aid + ? ' checked="checked" ' : ''; + } else { + $type = ' type="checkbox" '; + $name = " name=\"$nameprefix$aid\" "; + $checked = !empty($question->response[$nameprefix.$aid]) + ? ' checked="checked" ' : ''; + } + if ($readonly) { + $readonly = ' readonly="readonly" disabled="disabled" '; + } + echo "id\" />"; + + echo "'.format_text("$qnumchar. $answer->answer").''.format_text("$qnumchar. $answer->answer").' "; + if ($checked) { // Simpliest condition to use here + quiz_print_comment($answer->feedback); + } + echo "
"; + echo "
"; + } + + function grade_response($question, $nameprefix, $addedanswercondition='') { + + $result->correctanswers = array(); + $result->answers = array(); + $result->grade = 0.0; + + $answers = $this->get_answers($question, $addedanswercondition); + + /// Set ->answers[] and ->grade + if (!empty($question->response)) { + foreach ($question->response as $name => $response) { + if (isset($answers[$response])) { + $result->answers[$name] = $answers[$response]; + $result->grade += $answers[$response]->fraction; + } + } + } + + /// Set ->correctanswers[] + foreach ($answers as $answer) { + + if ($answer->single) { + $result->correctanswers = + quiz_extract_correctanswers($answers, $nameprefix); + break; + + } else { + if ($answer->fraction > 0.0) { + $result->correctanswers[$nameprefix.$answer->id] = $answer; + } + } + } + return $result; + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[MULTICHOICE]= new quiz_multichoice_qtype(); + +?> diff --git a/mod/quiz/questiontypes/numerical/editquestion.php b/mod/quiz/questiontypes/numerical/editquestion.php new file mode 100644 index 0000000000..6d1ace119a --- /dev/null +++ b/mod/quiz/questiontypes/numerical/editquestion.php @@ -0,0 +1,25 @@ +id)) { + $answersraw= quiz_get_answers($question); + } + $answers= array(); + for ($i=0; $i<6; $i++) { + $answers[$i]->answer = ""; // Make answer slots, default as blank... + $answers[$i]->min = ""; + $answers[$i]->max = ""; + $answers[$i]->feedback = ""; + } + if (!empty($answersraw)) { + $i=0; + foreach ($answersraw as $answer) { + $answers[$i] = $answer; + $i++; + } + } + print_heading_with_help(get_string("editingnumerical", "quiz"), "numerical", "quiz"); + require("numerical.html"); + +?> diff --git a/mod/quiz/questiontypes/numerical/icon.gif b/mod/quiz/questiontypes/numerical/icon.gif new file mode 100644 index 0000000000..28780428e1 Binary files /dev/null and b/mod/quiz/questiontypes/numerical/icon.gif differ diff --git a/mod/quiz/questiontypes/numerical/numerical.html b/mod/quiz/questiontypes/numerical/numerical.html new file mode 100644 index 0000000000..2488599453 --- /dev/null +++ b/mod/quiz/questiontypes/numerical/numerical.html @@ -0,0 +1,141 @@ +
action="question.php"> +
+ + + + + + + + + + + + + + + + + + + + + min) && is_numeric($answers[0]->answer)) { + $acceptederror = (float)($answers[0]->answer) + - (float)($answers[0]->min); + } else { + $acceptederror = ""; + } + ?> + + + + + + + + + + +

:

+ id, true, true); ?> +

:

+ + +

:

+
+
+
+

+ +

+
+ "; + } + + print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); + + if ($usehtmleditor) { /// Trying this out for a while + echo ''; + } else { + echo "
"; + print_string("formattexttype"); + echo ": "; + if (!isset($question->questiontextformat)) { + $question->questiontextformat = FORMAT_MOODLE; + } + choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, ""); + helpbutton("textformat", get_string("helpformatting")); + echo "
"; + } + ?> +

:

+ image", get_string("none"),"",""); + } + ?> +

:

+    +

:

+ ± + + + + +
+

:

+ +
+ + + +"> +
+
+ + + diff --git a/mod/quiz/questiontypes/numerical/questiontype.php b/mod/quiz/questiontypes/numerical/questiontype.php new file mode 100644 index 0000000000..77552108e5 --- /dev/null +++ b/mod/quiz/questiontypes/numerical/questiontype.php @@ -0,0 +1,178 @@ +dirroot/mod/quiz/questiontypes/shortanswer/questiontype.php"); + +class quiz_numerical_qtype extends quiz_shortanswer_qtype { + + function get_answers($question, $addedcondition='') { + // The added condition is one addition that has been added + // to the behaviour of this question type in order to make + // it embeddable within a multianswer (embedded cloze) question + + global $CFG; + + // There can be multiple answers + return get_records_sql("SELECT a.*, n.min, n.max + FROM {$CFG->prefix}quiz_numerical n, + {$CFG->prefix}quiz_answers a + WHERE a.question = '$question->id' + AND n.answer = a.id " + . $addedcondition); + } + + function name() { + return 'numerical'; + } + + function save_question_options($question) { + + if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { + $oldanswers = array(); + } + + $answers = array(); + $maxfraction = -1; + + // Insert all the new answers + foreach ($question->answer as $key => $dataanswer) { + if ($dataanswer != "") { + if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it + $answer = $oldanswer; + $answer->answer = $dataanswer; + $answer->fraction = $question->fraction[$key]; + $answer->feedback = $question->feedback[$key]; + if (!update_record("quiz_answers", $answer)) { + $result->error = "Could not update quiz answer! (id=$answer->id)"; + return $result; + } + } else { // This is a completely new answer + unset($answer); + $answer->answer = $dataanswer; + $answer->question = $question->id; + $answer->fraction = $question->fraction[$key]; + $answer->feedback = $question->feedback[$key]; + if (!$answer->id = insert_record("quiz_answers", $answer)) { + $result->error = "Could not insert quiz answer!"; + return $result; + } + } + $answers[] = $answer->id; + if ($question->fraction[$key] > $maxfraction) { + $maxfraction = $question->fraction[$key]; + } + + if ($options = get_record("quiz_numerical", "answer", $answer->id)) { + $options->min= $question->min[$key]; + $options->max= $question->max[$key]; + if (!update_record("quiz_numerical", $options)) { + $result->error = "Could not update quiz numerical options! (id=$options->id)"; + return $result; + } + } else { // completely new answer + unset($options); + $options->question = $question->id; + $options->answer = $answer->id; + $options->min = $question->min[$key]; + $options->max = $question->max[$key]; + if (!insert_record("quiz_numerical", $options)) { + $result->error = "Could not insert quiz numerical options!"; + return $result; + } + } + } + } + + /// Perform sanity checks on fractional grades + if ($maxfraction != 1) { + $maxfraction = $maxfraction * 100; + $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction); + return $result; + } else { + return true; + } + } + + function grade_response($question, $nameprefix, $addedanswercondition='') { + + $result->answers = array(); + if (isset($question->response[$nameprefix])) { + $response = trim(stripslashes($question->response[$nameprefix])); + if (!is_numeric($response) and is_numeric( + $tmp = str_replace(',', '.', $response))) { + /// I haven't ever needed to make a workaround like this + /// before, I have no idea why I need to do it now... + $response = $tmp; + } + } else { + $response = NULL; + } + $answers = $this->get_answers($question, $addedanswercondition); + foreach ($answers as $answer) { + + /// Check if response matches answer... + if ('' != $response and empty($result->answers) + || $answer->fraction + > $result->answers[$nameprefix]->fraction + and strtolower($response) == strtolower($answer->answer) + || '' != trim($answer->min) + && ((float)$response >= (float)$answer->min) + && ((float)$response <= (float)$answer->max)) { + $result->answers[$nameprefix] = $answer; + } + } + + $result->grade = isset($result->answers[$nameprefix]) + ? $result->answers[$nameprefix]->fraction + : 0.0; + $result->correctanswers = quiz_extract_correctanswers($answers, + $nameprefix); + + ///////////////////////////////////////////////// + // For numerical answer we have the policy to + // set feedback for any response, even it the + // response does not entitles the student to it. + ///////////////////////////////////////////////// + if ('' !== $response and empty($result->answers) + || empty($result->answers[$nameprefix]->feedback)) { + // Look for just any feedback: + foreach ($result->correctanswers as $correctanswer) { + if ($correctanswer->feedback) { + $result->answers[$nameprefix]->feedback = + $correctanswer->feedback; + if (empty($result->answers[$nameprefix]->id)) { + // Better fake an answer as well: + $result->answers[$nameprefix]->id = 0; + $result->answers[$nameprefix]->answer = $response; + $result->answers[$nameprefix]->fraction = 0.0; + $result->answers[$nameprefix]->question = $question->id; + } + break; + } + } + } + + return $result; + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[NUMERICAL]= new quiz_numerical_qtype(); + +?> diff --git a/mod/quiz/questiontypes/random/editquestion.php b/mod/quiz/questiontypes/random/editquestion.php new file mode 100644 index 0000000000..b6737f31b3 --- /dev/null +++ b/mod/quiz/questiontypes/random/editquestion.php @@ -0,0 +1,6 @@ + diff --git a/mod/quiz/questiontypes/random/icon.gif b/mod/quiz/questiontypes/random/icon.gif new file mode 100644 index 0000000000..46dac35cb2 Binary files /dev/null and b/mod/quiz/questiontypes/random/icon.gif differ diff --git a/mod/quiz/questiontypes/random/questiontype.php b/mod/quiz/questiontypes/random/questiontype.php new file mode 100644 index 0000000000..6bced4e9b5 --- /dev/null +++ b/mod/quiz/questiontypes/random/questiontype.php @@ -0,0 +1,206 @@ +response as $key => $response) { + if (ereg('[^0-9][0-9]+random$', $key)) { + $randomquestion = get_record('quiz_questions', + 'id', $response); + $randomquestion->response = $question->response; + unset($randomquestion->response[$key]); + if ($subwrapped = $QUIZ_QTYPES[$randomquestion->qtype] + ->wrapped_questions($randomquestion)) { + return "$response,$subwrapped"; + } else { + return $response; + } + } + } + return false; + } + + function convert_to_response_answer_field($questionresponse) { + /// THIS IS PART OF A WORKAROUND AS THIS IS THE ONLY + /// CASE WHERE IT IS NEEDED TO STORE TWO RESPONSE RECORDS... + + global $QUIZ_QTYPES; + + foreach ($questionresponse as $key => $response) { + if (ereg('[^0-9][0-9]+random$', $key)) { + unset($questionresponse[$key]); + $randomquestion = get_record('quiz_questions', + 'id', $response); + return "random$response-" + .$QUIZ_QTYPES[$randomquestion->qtype] + ->convert_to_response_answer_field($questionresponse); + } + } + return ''; + } + + function create_response($question, $nameprefix, $questionsinuse) { + // It's for question types like RANDOMSAMATCH and RANDOM that + // the true power of the pattern with this function comes to the surface. + // This implementation will stand even after a possible exclusion of + // the funtions extract_response and convert_to_response_answer_field + global $CFG; + + if (!isset($this->catrandoms[$question->category])) { + //Need to fetch random questions from category $question->category" + + $possiblerandomqtypes = "'" + . implode("','", $this->possiblerandomqtypes) . "'"; + $this->catrandoms[$question->category] = get_records_sql + ("SELECT * FROM {$CFG->prefix}quiz_questions + WHERE category = '$question->category' + AND id NOT IN ($questionsinuse) + AND qtype IN ($possiblerandomqtypes)"); + shuffle($this->catrandoms[$question->category]); + } + + while ($randomquestion = + array_pop($this->catrandoms[$question->category])) { + if (!ereg("(^|,)$randomquestion->id(,|$)", $questionsinuse)) { + /// $randomquestion is not in use and will therefore be used + /// as the randomquestion here... + + global $QUIZ_QTYPES; + $response = $QUIZ_QTYPES[$randomquestion->qtype] + ->create_response($randomquestion, + quiz_qtype_nameprefix($randomquestion, $nameprefix), + "$questionsinuse,$randomquestion->id"); + $response[$nameprefix] = $randomquestion->id; + return $response; + } + } + notify(get_string('toomanyrandom', 'quiz', $question->category)); + return array(); + } + + function extract_response($rawresponse, $nameprefix) { + global $QUIZ_QTYPES; + if ($randomquestion = get_record('quiz_questions', + 'id', $rawresponse->answer)) { + if ($randomresponse = get_record + ('quiz_responses', 'question', $rawresponse->answer, + 'attempt', $rawresponse->attempt)) { + + /// The prefered case: + // The response field for the random question was found + // the response array can be extracted: + + $response = $QUIZ_QTYPES[$randomquestion->qtype] + ->extract_response($randomresponse, + quiz_qtype_nameprefix($randomquestion, $nameprefix)); + + } else { + notify("Error: Cannot find response to random question $randomquestion->id"); + + /// Instead: workaround by creating a new response: + $response = $QUIZ_QTYPES[$randomquestion->qtype] + ->create_response($randomquestion, + quiz_qtype_nameprefix($randomquestion, $nameprefix), + "$rawresponse->question,$rawresponse->answer"); + // (That last argument is instead of $questionsinuse. + // It is not correct but it would be very messy to + // determine the correct value, while very few + // question types actually use it and they who do have + // good chances to execute properly anyway.) + } + $response[$nameprefix] = $randomquestion->id; + return $response; + } else { + notify("Error: Unable to find random question $rawresponse->question"); + /// No new random question is picked as this is probably + /// not what the moodle user has in mind anyway + return array(); + } + } + + function print_question_formulation_and_controls($question, + $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + global $QUIZ_QTYPES; + + // Get the wrapped question... + if ($actualquestion = $this->get_wrapped_question($question, + $nameprefix)) { + echo ''; + return $QUIZ_QTYPES[$actualquestion->qtype] + ->print_question_formulation_and_controls($actualquestion, + $quiz, $readonly, $answers, $correctanswers, + quiz_qtype_nameprefix($actualquestion, $nameprefix)); + } else { + echo '

' . get_string('random', 'quiz') . '

'; + } + } + + function get_wrapped_question($question, $nameprefix) { + if (!empty($question->response[$nameprefix]) + and $actualquestion = get_record('quiz_questions', + 'id', $question->response[$nameprefix], + // The category check is a security check + 'category', $question->category)) { + $actualquestion->response = $question->response; + unset($actualquestion->response[$nameprefix]); + $actualquestion->maxgrade = $question->maxgrade; + return $actualquestion; + } else { + return false; + } + } + + function grade_response($question, $nameprefix) { + global $QUIZ_QTYPES; + + // Get the wrapped question... + if ($actualquestion = $this->get_wrapped_question($question, + $nameprefix)) { + return $QUIZ_QTYPES[$actualquestion->qtype]->grade_response( + $actualquestion, + quiz_qtype_nameprefix($actualquestion, $nameprefix)); + } else { + $result->grade = 0.0; + $result->answers = array(); + $result->correctanswers = array(); + return $result; + } + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[RANDOM]= new quiz_random_qtype(); + +?> diff --git a/mod/quiz/questiontypes/random/random.html b/mod/quiz/questiontypes/random/random.html new file mode 100644 index 0000000000..8c453bc4be --- /dev/null +++ b/mod/quiz/questiontypes/random/random.html @@ -0,0 +1,30 @@ +
+
+ + + + + + + + + + +

:

+ id, true, true); ?> +

:

+ name)) { + $question->name = get_string("random", "quiz"); + } ?> + + +
+ + + + + +"> +
+
+ diff --git a/mod/quiz/questiontypes/randomsamatch/editquestion.php b/mod/quiz/questiontypes/randomsamatch/editquestion.php new file mode 100644 index 0000000000..4e89551b49 --- /dev/null +++ b/mod/quiz/questiontypes/randomsamatch/editquestion.php @@ -0,0 +1,11 @@ +id)) { + $options = get_record("quiz_randomsamatch", "question", $question->id); + } else { + $options->choose = ""; + } + $numberavailable = count_records("quiz_questions", "category", $category->id, "qtype", SHORTANSWER); + print_heading_with_help(get_string("editingrandomsamatch", "quiz"), "randomsamatch", "quiz"); + require("randomsamatch.html"); + +?> diff --git a/mod/quiz/questiontypes/randomsamatch/icon.gif b/mod/quiz/questiontypes/randomsamatch/icon.gif new file mode 100644 index 0000000000..68543632a2 Binary files /dev/null and b/mod/quiz/questiontypes/randomsamatch/icon.gif differ diff --git a/mod/quiz/questiontypes/randomsamatch/questiontype.php b/mod/quiz/questiontypes/randomsamatch/questiontype.php new file mode 100644 index 0000000000..d84cfbd8a3 --- /dev/null +++ b/mod/quiz/questiontypes/randomsamatch/questiontype.php @@ -0,0 +1,231 @@ +question = $question->id; + $options->choose = $question->choose; + if ($existing = get_record("quiz_randomsamatch", + "question", $options->question)) { + $options->id = $existing->id; + if (!update_record("quiz_randomsamatch", $options)) { + $result->error = "Could not update quiz randomsamatch options!"; + return $result; + } + } else { + if (!insert_record("quiz_randomsamatch", $options)) { + $result->error = "Could not insert quiz randomsamatch options!"; + return $result; + } + } + return true; + } + + function wrapped_questions($question) { + if (empty($question->response)) { + return false; + } else { + $wrapped = ''; + $delimiter = ''; + foreach ($question->response as $rkey => $response) { + $wrapped .= $delimiter.$this->extract_response_id($rkey); + $delimiter = ','; + } + return $wrapped; + } + } + + function create_response($question, $nameprefix, $questionsinuse) { + // It's for question types like RANDOMSAMATCH and RANDOM that + // the true power of the pattern with this function comes to the surface. + // This implementation will stand even after a possible exclusion of + // the funtions extract_response and convert_to_response_answer_field + + if (!isset($this->catrandoms[$question->category])) { + /// Need to fetch the shortanswer question ids for the category: + + $saquestions = get_records_select('quiz_questions', + " category='$question->category' + AND qtype='".SHORTANSWER."' + AND id NOT IN ($questionsinuse) "); + $this->catrandoms[$question->category] = array_keys($saquestions); + shuffle($this->catrandoms[$question->category]); + } + + /// Access question options to find out how many short-answer + /// questions we are supposed to pick... + if ($options = get_record('quiz_randomsamatch', + 'question', $question->id)) { + $questionstopick = $options->choose; + } else { + notify("Error: Missing question options! - Try to pick two shortanswer questions anyway"); + $questionstopick = 2; + } + + /// Pick the short-answer question ids and create the $response array + $response = array(); + while ($questionstopick) { + $said = array_pop($this->catrandoms[$question->category]); + if (!ereg("(^|,)$said(,|$)", $questionsinuse)) { + $response[$nameprefix.$said] = '0'; + --$questionstopick; + } + } + + if ($questionstopick) { + notify("Error: could not get enough Short-Answer questions!"); + $count = count($response); + $wanted = $count + $questionstopick; + notify("Got $count Short-Answer questions, but wanted $wanted."); + } + + return $response; + } + + function extract_response($rawresponse, $nameprefix) { + /// Simple implementation that does not check with the database + /// and thus - does not bother to check whether there has been + /// any changes to the question options. + $response = array(); + $rawitems = explode(',', $rawresponse->answer); + foreach ($rawitems as $rawitem) { + $splits = explode('-', $rawitem, 2); + $response[$nameprefix.$splits[0]] = $splits[1]; + } + return $response; + } + + function print_question_formulation_and_controls($question, + $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + + // Print question formulation + + echo format_text($question->questiontext, + $question->questiontextformat, NULL, $quiz->course); + quiz_print_possible_question_image($quiz->id, $question); + + // Summarize shortanswer questions answer alternatives: + if (empty($correctanswers)) { + // Get them using the grade_response method + $tempresult = $this->grade_response($question, $nameprefix); + $saanswers = $tempresult->correctanswers; + } else { + $saanswers = $correctanswers; + } + foreach ($saanswers as $key => $saanswer) { + unset($saanswers[$key]); // Unsets the nameprefix occurence + $saanswers[$saanswer->id] = trim($saanswer->answer); + } + $saanswers = draw_rand_array($saanswers, count($saanswers)); + + // Print the shortanswer questions and input controls: + echo ''; + foreach ($question->response as $inputname => $response) { + if (!($saquestion = get_record('quiz_questions', 'id', + quiz_extract_posted_id($inputname, $nameprefix)))) { + notify("Error: cannot find shortanswer question for $inputname "); + continue; + } + + echo ''; + echo ''; + } + echo '
'; + echo $saquestion->questiontext; + echo ''; + if (!empty($correctanswers) + && $correctanswers[$inputname]->id == $response) { + echo ''; + choose_from_menu($saanswers, $inputname, $response); + echo '
'; + } else { + choose_from_menu($saanswers, $inputname, $response); + if ($readonly && $quiz->correctanswers + && isset($correctanswer[$inputname])) { + quiz_print_correctanswer($correctanswer[$inputname]->answer); + } + } + if ($quiz->feedback && isset($answers[$inputname]) + && $answers[$inputname]->feedback) { + quiz_print_comment($answers[$inputname]->feedback); + } + echo '
'; + } + + function grade_response($question, $nameprefix) { + global $QUIZ_QTYPES; + + $result->answers = array(); + $result->correctanswers = array(); + $result->grade = 0.0; + + foreach ($question->response as $inputname => $subresponse) { + if ($subquestion = get_record('quiz_questions', + 'id', quiz_extract_posted_id($inputname, $nameprefix), + // These two query conditions are security checks that prevents cheating... + 'qtype', SHORTANSWER, + 'category', $question->category)) { + + if ($subresponse = get_record('quiz_answers', + 'id', $subresponse)) { + $subquestion->response[$inputname] = $subresponse->answer; + } else { + $subquestion->response[$inputname] = ''; + } + + // Use the shortanswer framework to for grading... + $subresult = $QUIZ_QTYPES[SHORTANSWER] + ->grade_response($subquestion, $inputname); + + // Summarize shortanswer results + if (isset($subresult->answers[$inputname])) { + $result->answers[$inputname] = + $subresult->answers[$inputname]; + $result->grade += $result->answers[$inputname]->fraction; + if ($result->answers[$inputname]->fraction >= 1.0) { + $result->correctanswers[$inputname] = + $result->answers[$inputname]; + continue; + } + } + // Pick the first correctanswer: + foreach ($subresult->correctanswers as $correct) { + $result->correctanswers[$inputname] = $correct; + break; + } + } + } + if ($result->grade) { + $result->grade /= count($question->response); + } + return $result; + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[RANDOMSAMATCH]= new quiz_randomsamatch_qtype(); + +?> diff --git a/mod/quiz/questiontypes/randomsamatch/randomsamatch.html b/mod/quiz/questiontypes/randomsamatch/randomsamatch.html new file mode 100644 index 0000000000..e763e604cf --- /dev/null +++ b/mod/quiz/questiontypes/randomsamatch/randomsamatch.html @@ -0,0 +1,98 @@ +
action="question.php"> +
+ + + + + + + + + + + +
+
+
+

+ +

+ + + + + + + +

:

+ category]; ?> + category"; ?>"> +

:

+ name)) { + $question->name = get_string("randomsamatch", "quiz"); + } + ?> + + +

:

+ "; + } + + if (empty($question->questiontext)) { + $question->questiontext = get_string("randomsamatchintro", "quiz"); + } + print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); + + if ($usehtmleditor) { /// Trying this out for a while + echo ''; + } else { + echo "
"; + print_string("formattexttype"); + echo ": "; + if (!$question->questiontextformat) { + $question->questiontextformat = FORMAT_MOODLE; + } + choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, ""); + helpbutton("textformat", get_string("helpformatting")); + echo "
"; + } + ?> +

:

+ choose", ""); + unset($menu); + ?> +
+ + + +"> + +
+
+ diff --git a/mod/quiz/questiontypes/shortanswer/editquestion.php b/mod/quiz/questiontypes/shortanswer/editquestion.php new file mode 100644 index 0000000000..3635b73ee1 --- /dev/null +++ b/mod/quiz/questiontypes/shortanswer/editquestion.php @@ -0,0 +1,23 @@ +id)) { + $options = get_record("quiz_shortanswer", "question", $question->id); + } else { + $options->usecase = 0; + } + if (!empty($options->answers)) { + $answersraw = get_records_list("quiz_answers", "id", $options->answers); + } + for ($i=0; $i diff --git a/mod/quiz/questiontypes/shortanswer/icon.gif b/mod/quiz/questiontypes/shortanswer/icon.gif new file mode 100644 index 0000000000..747eb01e64 Binary files /dev/null and b/mod/quiz/questiontypes/shortanswer/icon.gif differ diff --git a/mod/quiz/questiontypes/shortanswer/questiontype.php b/mod/quiz/questiontypes/shortanswer/questiontype.php new file mode 100644 index 0000000000..dd826fc001 --- /dev/null +++ b/mod/quiz/questiontypes/shortanswer/questiontype.php @@ -0,0 +1,198 @@ +prefix}quiz_shortanswer sa, + {$CFG->prefix}quiz_answers a + WHERE sa.question = '$question->id' + AND sa.question = a.question " + . $addedcondition); + + } + + function name() { + return 'shortanswer'; + } + + function save_question_options($question) { + if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { + $oldanswers = array(); + } + + $answers = array(); + $maxfraction = -1; + + // Insert all the new answers + foreach ($question->answer as $key => $dataanswer) { + if ($dataanswer != "") { + if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it + $answer = $oldanswer; + $answer->answer = trim($dataanswer); + $answer->fraction = $question->fraction[$key]; + $answer->feedback = $question->feedback[$key]; + if (!update_record("quiz_answers", $answer)) { + $result->error = "Could not update quiz answer! (id=$answer->id)"; + return $result; + } + } else { // This is a completely new answer + unset($answer); + $answer->answer = trim($dataanswer); + $answer->question = $question->id; + $answer->fraction = $question->fraction[$key]; + $answer->feedback = $question->feedback[$key]; + if (!$answer->id = insert_record("quiz_answers", $answer)) { + $result->error = "Could not insert quiz answer!"; + return $result; + } + } + $answers[] = $answer->id; + if ($question->fraction[$key] > $maxfraction) { + $maxfraction = $question->fraction[$key]; + } + } + } + + if ($options = get_record("quiz_shortanswer", "question", $question->id)) { + $options->answers = implode(",",$answers); + $options->usecase = $question->usecase; + if (!update_record("quiz_shortanswer", $options)) { + $result->error = "Could not update quiz shortanswer options! (id=$options->id)"; + return $result; + } + } else { + unset($options); + $options->question = $question->id; + $options->answers = implode(",",$answers); + $options->usecase = $question->usecase; + if (!insert_record("quiz_shortanswer", $options)) { + $result->error = "Could not insert quiz shortanswer options!"; + return $result; + } + } + + /// Perform sanity checks on fractional grades + if ($maxfraction != 1) { + $maxfraction = $maxfraction * 100; + $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction); + return $result; + } else { + return true; + } + } + + function print_question_formulation_and_controls($question, + $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + /// This implementation is also used by question type NUMERICAL + + /// Print question text and media + + echo format_text($question->questiontext, + $question->questiontextformat, + NULL, $quiz->course); + quiz_print_possible_question_image($quiz->id, $question); + + /// Print input controls + + $stranswer = get_string("answer", "quiz"); + if (isset($question->response[$nameprefix])) { + $value = ' value="'.$question->response[$nameprefix].'" '; + } else { + $value = ' value="" '; + } + $inputname = ' name="'.$nameprefix.'" '; + echo "

$stranswer:

"; + + if ($quiz->feedback && isset($answers[$nameprefix]) + && $feedback = $answers[$nameprefix]->feedback) { + quiz_print_comment("

$feedback

"); + } + if ($readonly && $quiz->correctanswers) { + $delimiter = ''; + $correct = ''; + foreach ($correctanswers as $correctanswer) { + $correct .= $delimiter.$correctanswer->answer; + $delimiter = ', '; + } + quiz_print_correctanswer($correct); + } + } + + function grade_response($question, $nameprefix, $addedanswercondition='') { + + if (isset($question->response[$nameprefix])) { + $response0 = trim(stripslashes($question->response[$nameprefix])); + } else { + $response0 = ''; + } + $answers = $this->get_answers($question, $addedanswercondition); + + /// Determine ->answers[] + $result->answers = array(); + if ('' !== $response0) { + foreach ($answers as $answer) { + + $answer->answer = trim($answer->answer); // Just in case + + if (empty($result->answers) || $answer->fraction + > $result->answers[$nameprefix]->fraction) { + + if (!$answer->usecase) { // Don't compare case + $response0 = strtolower($response0); + $answer0 = strtolower($answer->answer); + } else { + $answer0 = $answer->answer; + } + + if (strpos(' '.$answer0, '*')) { + $answer0 = str_replace('\*','@@@@@@',$answer0); + $answer0 = str_replace('*','.*',$answer0); + $answer0 = str_replace('@@@@@@', '\*',$answer0); + $answer0 = str_replace('+', '\+',$answer0); + if (ereg('^'.$answer0.'$', $response0)) { + $result->answers[$nameprefix] = $answer; + } + + } else if ($answer0 == $response0) { + $result->answers[$nameprefix] = $answer; + } + } + } + } + + + $result->grade = isset($result->answers[$nameprefix]) + ? $result->answers[$nameprefix]->fraction + : 0.0; + $result->correctanswers = quiz_extract_correctanswers($answers, + $nameprefix); + return $result; + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[SHORTANSWER]= new quiz_shortanswer_qtype(); + +?> diff --git a/mod/quiz/questiontypes/shortanswer/shortanswer.html b/mod/quiz/questiontypes/shortanswer/shortanswer.html new file mode 100644 index 0000000000..ce621449dc --- /dev/null +++ b/mod/quiz/questiontypes/shortanswer/shortanswer.html @@ -0,0 +1,126 @@ +
action="question.php"> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

:

+ id, true, true); ?> +

:

+ + +

:

+
+
+
+

+ +

+
+ "; + } + + print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); + + if ($usehtmleditor) { /// Trying this out for a while + echo ''; + } else { + echo "
"; + print_string("formattexttype"); + echo ": "; + if (!isset($question->questiontextformat)) { + $question->questiontextformat = FORMAT_MOODLE; + } + choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, ""); + helpbutton("textformat", get_string("helpformatting")); + echo "
"; + } + ?> +

:

+ image", get_string("none"),"",""); + } + ?> +

:

+ usecase", ""); + ?> +

:

+

+

:

+    + fraction,""); ?> +
+

:

+ +
 
+ + + +"> + +
+
+ diff --git a/mod/quiz/questiontypes/truefalse/editquestion.php b/mod/quiz/questiontypes/truefalse/editquestion.php new file mode 100644 index 0000000000..c98f55e2ac --- /dev/null +++ b/mod/quiz/questiontypes/truefalse/editquestion.php @@ -0,0 +1,28 @@ +id)) { + $options = get_record("quiz_truefalse", "question", "$question->id"); + } + if (!empty($options->trueanswer)) { + $true = get_record("quiz_answers", "id", $options->trueanswer); + } else { + $true->fraction = 1; + $true->feedback = ""; + } + if (!empty($options->falseanswer)) { + $false = get_record("quiz_answers", "id", "$options->falseanswer"); + } else { + $false->fraction = 0; + $false->feedback = ""; + } + + if ($true->fraction > $false->fraction) { + $question->answer = 1; + } else { + $question->answer = 0; + } + + print_heading_with_help(get_string("editingtruefalse", "quiz"), "truefalse", "quiz"); + require("truefalse.html"); + +?> diff --git a/mod/quiz/questiontypes/truefalse/icon.gif b/mod/quiz/questiontypes/truefalse/icon.gif new file mode 100644 index 0000000000..49964330b8 Binary files /dev/null and b/mod/quiz/questiontypes/truefalse/icon.gif differ diff --git a/mod/quiz/questiontypes/truefalse/questiontype.php b/mod/quiz/questiontypes/truefalse/questiontype.php new file mode 100644 index 0000000000..30e8f6472e --- /dev/null +++ b/mod/quiz/questiontypes/truefalse/questiontype.php @@ -0,0 +1,179 @@ +id, "id ASC")) { + $oldanswers = array(); + } + + if ($true = array_shift($oldanswers)) { // Existing answer, so reuse it + $true->answer = get_string("true", "quiz"); + $true->fraction = $question->answer; + $true->feedback = $question->feedbacktrue; + if (!update_record("quiz_answers", $true)) { + $result->error = "Could not update quiz answer \"true\")!"; + return $result; + } + } else { + unset($true); + $true->answer = get_string("true", "quiz"); + $true->question = $question->id; + $true->fraction = $question->answer; + $true->feedback = $question->feedbacktrue; + if (!$true->id = insert_record("quiz_answers", $true)) { + $result->error = "Could not insert quiz answer \"true\")!"; + return $result; + } + } + + if ($false = array_shift($oldanswers)) { // Existing answer, so reuse it + $false->answer = get_string("false", "quiz"); + $false->fraction = 1 - (int)$question->answer; + $false->feedback = $question->feedbackfalse; + if (!update_record("quiz_answers", $false)) { + $result->error = "Could not insert quiz answer \"false\")!"; + return $result; + } + } else { + unset($false); + $false->answer = get_string("false", "quiz"); + $false->question = $question->id; + $false->fraction = 1 - (int)$question->answer; + $false->feedback = $question->feedbackfalse; + if (!$false->id = insert_record("quiz_answers", $false)) { + $result->error = "Could not insert quiz answer \"false\")!"; + return $result; + } + } + + if ($options = get_record("quiz_truefalse", "question", $question->id)) { + // No need to do anything, since the answer IDs won't have changed + // But we'll do it anyway, just for robustness + $options->trueanswer = $true->id; + $options->falseanswer = $false->id; + if (!update_record("quiz_truefalse", $options)) { + $result->error = "Could not update quiz truefalse options! (id=$options->id)"; + return $result; + } + } else { + unset($options); + $options->question = $question->id; + $options->trueanswer = $true->id; + $options->falseanswer = $false->id; + if (!insert_record("quiz_truefalse", $options)) { + $result->error = "Could not insert quiz truefalse options!"; + return $result; + } + } + return true; + } + + function print_question_formulation_and_controls($question, + $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + + // Get additional information from database + + if (!$options = get_record("quiz_truefalse", "question", $question->id)) { + notify("Error: Missing question options!"); + } + if (!$true = get_record("quiz_answers", "id", $options->trueanswer)) { + notify("Error: Missing question answers!"); + } + if (!$false = get_record("quiz_answers", "id", $options->falseanswer)) { + notify("Error: Missing question answers!"); + } + + // Print question formulation + + echo format_text($question->questiontext, + $question->questiontextformat, + NULL, $quiz->course); + quiz_print_possible_question_image($quiz->id, $question); + + // Print input controls + + $stranswer = get_string("answer", "quiz"); + + if (!$true->answer) { + $true->answer = get_string("true", "quiz"); + } + if (!$false->answer) { + $false->answer = get_string("false", "quiz"); + } + + $truechecked = ""; + $falsechecked = ""; + + if (!isset($question->response[$nameprefix])) { + $question->response[$nameprefix] = ''; + } + if ($true->id == $question->response[$nameprefix]) { + $truechecked = 'checked="checked"'; + } else if ($false->id == $question->response[$nameprefix]) { + $falsechecked = 'checked="checked"'; + } + if ($readonly) { + $readonly = ' readonly="readonly" disabled="disabled" '; + } + + $truecorrect = ""; + $falsecorrect = ""; + if ($readonly && $quiz->correctanswers) { + if (!empty($correctanswers[$nameprefix.$true->id])) { + $truecorrect = 'class="highlight"'; + } + if (!empty($correctanswers[$nameprefix.$false->id])) { + $falsecorrect = 'class="highlight"'; + } + } + $inputname = ' name="'.$nameprefix.'" '; + echo "
$stranswer:  "; + echo ""; + echo "id\" />$true->answer"; + echo ""; + echo "id\" />$false->answer"; + echo "

";// changed from CLEAR=ALL jm + if ($quiz->feedback && isset($answers[$nameprefix]) + && $feedback = $answers[$nameprefix]->feedback) { + quiz_print_comment( + "

$feedback

"); + } + } + + function grade_response($question, $nameprefix) { + + $answers = get_records("quiz_answers", "question", $question->id); + if (isset($question->response[$nameprefix]) + && isset($answers[$question->response[$nameprefix]])) { + $result->answers = array($nameprefix + => $answers[$question->response[$nameprefix]]); + $result->grade = $result->answers[$nameprefix]->fraction; + + } else { + $result->answers = array(); + $result->grade = 0.0; + } + $result->correctanswers = quiz_extract_correctanswers($answers, + $nameprefix); + + return $result; + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[TRUEFALSE]= new quiz_truefalse_qtype(); + +?> diff --git a/mod/quiz/questiontypes/truefalse/truefalse.html b/mod/quiz/questiontypes/truefalse/truefalse.html new file mode 100644 index 0000000000..3b11eb0dbd --- /dev/null +++ b/mod/quiz/questiontypes/truefalse/truefalse.html @@ -0,0 +1,103 @@ +
action="question.php"> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

:

+ id, true, true); + ?> +

:

+ + +

:

+
+
+
+

+ +

+
+ "; + } + + print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); + + if ($usehtmleditor) { /// Trying this out for a while + echo ''; + } else { + echo "
"; + print_string("formattexttype"); + echo ": "; + if (!isset($question->questiontextformat)) { + $question->questiontextformat = FORMAT_MOODLE; + } + choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, ""); + helpbutton("textformat", get_string("helpformatting")); + echo "
"; + } + ?> +

:

+ image", get_string("none"),"",""); + } + ?> +

:

+ answer", ""); ?> +
+

():

+ +

():

+ +
+ + + +"> + +
+
+ diff --git a/mod/quiz/report/regrade/report.php b/mod/quiz/report/regrade/report.php index dbbb269025..351bf11737 100644 --- a/mod/quiz/report/regrade/report.php +++ b/mod/quiz/report/regrade/report.php @@ -29,12 +29,11 @@ class quiz_report extends quiz_default_report { $count->attempt++; - if (! $questions = quiz_get_attempt_responses($attempt, $quiz)) { + if (! $questions = quiz_get_attempt_questions($quiz, $attempt)) { error("Could not reconstruct quiz results for attempt $attempt->id!"); } - quiz_remove_unwanted_questions($questions, $quiz); - if (!$result = quiz_grade_attempt_results($quiz, $questions)) { + if (!$result = quiz_grade_responses($quiz, $questions)) { error("Could not re-grade this quiz attempt!"); } diff --git a/mod/quiz/report/simplestat/report.php b/mod/quiz/report/simplestat/report.php index 5b4ce5f55d..b2e9723874 100644 --- a/mod/quiz/report/simplestat/report.php +++ b/mod/quiz/report/simplestat/report.php @@ -2,6 +2,445 @@ /// Overview report just displays a big table of all the attempts +//////////////////////////////////////////////////////////////// +/// With the refactoring of the quiz module in July-2004, some +/// of the functions in lib.php were moved here instead as they +/// are no longer in use by the other quiz components. +/// These functions are quiz_get_attempt_responses, +/// quiz_grade_attempt, quiz_grade_attempt_results, +/// quiz_remove_unwanted_questions and quiz_get_answers. +/// They were all properly renamed by exchanging quiz_ +/// with quiz_report_simplestat_ +////////////////////////////////////////////////////////// + +function quiz_report_simplestat_get_attempt_responses($attempt) { +// Given an attempt object, this function gets all the +// stored responses and returns them in a format suitable +// for regrading using quiz_grade_attempt_results() + global $CFG; + + if (!$responses = get_records_sql("SELECT q.id, q.qtype, q.category, q.questiontext, + q.defaultgrade, q.image, r.answer + FROM {$CFG->prefix}quiz_responses r, + {$CFG->prefix}quiz_questions q + WHERE r.attempt = '$attempt->id' + AND q.id = r.question")) { + notify("Could not find any responses for that attempt!"); + return false; + } + + + foreach ($responses as $key => $response) { + if ($response->qtype == RANDOM) { + $responses[$key]->random = $response->answer; + $responses[$response->answer]->delete = true; + + $realanswer = $responses[$response->answer]->answer; + + if (is_array($realanswer)) { + $responses[$key]->answer = $realanswer; + } else { + $responses[$key]->answer = explode(",", $realanswer); + } + + } else if ($response->qtype == NUMERICAL or $response->qtype == SHORTANSWER) { + $responses[$key]->answer = array($response->answer); + } else { + $responses[$key]->answer = explode(",",$response->answer); + } + } + foreach ($responses as $key => $response) { + if (!empty($response->delete)) { + unset($responses[$key]); + } + } + + return $responses; +} + +function quiz_report_simplestat_grade_attempt_question_result($question, + $answers, + $gradecanbenegative= false) +{ + $grade = 0; // default + $correct = array(); + $feedback = array(); + $response = array(); + + switch ($question->qtype) { + case SHORTANSWER: + if ($question->answer) { + $question->answer = trim(stripslashes($question->answer[0])); + } else { + $question->answer = ""; + } + $response[0] = $question->answer; + $feedback[0] = ''; // Default + foreach ($answers as $answer) { // There might be multiple right answers + + $answer->answer = trim($answer->answer); // Just in case + + if ($answer->fraction >= 1.0) { + $correct[] = $answer->answer; + } + if (!$answer->usecase) { // Don't compare case + $answer->answer = strtolower($answer->answer); + $question->answer = strtolower($question->answer); + } + + $potentialgrade = (float)$answer->fraction * $question->grade; + + if ($potentialgrade >= $grade and (strpos(' '.$answer->answer, '*'))) { + $answer->answer = str_replace('\*','@@@@@@',$answer->answer); + $answer->answer = str_replace('*','.*',$answer->answer); + $answer->answer = str_replace('@@@@@@', '\*',$answer->answer); + $answer->answer = str_replace('+', '\+',$answer->answer); + if (eregi('^'.$answer->answer.'$', $question->answer)) { + $feedback[0] = $answer->feedback; + $grade = $potentialgrade; + } + + } else if ($answer->answer == $question->answer) { + $feedback[0] = $answer->feedback; + $grade = $potentialgrade; + } + } + + break; + + case NUMERICAL: + if ($question->answer) { + $question->answer = trim(stripslashes($question->answer[0])); + } else { + $question->answer = ""; + } + $response[0] = $question->answer; + $bestshortanswer = 0; + foreach ($answers as $answer) { // There might be multiple right answers + if ($answer->fraction > $bestshortanswer) { + $correct[$answer->id] = $answer->answer; + $bestshortanswer = $answer->fraction; + $feedback[0] = $answer->feedback; // Show feedback for best answer + } + if ('' != $question->answer // Must not be mixed up with zero! + && (float)$answer->fraction > (float)$grade // Do we need to bother? + and // and has lower procedence than && and ||. + strtolower($question->answer) == strtolower($answer->answer) + || '' != trim($answer->min) + && ((float)$question->answer >= (float)$answer->min) + && ((float)$question->answer <= (float)$answer->max)) + { + //$feedback[0] = $answer->feedback; No feedback was shown for wrong answers + $grade = (float)$answer->fraction; + } + } + $grade *= $question->grade; // Normalize to correct weight + break; + + case TRUEFALSE: + if ($question->answer) { + $question->answer = $question->answer[0]; + } else { + $question->answer = NULL; + } + foreach($answers as $answer) { // There should be two answers (true and false) + $feedback[$answer->id] = $answer->feedback; + if ($answer->fraction > 0) { + $correct[$answer->id] = true; + } + if ($question->answer == $answer->id) { + $grade = (float)$answer->fraction * $question->grade; + $response[$answer->id] = true; + } + } + break; + + + case MULTICHOICE: + foreach($answers as $answer) { // There will be multiple answers, perhaps more than one is right + $feedback[$answer->id] = $answer->feedback; + if ($answer->fraction > 0) { + $correct[$answer->id] = true; + } + if (!empty($question->answer)) { + foreach ($question->answer as $questionanswer) { + if ($questionanswer == $answer->id) { + $response[$answer->id] = true; + if ($answer->single) { + $grade = (float)$answer->fraction * $question->grade; + continue; + } else { + $grade += (float)$answer->fraction * $question->grade; + } + } + } + } + } + break; + + case MATCH: + $matchcount = $totalcount = 0; + + foreach ($question->answer as $questionanswer) { // Each answer is "subquestionid-answerid" + $totalcount++; + $qarr = explode('-', $questionanswer); // Extract subquestion/answer. + $subquestionid = $qarr[0]; + $subanswerid = $qarr[1]; + if ($subquestionid and $subanswerid and (($subquestionid == $subanswerid) or + ($answers[$subquestionid]->answertext == $answers[$subanswerid]->answertext))) { + // Either the ids match exactly, or the answertexts match exactly + // (in case two subquestions had the same answer) + $matchcount++; + $correct[$subquestionid] = true; + } else { + $correct[$subquestionid] = false; + } + $response[$subquestionid] = $subanswerid; + } + + $grade = $question->grade * $matchcount / $totalcount; + + break; + + case RANDOMSAMATCH: + $bestanswer = array(); + foreach ($answers as $answer) { // Loop through them all looking for correct answers + if (empty($bestanswer[$answer->question])) { + $bestanswer[$answer->question] = 0; + $correct[$answer->question] = ""; + } + if ($answer->fraction > $bestanswer[$answer->question]) { + $bestanswer[$answer->question] = $answer->fraction; + $correct[$answer->question] = $answer->answer; + } + } + $answerfraction = 1.0 / (float) count($question->answer); + foreach ($question->answer as $questionanswer) { // For each random answered question + $rqarr = explode('-', $questionanswer); // Extract question/answer. + $rquestion = $rqarr[0]; + $ranswer = $rqarr[1]; + $response[$rquestion] = $questionanswer; + if (isset($answers[$ranswer])) { // If the answer exists in the list + $answer = $answers[$ranswer]; + $feedback[$rquestion] = $answer->feedback; + if ($answer->question == $rquestion) { // Check that this answer matches the question + $grade += (float)$answer->fraction * $question->grade * $answerfraction; + } + } + } + break; + + case MULTIANSWER: + // Default setting that avoids a possible divide by zero: + $subquestion->grade = 1.0; + + foreach ($question->answer as $questionanswer) { + + // Resetting default values for subresult: + $subresult->grade = 0.0; + $subresult->correct = array(); + $subresult->feedback = array(); + + // Resetting subquestion responses: + $subquestion->answer = array(); + + $qarr = explode('-', $questionanswer, 2); + $subquestion->answer[] = $qarr[1]; // Always single answer for subquestions + foreach ($answers as $multianswer) { + if ($multianswer->id == $qarr[0]) { + $subquestion->qtype = $multianswer->answertype; + $subquestion->grade = $multianswer->norm; + $subresult = quiz_report_simplestat_grade_attempt_question_result($subquestion, $multianswer->subanswers, true); + break; + } + } + + + // Summarize subquestion results: + $grade += $subresult->grade; + $feedback[] = $subresult->feedback[0]; + $correct[] = $subresult->correct[0]; + + // Each response instance also contains the partial + // fraction grade for the response: + $response[] = $subresult->grade/$subquestion->grade + . '-' . $subquestion->answer[0]; + } + // Normalize grade: + $grade *= $question->grade/($question->defaultgrade); + break; + + case DESCRIPTION: // Descriptions are not graded. + break; + + case RANDOM: // Returns a recursive call with the real question + $realquestion = get_record + ('quiz_questions', 'id', $question->random); + $realquestion->answer = $question->answer; + $realquestion->grade = $question->grade; + return quiz_report_simplestat_grade_attempt_question_result($realquestion, $answers); + } + + $result->grade = + $gradecanbenegative ? $grade // Grade can be negative + : max(0.0, $grade); // Grade must not be negative + $result->correct = $correct; + $result->feedback = $feedback; + $result->response = $response; + return $result; +} + +function quiz_report_simplestat_remove_unwanted_questions(&$questions, $quiz) { +/// Given an array of questions, and a list of question IDs, +/// this function removes unwanted questions from the array +/// Used by review.php and attempt.php to counter changing quizzes + + $quizquestions = array(); + $quizids = explode(",", $quiz->questions); + foreach ($quizids as $quizid) { + $quizquestions[$quizid] = true; + } + foreach ($questions as $key => $question) { + if (!isset($quizquestions[$question->id])) { + unset($questions[$key]); + } + } +} + +function quiz_report_simplestat_get_answers($question, $answerids=NULL) { +// Given a question, returns the correct answers for a given question + global $CFG; + + if (empty($answerids)) { + $answeridconstraint = ''; + } else { + $answeridconstraint = " AND a.id IN ($answerids) "; + } + + switch ($question->qtype) { + case SHORTANSWER: // Could be multiple answers + return get_records_sql("SELECT a.*, sa.usecase + FROM {$CFG->prefix}quiz_shortanswer sa, + {$CFG->prefix}quiz_answers a + WHERE sa.question = '$question->id' + AND sa.question = a.question " + . $answeridconstraint); + + case TRUEFALSE: // Should be always two answers + return get_records("quiz_answers", "question", $question->id); + + case MULTICHOICE: // Should be multiple answers + return get_records_sql("SELECT a.*, mc.single + FROM {$CFG->prefix}quiz_multichoice mc, + {$CFG->prefix}quiz_answers a + WHERE mc.question = '$question->id' + AND mc.question = a.question " + . $answeridconstraint); + + case MATCH: + return get_records("quiz_match_sub", "question", $question->id); + + case RANDOMSAMATCH: // Could be any of many answers, return them all + return get_records_sql("SELECT a.* + FROM {$CFG->prefix}quiz_questions q, + {$CFG->prefix}quiz_answers a + WHERE q.category = '$question->category' + AND q.qtype = ".SHORTANSWER." + AND q.id = a.question "); + + case NUMERICAL: // Logical support for multiple answers + return get_records_sql("SELECT a.*, n.min, n.max + FROM {$CFG->prefix}quiz_numerical n, + {$CFG->prefix}quiz_answers a + WHERE a.question = '$question->id' + AND n.answer = a.id " + . $answeridconstraint); + + case DESCRIPTION: + return true; // there are no answers for description + + case RANDOM: + return quiz_get_answers + (get_record('quiz_questions', 'id', $question->random)); + + case MULTIANSWER: // Includes subanswers + $answers = array(); + + $virtualquestion->id = $question->id; + + if ($multianswers = get_records('quiz_multianswers', 'question', $question->id)) { + foreach ($multianswers as $multianswer) { + $virtualquestion->qtype = $multianswer->answertype; + // Recursive call for subanswers + $multianswer->subanswers = quiz_get_answers($virtualquestion, $multianswer->answers); + $answers[] = $multianswer; + } + } + return $answers; + + default: + return false; + } +} + + +function quiz_report_simplestat_grade_attempt_results($quiz, $questions) { +/// Given a list of questions (including answers for each one) +/// this function does all the hard work of calculating the +/// grades for each question, as well as a total grade for +/// for the whole quiz. It returns everything in a structure +/// that looks like: +/// $result->sumgrades (sum of all grades for all questions) +/// $result->percentage (Percentage of grades that were correct) +/// $result->grade (final grade result for the whole quiz) +/// $result->grades[] (array of grades, indexed by question id) +/// $result->response[] (array of response arrays, indexed by question id) +/// $result->feedback[] (array of feedback arrays, indexed by question id) +/// $result->correct[] (array of feedback arrays, indexed by question id) + + if (!$questions) { + error("No questions!"); + } + + if (!$grades = get_records_menu("quiz_question_grades", "quiz", $quiz->id, "", "question,grade")) { + error("No grades defined for these quiz questions!"); + } + + $result->sumgrades = 0; + + foreach ($questions as $question) { + + $question->grade = $grades[$question->id]; + + if (!$answers = quiz_report_simplestat_get_answers($question)) { + error("No answers defined for question id $question->id!"); + } + + $questionresult = quiz_report_simplestat_grade_attempt_question_result($question, + $answers); + // if time limit is enabled and exceeded, return zero grades + if($quiz->timelimit > 0) { + if(($quiz->timelimit + 60) <= $quiz->timesincestart) { + $questionresult->grade = 0; + } + } + + $result->grades[$question->id] = round($questionresult->grade, 2); + $result->sumgrades += $questionresult->grade; + $result->feedback[$question->id] = $questionresult->feedback; + $result->response[$question->id] = $questionresult->response; + $result->correct[$question->id] = $questionresult->correct; + } + + $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; +} + + class quiz_report extends quiz_default_report { function display($quiz, $cm, $course) { /// This function just displays the report @@ -48,12 +487,12 @@ class quiz_report extends quiz_default_report { if (!$bestattempt = quiz_calculate_best_attempt($quiz, $attempts)) { continue; } - if (!$questions = quiz_get_attempt_responses($bestattempt, $quiz)) { + if (!$questions = quiz_report_simplestat_get_attempt_responses($bestattempt, $quiz)) { continue; } - quiz_remove_unwanted_questions($questions, $quiz); + quiz_report_simplestat_remove_unwanted_questions($questions, $quiz); - if (!$results = quiz_grade_attempt_results($quiz, $questions)) { + if (!$results = quiz_report_simplestat_grade_attempt_results($quiz, $questions)) { error("Could not re-grade this quiz attempt!"); } diff --git a/mod/quiz/review.php b/mod/quiz/review.php index 8665af476d..951e0944f7 100644 --- a/mod/quiz/review.php +++ b/mod/quiz/review.php @@ -85,20 +85,11 @@ print_heading($quiz->name); - if (! $questions = quiz_get_attempt_responses($attempt)) { - if ($user = get_record("user", "id", $attempt->userid)) { - $fullname = fullname($user); - } else { - $fullname = "????"; - } - print_heading(get_string("attemptincomplete", "quiz", $fullname)); - print_footer($course); - exit; + if (!($questions = quiz_get_attempt_questions($quiz, $attempt))) { + error("Unable to get questions from database for quiz $quiz->id attempt $attempt->id number $attempt->attempt"); } - quiz_remove_unwanted_questions($questions, $quiz); - - if (!$result = quiz_grade_attempt_results($quiz, $questions)) { + if (!$result = quiz_grade_responses($quiz, $questions)) { error("Could not re-grade this quiz attempt!"); } @@ -128,7 +119,7 @@ $quiz->correctanswers = true; $quiz->shuffleanswers = false; $quiz->shufflequestions = false; - quiz_print_quiz_questions($quiz, $result, $questions); + quiz_print_quiz_questions($quiz, $questions, $result); if (isteacher($course->id)) { print_continue("report.php?q=$quiz->id");