]> git.mjollnir.org Git - moodle.git/commitdiff
Quiz refactoring is now merged into the main development code!
authormoodler <moodler>
Wed, 21 Jul 2004 13:01:08 +0000 (13:01 +0000)
committermoodler <moodler>
Wed, 21 Jul 2004 13:01:08 +0000 (13:01 +0000)
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.

44 files changed:
mod/quiz/attempt.php
mod/quiz/format.php
mod/quiz/format/multianswer/format.php
mod/quiz/lib.php
mod/quiz/question.php
mod/quiz/questiontypes/description/description.html [new file with mode: 0644]
mod/quiz/questiontypes/description/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/description/icon.gif [new file with mode: 0644]
mod/quiz/questiontypes/description/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/match/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/match/icon.gif [new file with mode: 0644]
mod/quiz/questiontypes/match/match.html [new file with mode: 0644]
mod/quiz/questiontypes/match/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/multianswer/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/multianswer/icon.gif [new file with mode: 0644]
mod/quiz/questiontypes/multianswer/multianswer.html [new file with mode: 0644]
mod/quiz/questiontypes/multianswer/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/multichoice/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/multichoice/icon.gif [new file with mode: 0644]
mod/quiz/questiontypes/multichoice/multichoice.html [new file with mode: 0644]
mod/quiz/questiontypes/multichoice/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/numerical/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/numerical/icon.gif [new file with mode: 0644]
mod/quiz/questiontypes/numerical/numerical.html [new file with mode: 0644]
mod/quiz/questiontypes/numerical/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/random/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/random/icon.gif [new file with mode: 0644]
mod/quiz/questiontypes/random/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/random/random.html [new file with mode: 0644]
mod/quiz/questiontypes/randomsamatch/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/randomsamatch/icon.gif [new file with mode: 0644]
mod/quiz/questiontypes/randomsamatch/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/randomsamatch/randomsamatch.html [new file with mode: 0644]
mod/quiz/questiontypes/shortanswer/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/shortanswer/icon.gif [new file with mode: 0644]
mod/quiz/questiontypes/shortanswer/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/shortanswer/shortanswer.html [new file with mode: 0644]
mod/quiz/questiontypes/truefalse/editquestion.php [new file with mode: 0644]
mod/quiz/questiontypes/truefalse/icon.gif [new file with mode: 0644]
mod/quiz/questiontypes/truefalse/questiontype.php [new file with mode: 0644]
mod/quiz/questiontypes/truefalse/truefalse.html [new file with mode: 0644]
mod/quiz/report/regrade/report.php
mod/quiz/report/simplestat/report.php
mod/quiz/review.php

index 3bd337eab3fab792f52670743a8b052a5acb9721..a93239ce35a58d8c34f3b681142503a6de41b91d 100644 (file)
 
     $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;
             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)");
             }
         }
 
             }
         }
 
-        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!");
         }
 
 
         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");
         }
 
 
 /// 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
 
     echo "<br />";
 
-    $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);
 
 ?>
index b9e542a247057f98d16c6ecdd5c78e084954b9f0..bd9339ea737dd3f1ed8c753c516ca77b513b56a1 100644 (file)
@@ -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);
index 2fa622e566bcb272d647695316976a412eb2e15b..6ac834001f1c3b4082e18692588dbca02e693070 100644 (file)
@@ -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];
index 8e7ed1bf69a9113a9afb2d50e0280a2c0dc5c9e9..7e5c2ca9be53686565d4fe5e9faf4c0a21f0c008 100644 (file)
@@ -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 '<p align="center"><b>' . $number . '</b></p>';
+        if (false !== $grade) {
+            $strmarks  = get_string("marks", "quiz");
+            echo '<p align="center"><font size="1">';
+            if (false !== $actualgrade) {
+                echo "$strmarks: $actualgrade/$grade</font></p>";
+            } else {
+                echo "$grade $strmarks</font></p>";
+            }
+        }
+        print_spacer(1,100);
+
+        /// Print possible recently-added information:
+
+        if ($recentlyadded) {
+            echo '</td><td valign="top" align="right">';
+            // Notify the user of this recently added question
+            echo '<font color="red">';
+            echo get_string('recentlyaddedquestion', 'quiz');
+            echo '</font>';
+            echo '</td></tr><tr><td></td><td valign="top">';
+
+        } else { // The normal case
+            echo '</td><td valign="top">';
+        }
+    }
+
+    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 '<table width="100%" cellspacing="10">';
+        echo '<tr><td nowrap="nowrap" width="100" valign="top">';
+
+        $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 "</td></tr></table>";
+        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<br/>"; // 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 "<a href=\"question.php?id=$question->id\" title=\"".$QUIZ_QUESTION_TYPE[$question->qtype]."\">";
-    }
-    switch ($question->qtype) {
-        case SHORTANSWER:
-            echo '<img border="0" height="16" width="16" src="pix/sa.gif">';
-            break;
-        case TRUEFALSE:
-            echo '<img border="0" height="16" width="16" src="pix/tf.gif">';
-            break;
-        case MULTICHOICE:
-            echo '<img border="0" height="16" width="16" src="pix/mc.gif">';
-            break;
-        case RANDOM:
-            echo '<img border="0" height="16" width="16" src="pix/rs.gif">';
-            break;
-        case MATCH:
-            echo '<img border="0" height="16" width="16" src="pix/ma.gif">';
-            break;
-        case RANDOMSAMATCH:
-            echo '<img border="0" height="16" width="16" src="pix/rm.gif">';
-            break;
-        case DESCRIPTION:
-            echo '<img border="0" height="16" width="16" src="pix/de.gif">';
-            break;
-        case NUMERICAL:
-            echo '<img border="0" height="16" width="16" src="pix/nu.gif">';
-            break;
-        case MULTIANSWER:
-            echo '<img border="0" height="16" width="16" src="pix/mu.gif">';
-            break;
+        echo "<a href=\"question.php?id=$question->id\" title=\""
+                .$QUIZ_QUESTION_TYPE[$question->qtype]."\">";
     }
+    echo '<img border="0" height="16" width="16" src="questiontypes/';
+    echo $QUIZ_QTYPES[$question->qtype]->name().'/icon.gif"/>';
     if ($editlink) {
         echo "</a>\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 '<p align="center">';
-        echo format_text($question->questiontext, $question->questiontextformat, NULL, $courseid);
-        quiz_print_possible_question_image($quizid, $question);
-        echo '</p>';
-        return true;
-    }
-
-    if (empty($actualgrade)) {
-        $actualgrade = 0;
-    }
-
-    $stranswer = get_string("answer", "quiz");
-    $strmarks  = get_string("marks", "quiz");
-
-    echo '<table width="100%" cellspacing="10">';
-    echo '<tr><td nowrap="nowrap" width="100" valign="top">';
-    echo '<p align="center"><b>' . $number . '</b></p>';
-    if ($showgrades) {
-        if ($feedback or $response) {
-            echo "<p align=\"center\"><font size=\"1\">$strmarks: $actualgrade/$grade</font></p>";
-        } else {
-            echo "<p align=\"center\"><font size=\"1\">$grade $strmarks</font></p>";
-        }
-    }
-    print_spacer(1,100);
-
-    if (isset($question->recentlyadded) and $question->recentlyadded) {
-        echo '</td><td valign="top" align="right">';
-        // Notify the user of this recently added question
-        echo '<font color="red">';
-        echo get_string('recentlyaddedquestion', 'quiz');
-        echo '</font>';
-        echo '</td></tr><tr><td></td><td valign="top">';
-
-    } else { // The normal case
-        echo '</td><td valign="top">';
-    }
-
-
-    if (empty($realquestion)) {
-        $realquestion->id = $question->id;
-    } else {    // Add a marker to connect this question to the actual random parent
-        echo "<input type=\"hidden\" name=\"q{$realquestion->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 "<p align=\"right\">$stranswer: <input type=\"text\" name=\"q$realquestion->id\" size=\"80\" $value /></p>";
-           if ($feedback) {
-               quiz_print_comment("<p align=\"right\">$feedback[0]</p>");
-           }
-           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 "<table align=\"right\" cellpadding=\"5\"><tr><td align=\"right\">$stranswer:&nbsp;&nbsp;";
-           echo "<td $truecorrect>";
-           echo "<input $truechecked type=\"radio\" name=\"q$realquestion->id\" value=\"$true->id\" />$true->answer";
-           echo "</td><td $falsecorrect>";
-           echo "<input $falsechecked type=\"radio\" name=\"q$realquestion->id\" value=\"$false->id\" />$false->answer";
-           echo "</td></tr></table><br clear=\"all\">";// changed from CLEAR=ALL jm
-           if ($feedback) {
-               quiz_print_comment("<p align=\"right\">$feedback[$feedbackid]</p>");
-           }
-
-           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 "<table align=\"right\">";
-           echo "<tr><td valign=\"top\">$stranswer:&nbsp;&nbsp;</td><td>";
-           echo "<table>";
-           $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 '<tr><td valign="top">';
-               if ($options->single) {
-                   echo "<input $checked type=\"radio\" name=\"q$realquestion->id\" value=\"$answer->id\" />";
-               } else {
-                   echo "<input $checked type=\"checkbox\" name=\"q$realquestion->id"."a$answer->id\" value=\"$answer->id\" />";
-               }
-               echo "</td>";
-               if (empty($feedback) or empty($correct[$answer->id])) {
-                   echo '<td valign="top">'.format_text("$qnumchar. $answer->answer").'</td>';
-               } else {
-                   echo '<td valign="top" class="highlight">'.format_text("$qnumchar. $answer->answer").'</td>';
-               }
-               if (!empty($feedback)) {
-                   echo "<td valign=\"top\">&nbsp;";
-                   if (!empty($response[$answerid])) {
-                       quiz_print_comment($feedback[$answerid]);
-                   }
-                   echo "</td>";
-               }
-               echo "</tr>";
-           }
-           echo "</table>";
-           echo "</td></tr></table>";
-           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 '<table border="0" cellpadding="10" align="right">';
-           foreach ($subquestions as $key => $subquestion) {
-               echo '<tr><td align="left" valign="top">';
-               echo $subquestion->questiontext;
-               echo '</td>';
-               if (empty($response)) {
-                   echo '<td align="right" valign="top">';
-                   choose_from_menu($answers, "q$realquestion->id"."r$subquestion->id");
-               } else {
-                   if (empty($response[$key])) {
-                       echo '<td align="right" valign="top">';
-                       choose_from_menu($answers, "q$realquestion->id"."r$subquestion->id");
-                   } else {
-                       if ($response[$key] == $correct[$key]) {
-                           echo '<td align="right" valign="top" class="highlight">';
-                           choose_from_menu($answers, "q$realquestion->id"."r$subquestion->id", $response[$key]);
-                       } else {
-                           echo '<td align="right" valign="top">';
-                           choose_from_menu($answers, "q$realquestion->id"."r$subquestion->id", $response[$key]);
-                       }
-                   }
-
-                   if (!empty($feedback[$key])) {
-                       quiz_print_comment($feedback[$key]);
-                   }
-               }
-               echo '</td></tr>';
-           }
-           echo '</table>';
-
-           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 '<table border="0" cellpadding="10">';
-           foreach ($randomquestions as $key => $randomquestion) {
-               echo '<tr><td align="left" valign="top">';
-               echo $randomquestion->questiontext;
-               echo '</td>';
-               echo '<td align="right" valign="top">';
-               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 '<span="highlight">';
-                           choose_from_menu($randomanswers, "q$realquestion->id"."r$randomquestion->id", $responseanswer[$key]);
-                           echo '</span><br />';
-                       } 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 '</td></tr>';
-           }
-           echo '</table>';
-           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 " <input $style $popup $inputname value=\"$actualresponse\" type=\"text\" size=\"12\" /> ";
-                       break;
-                   case MULTICHOICE:
-                       $outputoptions = '';
-                       $answers = get_records_list("quiz_answers", "id", $multianswer->answers);
-                       $outputoptions .= '<option></option>'; // Default empty option
-                       foreach ($answers as $answer) {
-                           if ($answer->id == $actualresponse) {
-                               $selected = 'selected';
-                               $feedbackitem = $answer->feedback;
-                           } else {
-                               $selected = '';
-                           }
-                           $outputoptions .= "<option value=\"$answer->id\" $selected>$answer->answer</option>";
-                       }
-                       if ($feedbackitem) {
-                           $title = str_replace("'", "\\'", $feedbackitem);
-                           $popup = " onmouseover=\"return overlib('$title', CAPTION, '$strfeedback', FGCOLOR, '$THEME->cellcontent');\" ".
-                                    " onmouseout=\"return nd();\" ";
-                       } else {
-                           $popup = '';
-                       }
-                       echo "<select $popup $style $inputname>";
-                       echo $outputoptions;
-                       echo '</select>';
-                       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 '<p>' . get_string('random', 'quiz') . '</p>';
-           break;
-
-       default:
-           notify("Error: Unknown question type!");
-    }
-
-    echo "</td></tr></table>";
-}
-
-
-
-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 "<input type=\"hidden\" name=\"q\" value=\"$quiz->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 "<br />";
     }
 
-    $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 "<input type=\"hidden\" name=\"shuffleorder\" value=\"$shuffleorder\" />\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
index 6b4a4aeb0e537c0fe147e9c64f365577bde43c4c..c37982c24432366155e8068077f987d497f122bb 100644 (file)
         }
     }
 
-    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);
         $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; $i<QUIZ_MAX_NUMBER_ANSWERS; $i++) {
-                $answers[] = "";   // Make answer slots, default as blank
-            }
-            if (!empty($answersraw)) {
-                $i=0;
-                foreach ($answersraw as $answer) {
-                    $answers[$i] = $answer;   // insert answers into slots
-                    $i++;
-                }
-            }
-            print_heading_with_help(get_string("editingshortanswer", "quiz"), "shortanswer", "quiz");
-            require("shortanswer.html");
-        break;
-
-        case TRUEFALSE:
-            if (!empty($question->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");
-        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; $i<QUIZ_MAX_NUMBER_ANSWERS; $i++) {
-                $answers[] = "";   // Make answer slots, default as blank
-            }
-            if (!empty($answersraw)) {
-                $i=0;
-                foreach ($answersraw as $answer) {
-                    $answers[$i] = $answer;   // insert answers into slots
-                    $i++;
-                }
-            }
-            print_heading_with_help(get_string("editingmultichoice", "quiz"), "multichoice", "quiz");
-            require("multichoice.html");
-        break;
-
-        case MATCH:
-            if (!empty($question->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; $i<QUIZ_MAX_NUMBER_ANSWERS; $i++) {
-                    $subquestions[] = "";   // Make question slots, default as blank
-                    $subanswers[] = "";     // Make answer slots, default as blank
-                }
-                if (!empty($oldsubquestions)) {
-                    $i=0;
-                    foreach ($oldsubquestions as $oldsubquestion) {
-                        $subquestions[$i] = $oldsubquestion->questiontext;   // 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 (file)
index 0000000..38188b5
--- /dev/null
@@ -0,0 +1,80 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">\r
+<CENTER>\r
+<TABLE cellpadding=5>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   quiz_category_select_menu($course->id, true, true); ?>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+        <INPUT type="text" name="name" size=40 value="<?php  p($question->name) ?>">\r
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>\r
+    </TD>\r
+</TR>\r
+<tr valign=top>\r
+    <td align="right"><p><b><?php  print_string("question", "quiz") ?>:</b></p>\r
+    <br />\r
+    <br />\r
+    <br />\r
+    <p><font SIZE="1">\r
+    <?php\r
+           if ($usehtmleditor) {\r
+               helpbutton("richtext", get_string("helprichtext"), "moodle", true, true);\r
+           } else {\r
+               helpbutton("text", get_string("helptext"), "moodle", true, true);\r
+           }\r
+    ?>\r
+    </font></p>\r
+    </td>\r
+    <td>\r
+        <?php  if (isset($err["questiontext"])) {\r
+               formerr($err["questiontext"]); \r
+               echo "<br />";\r
+           }\r
+\r
+           print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext);\r
+\r
+           if ($usehtmleditor) {   /// Trying this out for a while\r
+               echo '<input type="hidden" name="questiontextformat" value="'.FORMAT_HTML.'">';\r
+           } else {\r
+               echo "<div align=right>";\r
+               print_string("formattexttype");\r
+               echo ":&nbsp;";\r
+               if (!isset($question->questiontextformat)) {\r
+                   $question->questiontextformat = FORMAT_MOODLE;\r
+               }\r
+               choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, "");\r
+               helpbutton("textformat", get_string("helpformatting"));\r
+               echo "</div>";\r
+           }\r
+        ?>\r
+    </td>\r
+</tr>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("imagedisplay", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   if (empty($images)) {\r
+            print_string("noimagesyet");\r
+        } else {\r
+            choose_from_menu($images, "image", "$question->image", get_string("none"),"","");\r
+        }\r
+    ?>\r
+    </TD>\r
+</TR>\r
+</TABLE>\r
+\r
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">\r
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">\r
+<INPUT type="hidden" name=defaultgrade value="0">\r
+<INPUT type="submit" value="<?php  print_string("savechanges") ?>">\r
+\r
+</CENTER>\r
+</FORM>\r
+<?php  \r
+   if ($usehtmleditor) { \r
+       print_richedit_javascript("theform", "questiontext", "no");\r
+   }\r
+?>\r
diff --git a/mod/quiz/questiontypes/description/editquestion.php b/mod/quiz/questiontypes/description/editquestion.php
new file mode 100644 (file)
index 0000000..cd5a89c
--- /dev/null
@@ -0,0 +1,6 @@
+<?PHP // $Id$
+
+            print_heading_with_help(get_string("editingdescription", "quiz"), "description", "quiz");
+            require("description.html");
+
+?>
diff --git a/mod/quiz/questiontypes/description/icon.gif b/mod/quiz/questiontypes/description/icon.gif
new file mode 100644 (file)
index 0000000..9c6ec29
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 (file)
index 0000000..dba6997
--- /dev/null
@@ -0,0 +1,57 @@
+<?PHP  // $Id$
+
+///////////////////
+/// DESCRIPTION ///
+///////////////////
+
+/// QUESTION TYPE CLASS //////////////////
+
+//
+// The question type DESCRIPTION is not really a question type
+// and it therefore often sticks to some kind of odd behaviour
+//
+
+class quiz_description_qtype extends quiz_default_questiontype {
+
+    function name() {
+        return 'description';
+    }
+
+    function save_question_options($question) {
+        /// No options to be saved for this question type:
+        return true;
+    }
+
+    function create_response($question, $nameprefix, $questionsinuse) {
+        /// This question type does never have any responses,
+        /// so do not return any...
+
+        return array();
+    }
+
+    function print_question($currentnumber, $quiz, $question,
+                            $readonly, $resultdetails) {
+        echo '<p align="center">';
+        echo format_text($question->questiontext,
+                         $question->questiontextformat,
+                         NULL, $quiz->course);
+        quiz_print_possible_question_image($quiz->id, $question);
+        echo '</p>';
+        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 (file)
index 0000000..c104751
--- /dev/null
@@ -0,0 +1,25 @@
+<?PHP // $Id$
+            if (!empty($question->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; $i<QUIZ_MAX_NUMBER_ANSWERS; $i++) {
+                    $subquestions[] = "";   // Make question slots, default as blank
+                    $subanswers[] = "";     // Make answer slots, default as blank
+                }
+                if (!empty($oldsubquestions)) {
+                    $i=0;
+                    foreach ($oldsubquestions as $oldsubquestion) {
+                        $subquestions[$i] = $oldsubquestion->questiontext;   // 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 (file)
index 0000000..fa1ed50
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 (file)
index 0000000..37327f0
--- /dev/null
@@ -0,0 +1,104 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">\r
+<CENTER>\r
+<TABLE cellpadding=5>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   quiz_category_select_menu($course->id, true, true); ?>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+        <INPUT type="text" name="name" size=40 value="<?php  p($question->name) ?>">\r
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>\r
+    </TD>\r
+</TR>\r
+<tr valign=top>\r
+    <td align="right"><p><b><?php  print_string("question", "quiz") ?>:</b></p>\r
+    <br />\r
+    <br />\r
+    <br />\r
+    <p><font SIZE="1">\r
+    <?php\r
+           if ($usehtmleditor) {\r
+               helpbutton("richtext", get_string("helprichtext"), "moodle", true, true);\r
+           } else {\r
+               helpbutton("text", get_string("helptext"), "moodle", true, true);\r
+           }\r
+    ?>\r
+    </font></p>\r
+    </td>\r
+    <td>\r
+        <?php  if (isset($err["questiontext"])) {\r
+               formerr($err["questiontext"]); \r
+               echo "<br />";\r
+           }\r
+\r
+           print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext);\r
+\r
+           if ($usehtmleditor) {   /// Trying this out for a while\r
+               echo '<input type="hidden" name="questiontextformat" value="'.FORMAT_HTML.'">';\r
+           } else {\r
+               echo "<div align=right>";\r
+               print_string("formattexttype");\r
+               echo ":&nbsp;";\r
+               if (!isset($question->questiontextformat)) {\r
+                   $question->questiontextformat = FORMAT_MOODLE;\r
+               }\r
+               choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, "");\r
+               helpbutton("textformat", get_string("helpformatting"));\r
+               echo "</div>";\r
+           }\r
+        ?>\r
+    </td>\r
+</tr>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("imagedisplay", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   if (empty($images)) {\r
+            print_string("noimagesyet");\r
+        } else {\r
+            choose_from_menu($images, "image", "$question->image", get_string("none"),"","");\r
+        }\r
+    ?>\r
+    </TD>\r
+</TR>\r
+\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("choices", "quiz") ?></B>:</P></TD>\r
+    <TD><P><?php  print_string("filloutthreequestions", "quiz") ?></P>\r
+    </TD>\r
+</TR>\r
+\r
+<?PHP \r
+    for ($i=1; $i<=QUIZ_MAX_NUMBER_ANSWERS; $i++) {\r
+?>\r
+\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  echo get_string("question", "quiz")." $i";  ?> :</B></P></TD>\r
+    <TD>\r
+        <textarea name="subquestions[]" rows=5 cols=50><?php  p($subquestions[$i-1]) ?></textarea>\r
+        <br />\r
+        <?php  echo get_string("matchanswer", "quiz")." $i";  ?>&nbsp;&nbsp;\r
+        <INPUT type="text" name="subanswers[]" size=50 value="<?php  p($subanswers[$i-1]) ?>">\r
+    </TD>\r
+</TR>\r
+\r
+<?PHP\r
+    }\r
+?>\r
+\r
+</TABLE>\r
+\r
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">\r
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">\r
+<INPUT type="submit" value="<?php  print_string("savechanges") ?>">\r
+\r
+</CENTER>\r
+</FORM>\r
+<?php  \r
+   if ($usehtmleditor) { \r
+       print_richedit_javascript("theform", "questiontext", "no");\r
+   }\r
+?>\r
diff --git a/mod/quiz/questiontypes/match/questiontype.php b/mod/quiz/questiontypes/match/questiontype.php
new file mode 100644 (file)
index 0000000..419e612
--- /dev/null
@@ -0,0 +1,247 @@
+<?PHP  // $Id$
+
+/////////////
+/// MATCH ///
+/////////////
+
+/// QUESTION TYPE CLASS //////////////////
+class quiz_match_qtype extends quiz_default_questiontype {
+
+    function name() {
+        return 'match';
+    }
+
+    function save_question_options($question) {
+        
+        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;
+            }
+        }
+        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 '<table border="0" cellpadding="10" align="right">';
+        foreach ($subquestions as $subquestion) {
+
+            /// Subquestion text:
+            echo '<tr><td align="left" valign="top">';
+            echo $subquestion->questiontext;
+            echo '</td>';
+
+            /// 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 "<td align=\"right\" valign=\"top\" $class>";
+            choose_from_menu($answers, $menuname, $response);
+            if ($quiz->feedback && isset($answers[$menuname])
+                    && $answers[$menuname]->feedback) {
+                quiz_print_comment($answers[$menuname]->feedback);
+            }
+            echo '</td></tr>';
+        }
+        echo '</table>';
+    }
+
+    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 (file)
index 0000000..0e58937
--- /dev/null
@@ -0,0 +1,46 @@
+<?PHP // $Id$
+    if ($question->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 (file)
index 0000000..edaa241
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 (file)
index 0000000..4c3da4b
--- /dev/null
@@ -0,0 +1,114 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">
+
+<CENTER>
+
+<TABLE cellpadding=5>
+
+<TR valign=top>
+
+    <TD align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>
+
+    <TD>
+
+    <?php   quiz_category_select_menu($course->id, true, true ); ?>
+
+    </TD>
+
+</TR>
+
+<TR valign=top>
+
+    <TD align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>
+
+    <TD>
+
+        <INPUT type="text" name="name" size=40 value="<?php  p($question->name) ?>">
+
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>
+
+    </TD>
+
+</TR>
+
+<TR valign=top>
+
+    <TD align=right><P><B><?php  print_string("question", "quiz") ?>:</B></P></TD>
+
+    <TD>
+
+        <?php  if (isset($err["questiontext"])) {
+
+               formerr($err["questiontext"]); 
+
+               echo "<BR />";
+
+           }
+
+           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");
+
+           }
+
+        ?>
+
+    </TD>
+
+</TR>
+
+<TR valign=top>
+
+    <TD align=right><P><B><?php  print_string("imagedisplay", "quiz") ?>:</B></P></TD>
+
+    <TD>
+
+    <?php   if (empty($images)) {
+
+            print_string("noimagesyet");
+
+        } else {
+
+            choose_from_menu($images, "image", "$question->image", get_string("none"),"","");
+
+        }
+
+    ?>
+
+    </TD>
+
+</TR>
+
+</TABLE>
+
+
+
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">
+
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">
+
+<INPUT type="hidden" name=defaultgrade value="<?php  p($question->defaultgrade) ?>">
+
+<INPUT type="submit" value="<?php  print_string("savechanges") ?>">
+
+
+
+</CENTER>
+
+</FORM>
+
+<?php  
+
+   if ($usehtmleditor) { 
+
+       print_richedit_javascript("theform", "questiontext", "no");
+
+   }
+
+?>
+
diff --git a/mod/quiz/questiontypes/multianswer/questiontype.php b/mod/quiz/questiontypes/multianswer/questiontype.php
new file mode 100644 (file)
index 0000000..550002d
--- /dev/null
@@ -0,0 +1,623 @@
+<?PHP  // $Id$
+
+///////////////////
+/// MULTIANSWER /// (Embedded - cloze)
+///////////////////
+
+///
+/// The multianswer question type is special in that it
+/// depends on a few other question types, i.e.
+/// MULTICHOICE, SHORTANSWER and NUMERICAL.
+/// These question types have got a few special features that
+/// makes them useable by the MULTIANSWER question type
+///
+
+/// QUESTION TYPE CLASS //////////////////
+class quiz_embedded_cloze_qtype extends quiz_default_questiontype {
+
+    function get_answers($question) {
+        /// The returned answers includes subanswers...
+        // As this question type embedds some other question types,
+        // it is necessary to have access to those:
+        global $QUIZ_QTYPES;
+
+        $answers = array();
+
+        $virtualquestion->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 " <input $style $readonly $popup name=\"$inputname\"
+                            type=\"text\" value=\"$response\" size=\"12\" /> ";
+                    break;
+                case MULTICHOICE:
+                    $outputoptions = '<option></option>'; // Default empty option
+                    $mcanswers = get_records_list("quiz_answers", "id", $multianswer->answers);
+                    foreach ($mcanswers as $mcanswer) {
+                        $selected = $response == $mcanswer->id
+                                ? ' selected="selected" ' : '';
+                        $outputoptions .= "<option value=\"$mcanswer->id\" $selected>$mcanswer->answer</option>";
+                    }
+                   echo "<select $popup $style name=\"$inputname\" $readonly>";
+                   echo $outputoptions;
+                   echo '</select>';
+                   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 (file)
index 0000000..d313938
--- /dev/null
@@ -0,0 +1,23 @@
+<?PHP // $Id$
+            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; $i<QUIZ_MAX_NUMBER_ANSWERS; $i++) {
+                $answers[] = "";   // Make answer slots, default as blank
+            }
+            if (!empty($answersraw)) {
+                $i=0;
+                foreach ($answersraw as $answer) {
+                    $answers[$i] = $answer;   // insert answers into slots
+                    $i++;
+                }
+            }
+            print_heading_with_help(get_string("editingmultichoice", "quiz"), "multichoice", "quiz");
+            require("multichoice.html");
+
+?>
diff --git a/mod/quiz/questiontypes/multichoice/icon.gif b/mod/quiz/questiontypes/multichoice/icon.gif
new file mode 100644 (file)
index 0000000..d264c81
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 (file)
index 0000000..5afb256
--- /dev/null
@@ -0,0 +1,124 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">\r
+<CENTER>\r
+<TABLE cellpadding=5>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   quiz_category_select_menu($course->id, true, true); ?>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+        <INPUT type="text" name="name" size=40 value="<?php  p($question->name) ?>">\r
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>\r
+    </TD>\r
+</TR>\r
+<tr valign=top>\r
+    <td align="right"><p><b><?php  print_string("question", "quiz") ?>:</b></p>\r
+    <br />\r
+    <br />\r
+    <br />\r
+    <p><font SIZE="1">\r
+    <?php\r
+           if ($usehtmleditor) {\r
+               helpbutton("richtext", get_string("helprichtext"), "moodle", true, true);\r
+           } else {\r
+               helpbutton("text", get_string("helptext"), "moodle", true, true);\r
+           }\r
+    ?>\r
+    </font></p>\r
+    </td>\r
+    <td>\r
+        <?php  if (isset($err["questiontext"])) {\r
+               formerr($err["questiontext"]); \r
+               echo "<br />";\r
+           }\r
+\r
+           print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext);\r
+\r
+           if ($usehtmleditor) {   /// Trying this out for a while\r
+               echo '<input type="hidden" name="questiontextformat" value="'.FORMAT_HTML.'">';\r
+           } else {\r
+               echo "<div align=right>";\r
+               print_string("formattexttype");\r
+               echo ":&nbsp;";\r
+               if (!isset($question->questiontextformat)) {\r
+                   $question->questiontextformat = FORMAT_MOODLE;\r
+               }\r
+               choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, "");\r
+               helpbutton("textformat", get_string("helpformatting"));\r
+               echo "</div>";\r
+           }\r
+        ?>\r
+    </td>\r
+</tr>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("imagedisplay", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   if (empty($images)) {\r
+            print_string("noimagesyet");\r
+        } else {\r
+            choose_from_menu($images, "image", "$question->image", get_string("none"),"","");\r
+        }\r
+    ?>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("answerhowmany", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php \r
+        $menu[0] = get_string("answersingleno", "quiz");\r
+        $menu[1] = get_string("answersingleyes", "quiz");\r
+        choose_from_menu($menu, "single", "$options->single", "");\r
+        unset($menu);\r
+     ?>\r
+    </TD>\r
+</TR>\r
+\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("choices", "quiz") ?></B>:</P></TD>\r
+    <TD><P><?php  print_string("fillouttwochoices", "quiz") ?></P>\r
+    </TD>\r
+</TR>\r
+\r
+<?PHP \r
+    for ($i=1; $i<=QUIZ_MAX_NUMBER_ANSWERS; $i++) {\r
+?>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  echo get_string("choice", "quiz")." $i";  ?>:</B></P></TD>\r
+    <TD>\r
+        <INPUT type="text" name="answer[]" size=50 maxlength=255 value="<?php  p($answers[$i-1]->answer) ?>">&nbsp;&nbsp;\r
+        <?php  print_string("grade");\r
+           echo ":&nbsp;";\r
+           choose_from_menu($gradeoptionsfull, "fraction[]", $answers[$i-1]->fraction, ""); ?>\r
+        <BR>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("feedback", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+        <textarea name="feedback[]" rows=2 cols=50 wrap="virtual"><?php  p($answers[$i-1]->feedback) ?></textarea>\r
+    </TD>\r
+</TR>\r
+\r
+<TR valign=top>\r
+    <TD colspan=2>&nbsp;</TD>\r
+</TR>\r
+\r
+<?php\r
+    } /// End of loop, printing answers\r
+?>\r
+</TABLE>\r
+\r
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">\r
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">\r
+<INPUT type="submit" value="<?php  print_string("savechanges") ?>">\r
+\r
+</CENTER>\r
+</FORM>\r
+<?php  \r
+   if ($usehtmleditor) { \r
+       print_richedit_javascript("theform", "questiontext", "no");\r
+   }\r
+?>\r
diff --git a/mod/quiz/questiontypes/multichoice/questiontype.php b/mod/quiz/questiontypes/multichoice/questiontype.php
new file mode 100644 (file)
index 0000000..06c53cb
--- /dev/null
@@ -0,0 +1,274 @@
+<?PHP  // $Id$
+
+///////////////////
+/// MULTICHOICE ///
+///////////////////
+
+/// QUESTION TYPE CLASS //////////////////
+
+///
+/// This class contains some special features in order to make the
+/// question type embeddable within a multianswer (cloze) question
+///
+
+class quiz_multichoice_qtype extends quiz_default_questiontype {
+
+    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 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 "
+                                       . $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 "<table align=\"right\">";
+        $stranswer = get_string("answer", "quiz");
+        echo "<tr><td valign=\"top\">$stranswer:&nbsp;&nbsp;</td><td>";
+        echo "<table>";
+        $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 '<tr><td valign="top">';
+
+            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 "<input $readonly $name $checked $type  value=\"$answer->id\" />";
+           
+            echo "</td>";
+            if ($readonly and $quiz->correctanswers || $quiz->feedback
+                    and !empty($correctanswers[$nameprefix.$aid])) {
+                echo '<td valign="top" class="highlight">'.format_text("$qnumchar. $answer->answer").'</td>';
+            } else {
+                echo '<td valign="top">'.format_text("$qnumchar. $answer->answer").'</td>';
+            }
+            if ($quiz->feedback) {
+               echo "<td valign=\"top\">&nbsp;";
+               if ($checked) { // Simpliest condition to use here
+                   quiz_print_comment($answer->feedback);
+               }
+               echo "</td>";
+           }
+           echo "</tr>";
+        }
+        echo "</table>";
+        echo "</td></tr></table>";
+    }
+
+    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 (file)
index 0000000..6d1ace1
--- /dev/null
@@ -0,0 +1,25 @@
+<?PHP // $Id$
+
+            // 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");
+
+?>
diff --git a/mod/quiz/questiontypes/numerical/icon.gif b/mod/quiz/questiontypes/numerical/icon.gif
new file mode 100644 (file)
index 0000000..2878042
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 (file)
index 0000000..2488599
--- /dev/null
@@ -0,0 +1,141 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">
+<CENTER>
+<TABLE cellpadding=5>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>
+    <TD>
+    <?php   quiz_category_select_menu($course->id, true, true); ?>
+    </TD>
+</TR>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>
+    <TD>
+        <INPUT type="text" name="name" size=50 value="<?php  p($question->name) ?>">
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>
+    </TD>
+</TR>
+<tr valign=top>
+    <td align="right"><p><b><?php  print_string("question", "quiz") ?>:</b></p>
+    <br />
+    <br />
+    <br />
+    <p><font SIZE="1">
+    <?php
+           if ($usehtmleditor) {
+               helpbutton("richtext", get_string("helprichtext"), "moodle", true, true);
+           } else {
+               helpbutton("text", get_string("helptext"), "moodle", true, true);
+           }
+    ?>
+    </font></p>
+    </td>
+    <td>
+        <?php  if (isset($err["questiontext"])) {
+               formerr($err["questiontext"]); 
+               echo "<br />";
+           }
+
+           print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext);
+
+           if ($usehtmleditor) {   /// Trying this out for a while
+               echo '<input type="hidden" name="questiontextformat" value="'.FORMAT_HTML.'">';
+           } else {
+               echo "<div align=right>";
+               print_string("formattexttype");
+               echo ":&nbsp;";
+               if (!isset($question->questiontextformat)) {
+                   $question->questiontextformat = FORMAT_MOODLE;
+               }
+               choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, "");
+               helpbutton("textformat", get_string("helpformatting"));
+               echo "</div>";
+           }
+        ?>
+    </td>
+</tr>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("imagedisplay", "quiz") ?>:</B></P></TD>
+    <TD>
+    <?php   if (empty($images)) {
+            print_string("noimagesyet");
+        } else {
+            choose_from_menu($images, "image", "$question->image", get_string("none"),"","");
+        }
+    ?>
+    </TD>
+</TR>
+
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("correctanswer", "quiz") ?>:</B></P></TD>
+    <?php
+        // Even thou the rest of the module can handle up to six numerical answers,
+        // this form will limit the number of numerical answers to one only.
+        if (is_numeric($answers[0]->min) && is_numeric($answers[0]->answer)) {
+            $acceptederror = (float)($answers[0]->answer)
+                           - (float)($answers[0]->min);
+        } else {
+            $acceptederror = "";
+        }
+    ?>
+    <TD>
+        <INPUT align="LEFT" type="text" id="correct0" name="answer[]" size="20" value="<?php  p($answers[0]->answer) ?>"/>&nbsp;&nbsp;
+    </TD>
+</TR>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("acceptederror", "quiz"); ?>:</B></P></TD>
+    <TD>
+        <INPUT align="LEFT" type="text" id="acceptederror0" name="acceptederror[]" size="15" value="<?php  p($acceptederror) ?>" />&plusmn;
+        <!-- Values max and min will be determined when the form is submitted -->
+        <INPUT type="HIDDEN" id="min0" name="min[]" value=""/>
+        <INPUT type="HIDDEN" id="max0" name="max[]" value=""/>
+        <INPUT type="HIDDEN" name="fraction[]" value="1"/>
+        <BR/>
+    </TD>
+</TR>
+<TR valign=top>
+    <TD align=right><P><B><?php  print_string("feedback", "quiz") ?>:</B></P></TD>
+    <TD>
+        <textarea name="feedback[]" rows=2 cols=50 wrap="virtual"><?php  p($answers[0]->feedback) ?></textarea>
+    </TD>
+</TR>
+</TABLE>
+
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">
+<INPUT type="submit" onClick="return determineMinAndMax();"  value="<?php  print_string("savechanges") ?>">
+</CENTER>
+</FORM>
+<SCRIPT language="JAVASCRIPT">
+function determineMinAndMax() {
+    // This client-side script will determine the values for min and max
+    // based on the input for answer and acceptederror.
+    with(document.theform) {
+        if (correct0.value=='') {
+            alert('<?php  print_string("missingcorrectanswer","quiz") ?>');
+            return false;
+        } else if (acceptederror0.value=='') {
+            var correct= parseFloat(correct0.value);
+            if (!isNaN(correct)) {
+                min0.value= correct;
+                max0.value= correct;
+            }
+            return true;
+        } else if (isNaN(acceptederror0.value) || isNaN(correct0.value)) {
+            alert('<?php  print_string("answerswithacceptederrormarginmustbenumeric", "quiz") ?>');
+            return false;
+        } else {
+            var correct= parseFloat(correct0.value);
+            var error= Math.abs(acceptederror0.value);
+            min0.value= correct-error;
+            max0.value= correct+error;
+            return true;
+        }
+    }
+}
+</SCRIPT>
+<?php  
+   if ($usehtmleditor) { 
+       print_richedit_javascript("theform", "questiontext", "no");
+   }
+?>
+
diff --git a/mod/quiz/questiontypes/numerical/questiontype.php b/mod/quiz/questiontypes/numerical/questiontype.php
new file mode 100644 (file)
index 0000000..7755210
--- /dev/null
@@ -0,0 +1,178 @@
+<?PHP  // $Id$
+
+/////////////////
+/// NUMERICAL ///
+/////////////////
+
+/// QUESTION TYPE CLASS //////////////////
+
+///
+/// This class contains some special features in order to make the
+/// question type embeddable within a multianswer (cloze) question
+///
+
+/// This question type behaves like shortanswer in most cases.
+/// Therefore, it extends the shortanswer question type...
+
+require_once("$CFG->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 (file)
index 0000000..b6737f3
--- /dev/null
@@ -0,0 +1,6 @@
+<?PHP // $Id$
+
+            print_heading_with_help(get_string("editingrandom", "quiz"), "random", "quiz");
+            require("random.html");
+
+?>
diff --git a/mod/quiz/questiontypes/random/icon.gif b/mod/quiz/questiontypes/random/icon.gif
new file mode 100644 (file)
index 0000000..46dac35
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 (file)
index 0000000..6bced4e
--- /dev/null
@@ -0,0 +1,206 @@
+<?PHP  // $Id$
+
+//////////////
+/// RANDOM ///
+//////////////
+
+/// QUESTION TYPE CLASS //////////////////
+class quiz_random_qtype extends quiz_default_questiontype {
+
+    var $possiblerandomqtypes = array(SHORTANSWER,
+                                      NUMERICAL,
+                                      MULTICHOICE,
+                                      MATCH,
+                                   // RANDOMSAMATCH,// Can cause unexpected outcomes
+                                      TRUEFALSE,
+                                      MULTIANSWER);
+
+    // Carries questions available as randoms sorted by category
+    // This array is used when needed only
+    var $catrandoms = array();
+
+    function name() {
+        return 'random';
+    }
+
+    function save_question_options($question) {
+        /// No options to be saved for this question type:
+        return true;
+    }
+
+    function wrapped_questions($question) {
+        global $QUIZ_QTYPES;
+        
+        foreach ($question->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 '<input type="hidden" name="' . $nameprefix
+                    . '" value="' . $actualquestion->id . '"/>';
+            return $QUIZ_QTYPES[$actualquestion->qtype]
+                    ->print_question_formulation_and_controls($actualquestion,
+                    $quiz, $readonly, $answers, $correctanswers,
+                    quiz_qtype_nameprefix($actualquestion, $nameprefix));
+        } else {
+            echo '<p>' . get_string('random', 'quiz') . '</p>';
+        }
+    }
+
+    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 (file)
index 0000000..8c453bc
--- /dev/null
@@ -0,0 +1,30 @@
+<center>\r
+<form name="theform" method="post" action="question.php">\r
+\r
+<table cellpadding=5>\r
+<tr valign=top>\r
+    <td align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>\r
+    <td>\r
+       <?php  quiz_category_select_menu($course->id, true, true); ?>\r
+    </td>\r
+</tr>\r
+<tr valign=top>\r
+    <td align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>\r
+    <td>\r
+        <?php  if (empty($question->name)) {\r
+               $question->name = get_string("random", "quiz");\r
+           } ?>\r
+        <input type="text" name="name" size=40 value="<?php  p($question->name) ?>">\r
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>\r
+    </td>\r
+</tr>\r
+</table>\r
+\r
+<input type="hidden" name=questiontext value="---">\r
+\r
+<input type="hidden" name=id value="<?php  p($question->id) ?>">\r
+<input type="hidden" name=qtype value="<?php  p($question->qtype) ?>">\r
+<input type="submit" value="<?php  print_string("savechanges") ?>">\r
+</form>\r
+</center>\r
+\r
diff --git a/mod/quiz/questiontypes/randomsamatch/editquestion.php b/mod/quiz/questiontypes/randomsamatch/editquestion.php
new file mode 100644 (file)
index 0000000..4e89551
--- /dev/null
@@ -0,0 +1,11 @@
+<?PHP // $Id$
+            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");
+
+?>
diff --git a/mod/quiz/questiontypes/randomsamatch/icon.gif b/mod/quiz/questiontypes/randomsamatch/icon.gif
new file mode 100644 (file)
index 0000000..6854363
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 (file)
index 0000000..d84cfbd
--- /dev/null
@@ -0,0 +1,231 @@
+<?PHP  // $Id$
+
+/////////////////////
+/// RANDOMSAMATCH ///
+/////////////////////
+
+/// The use of this question type together with the
+/// question type RANDOM within the same quiz can cause
+/// a shortanswer question to appear in a RANDOM question
+/// as well as one of the matcher questions in a question of this type
+
+/// QUESTION TYPE CLASS //////////////////
+class quiz_randomsamatch_qtype extends quiz_match_qtype {
+/// Extends MATCH as there are quite a few simularities...
+
+    // $catrandoms carries question ids for shortanswer questions
+    // available as random questios.
+    // They are sorted by category.
+    var $catrandoms = array();
+
+    function name() {
+        return 'randomsamatch';
+    }
+
+    function save_question_options($question) {
+        $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;
+            }
+        }
+        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 '<table border="0" cellpadding="10">';
+        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 '<tr><td align="left" valign="top">';
+            echo $saquestion->questiontext;
+            echo '</td>';
+            echo '<td align="right" valign="top">';
+            if (!empty($correctanswers)
+                    && $correctanswers[$inputname]->id == $response) {
+                echo '<span="highlight">';
+                choose_from_menu($saanswers, $inputname, $response);
+                echo '</span><br />';
+            } 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 '</td></tr>';
+        }
+        echo '</table>';
+    }
+
+    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 (file)
index 0000000..e763e60
--- /dev/null
@@ -0,0 +1,98 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">\r
+<CENTER>\r
+<TABLE cellpadding=5>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?PHP echo $categories[$question->category]; ?>\r
+    <input type="hidden" name="category" value="<?PHP echo "$question->category"; ?>">\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+        <?PHP\r
+           if (empty($question->name)) {\r
+               $question->name =  get_string("randomsamatch", "quiz");\r
+           }\r
+        ?>\r
+        <INPUT type="text" name="name" size=40 value="<?php  p($question->name) ?>">\r
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>\r
+    </TD>\r
+</TR>\r
+<tr valign=top>\r
+    <td align=right><p><b><?php  print_string("introduction", "quiz") ?>:</b></p></td>\r
+    <br />\r
+    <br />\r
+    <br />\r
+    <p><font SIZE="1">\r
+    <?php\r
+           if ($usehtmleditor) {\r
+               helpbutton("richtext", get_string("helprichtext"), "moodle", true, true);\r
+           } else {\r
+               helpbutton("text", get_string("helptext"), "moodle", true, true);\r
+           }\r
+    ?>\r
+    </font></p>\r
+    </td>\r
+    <td>\r
+        <?php  if (isset($err["questiontext"])) {\r
+               formerr($err["questiontext"]); \r
+               echo "<br />";\r
+           }\r
+\r
+           if (empty($question->questiontext)) {\r
+               $question->questiontext =  get_string("randomsamatchintro", "quiz");\r
+           }\r
+           print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext);\r
+\r
+           if ($usehtmleditor) {   /// Trying this out for a while\r
+               echo '<input type="hidden" name="questiontextformat" value="'.FORMAT_HTML.'">';\r
+           } else {\r
+               echo "<div align=right>";\r
+               print_string("formattexttype");\r
+               echo ":&nbsp;";\r
+               if (!$question->questiontextformat) {\r
+                   $question->questiontextformat = FORMAT_MOODLE;\r
+               }\r
+               choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, "");\r
+               helpbutton("textformat", get_string("helpformatting"));\r
+               echo "</div>";\r
+           }\r
+        ?>\r
+    </td>\r
+</tr>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("randomsamatchnumber", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php \r
+        if ($numberavailable < 2) {\r
+            echo "ERROR";\r
+            $maxrandom=2;\r
+        } else if ($numberavailable < 6) {\r
+            $maxrandom = $numberavailable;\r
+        } else {\r
+            $maxrandom = QUIZ_MAX_NUMBER_ANSWERS;\r
+        }\r
+\r
+        for ($i=2;$i<=$maxrandom;$i++) {\r
+            $menu[$i] = $i;\r
+        }\r
+        choose_from_menu($menu, "choose", "$options->choose", "");\r
+        unset($menu);\r
+     ?>\r
+    </TD>\r
+</TR>\r
+</TABLE>\r
+\r
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">\r
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">\r
+<INPUT type="submit" value="<?php  print_string("savechanges") ?>">\r
+\r
+</CENTER>\r
+</FORM>\r
+<?php  \r
+   if ($usehtmleditor) { \r
+       print_richedit_javascript("theform", "questiontext", "no");\r
+   }\r
+?>\r
diff --git a/mod/quiz/questiontypes/shortanswer/editquestion.php b/mod/quiz/questiontypes/shortanswer/editquestion.php
new file mode 100644 (file)
index 0000000..3635b73
--- /dev/null
@@ -0,0 +1,23 @@
+<?PHP // $Id$
+            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; $i<QUIZ_MAX_NUMBER_ANSWERS; $i++) {
+                $answers[] = "";   // Make answer slots, default as blank
+            }
+            if (!empty($answersraw)) {
+                $i=0;
+                foreach ($answersraw as $answer) {
+                    $answers[$i] = $answer;   // insert answers into slots
+                    $i++;
+                }
+            }
+            print_heading_with_help(get_string("editingshortanswer", "quiz"), "shortanswer", "quiz");
+            require("shortanswer.html");
+
+?>
diff --git a/mod/quiz/questiontypes/shortanswer/icon.gif b/mod/quiz/questiontypes/shortanswer/icon.gif
new file mode 100644 (file)
index 0000000..747eb01
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 (file)
index 0000000..dd826fc
--- /dev/null
@@ -0,0 +1,198 @@
+<?PHP  // $Id$
+
+///////////////////
+/// SHORTANSWER ///
+///////////////////
+
+/// QUESTION TYPE CLASS //////////////////
+
+///
+/// This class contains some special features in order to make the
+/// question type embeddable within a multianswer (cloze) question
+///
+
+class quiz_shortanswer_qtype extends quiz_default_questiontype {
+
+    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.*, sa.usecase
+                                FROM {$CFG->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 "<p align=\"right\">$stranswer: <input type=\"text\" $readonly $inputname size=\"80\" $value /></p>";
+
+        if ($quiz->feedback && isset($answers[$nameprefix])
+                && $feedback = $answers[$nameprefix]->feedback) {
+           quiz_print_comment("<p align=\"right\">$feedback</p>");
+        }
+        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 (file)
index 0000000..ce62144
--- /dev/null
@@ -0,0 +1,126 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">\r
+<CENTER>\r
+<TABLE cellpadding=5>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   quiz_category_select_menu($course->id, true, true); ?>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+        <INPUT type="text" name="name" size=50 value="<?php  p($question->name) ?>">\r
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>\r
+    </TD>\r
+</TR>\r
+<tr valign=top>\r
+    <td align="right"><p><b><?php  print_string("question", "quiz") ?>:</b></p>\r
+    <br />\r
+    <br />\r
+    <br />\r
+    <p><font SIZE="1">\r
+    <?php\r
+           if ($usehtmleditor) {\r
+               helpbutton("richtext", get_string("helprichtext"), "moodle", true, true);\r
+           } else {\r
+               helpbutton("text", get_string("helptext"), "moodle", true, true);\r
+           }\r
+    ?>\r
+    </font></p>\r
+    </td>\r
+    <td>\r
+        <?php  if (isset($err["questiontext"])) {\r
+               formerr($err["questiontext"]); \r
+               echo "<br />";\r
+           }\r
+\r
+           print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext);\r
+\r
+           if ($usehtmleditor) {   /// Trying this out for a while\r
+               echo '<input type="hidden" name="questiontextformat" value="'.FORMAT_HTML.'">';\r
+           } else {\r
+               echo "<div align=right>";\r
+               print_string("formattexttype");\r
+               echo ":&nbsp;";\r
+               if (!isset($question->questiontextformat)) {\r
+                   $question->questiontextformat = FORMAT_MOODLE;\r
+               }\r
+               choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, "");\r
+               helpbutton("textformat", get_string("helpformatting"));\r
+               echo "</div>";\r
+           }\r
+        ?>\r
+    </td>\r
+</tr>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("imagedisplay", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   if (empty($images)) {\r
+            print_string("noimagesyet");\r
+        } else {\r
+            choose_from_menu($images, "image", "$question->image", get_string("none"),"","");\r
+        }\r
+    ?>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("casesensitive", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php \r
+        unset($menu);\r
+        $menu[0] = get_string("caseno", "quiz");\r
+        $menu[1] = get_string("caseyes", "quiz");\r
+        choose_from_menu($menu, "usecase", "$options->usecase", "");\r
+     ?>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("correctanswers", "quiz") ?></B>:</P></TD>\r
+    <TD>\r
+        <P><?php  print_string("filloutoneanswer", "quiz") ?></P>\r
+    </TD>\r
+\r
+\r
+<?PHP \r
+    for ($i=1; $i<=QUIZ_MAX_NUMBER_ANSWERS; $i++) {\r
+?>\r
+\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  echo get_string("answer", "quiz")." $i";  ?>:</B></P></TD>\r
+    <TD>\r
+        <INPUT type="text" name="answer[]" size=50 value="<?php  p($answers[$i-1]->answer) ?>">&nbsp;&nbsp;\r
+        <?php  print_string("grade");\r
+           echo ":&nbsp;";\r
+           choose_from_menu($gradeoptions, "fraction[]", $answers[$i-1]->fraction,""); ?>\r
+        <BR>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("feedback", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+        <textarea name="feedback[]" rows=2 cols=50 wrap="virtual"><?php  p($answers[$i-1]->feedback) ?></textarea>\r
+    </TD>\r
+</TR>\r
+\r
+<TR valign=top>\r
+    <TD colspan=2>&nbsp;</TD>\r
+</TR>\r
+\r
+<?PHP\r
+    }\r
+?>\r
+\r
+</TABLE>\r
+\r
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">\r
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">\r
+<INPUT type="submit" value="<?php  print_string("savechanges") ?>">\r
+\r
+</CENTER>\r
+</FORM>\r
+<?php  \r
+   if ($usehtmleditor) { \r
+       print_richedit_javascript("theform", "questiontext", "no");\r
+   }\r
+?>\r
diff --git a/mod/quiz/questiontypes/truefalse/editquestion.php b/mod/quiz/questiontypes/truefalse/editquestion.php
new file mode 100644 (file)
index 0000000..c98f55e
--- /dev/null
@@ -0,0 +1,28 @@
+<?PHP // $Id$
+
+            if (!empty($question->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 (file)
index 0000000..4996433
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 (file)
index 0000000..30e8f64
--- /dev/null
@@ -0,0 +1,179 @@
+<?PHP  // $Id$
+
+/////////////////
+/// TRUEFALSE ///
+/////////////////
+
+/// QUESTION TYPE CLASS //////////////////
+class quiz_truefalse_qtype extends quiz_default_questiontype {
+
+    function name() {
+        return 'truefalse';
+    }
+
+    function save_question_options($question) {
+        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;
+            }
+        }
+        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 "<table align=\"right\" cellpadding=\"5\"><tr><td align=\"right\">$stranswer:&nbsp;&nbsp;";
+        echo "<td $truecorrect>";
+        echo "<input $truechecked type=\"radio\" $readonly $inputname value=\"$true->id\" />$true->answer";
+        echo "</td><td $falsecorrect>";
+        echo "<input $falsechecked type=\"radio\"  $readonly $inputname value=\"$false->id\" />$false->answer";
+        echo "</td></tr></table><br clear=\"all\">";// changed from CLEAR=ALL jm
+        if ($quiz->feedback && isset($answers[$nameprefix])
+                && $feedback = $answers[$nameprefix]->feedback) {
+           quiz_print_comment(
+                    "<p align=\"right\">$feedback</p>");
+        }
+    }
+
+    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 (file)
index 0000000..3b11eb0
--- /dev/null
@@ -0,0 +1,103 @@
+<FORM name="theform" method="post" <?php echo $onsubmit ?> action="question.php">\r
+<CENTER>\r
+<TABLE cellpadding=5>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("category", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   \r
+        quiz_category_select_menu($course->id, true, true);\r
+     ?>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("questionname", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+        <INPUT type="text" name="name" size=50 value="<?php  p($question->name) ?>">\r
+        <?php  if (isset($err["name"])) formerr($err["name"]); ?>\r
+    </TD>\r
+</TR>\r
+<tr valign=top>\r
+    <td align="right"><p><b><?php  print_string("question", "quiz") ?>:</b></p>\r
+    <br />\r
+    <br />\r
+    <br />\r
+    <p><font SIZE="1">\r
+    <?php\r
+           if ($usehtmleditor) {\r
+               helpbutton("richtext", get_string("helprichtext"), "moodle", true, true);\r
+           } else {\r
+               helpbutton("text", get_string("helptext"), "moodle", true, true);\r
+           }\r
+    ?>\r
+    </font></p>\r
+    </td>\r
+    <td>\r
+        <?php  if (isset($err["questiontext"])) {\r
+               formerr($err["questiontext"]); \r
+               echo "<br />";\r
+           }\r
+\r
+           print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext);\r
+\r
+           if ($usehtmleditor) {   /// Trying this out for a while\r
+               echo '<input type="hidden" name="questiontextformat" value="'.FORMAT_HTML.'">';\r
+           } else {\r
+               echo "<div align=right>";\r
+               print_string("formattexttype");\r
+               echo ":&nbsp;";\r
+               if (!isset($question->questiontextformat)) {\r
+                   $question->questiontextformat = FORMAT_MOODLE;\r
+               }\r
+               choose_from_menu(format_text_menu(), "questiontextformat", $question->questiontextformat, "");\r
+               helpbutton("textformat", get_string("helpformatting"));\r
+               echo "</div>";\r
+           }\r
+        ?>\r
+    </td>\r
+</tr>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("imagedisplay", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+    <?php   if (empty($images)) {\r
+            print_string("noimagesyet");\r
+        } else {\r
+            choose_from_menu($images, "image", "$question->image", get_string("none"),"","");\r
+        }\r
+    ?>\r
+    </TD>\r
+</TR>\r
+\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("correctanswer", "quiz") ?>:</B></P></TD>\r
+    <TD>\r
+        <?php  $menu[0] = get_string("false", "quiz");\r
+           $menu[1] = get_string("true", "quiz");\r
+           choose_from_menu($menu, "answer", "$question->answer", ""); ?>\r
+        <BR>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("feedback", "quiz") ?> (<?php  print_string("true", "quiz") ?>):</B></P></TD>\r
+    <TD>\r
+        <textarea name="feedbacktrue" rows=2 cols=50 wrap="virtual"><?php  p($true->feedback) ?></textarea>\r
+    </TD>\r
+</TR>\r
+<TR valign=top>\r
+    <TD align=right><P><B><?php  print_string("feedback", "quiz") ?> (<?php  print_string("false", "quiz") ?>):</B></P></TD>\r
+    <TD>\r
+        <textarea name="feedbackfalse" rows=2 cols=50 wrap="virtual"><?php  p($false->feedback) ?></textarea>\r
+    </TD>\r
+</TR>\r
+</TABLE>\r
+\r
+<INPUT type="hidden" name=id value="<?php  p($question->id) ?>">\r
+<INPUT type="hidden" name=qtype value="<?php  p($question->qtype) ?>">\r
+<INPUT type="submit" value="<?php  print_string("savechanges") ?>">\r
+\r
+</CENTER>\r
+</FORM>\r
+<?php  \r
+   if ($usehtmleditor) { \r
+       print_richedit_javascript("theform", "questiontext", "no");\r
+   }\r
+?>\r
index dbbb269025acb8fa1f0c1fc2e17d9a2ca101dcf1..351bf1173761298ce4c21a0b89874d6d8ef6e346 100644 (file)
@@ -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!");
             }
 
index 5b4ce5f55def2eb1fbba08eb31a22ddc41b1f295..b2e9723874e689c2d2f96b143e7349e3a9b3788d 100644 (file)
@@ -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!");
                 }
 
index 8665af476d0eeff53a7e468a293beff6b5c4d5d8..951e0944f78470604e311663edf00f25c8861df1 100644 (file)
     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!");
     }
 
     $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");