$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);
?>
// 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);
// 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) {
/// 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];
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) {
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
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;
}
/// 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;
// 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;
}
}
-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: ";
- 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: </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\"> ";
- 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) {
}
}
- 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
}
}
// 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";
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
$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) {
/// if necessary.
global $USER;
+ global $QUIZ_QTYPES;
// First find the attempt in the database (start of attempt)
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 = "";
}
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);
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
}
}
- 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);
--- /dev/null
+<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 ": ";\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
--- /dev/null
+<?PHP // $Id$
+
+ print_heading_with_help(get_string("editingdescription", "quiz"), "description", "quiz");
+ require("description.html");
+
+?>
--- /dev/null
+<?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();
+
+?>
--- /dev/null
+<?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");
+
+?>
--- /dev/null
+<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 ": ";\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"; ?> \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
--- /dev/null
+<?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();
+
+?>
--- /dev/null
+<?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');
+
+?>
--- /dev/null
+<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");
+
+ }
+
+?>
+
--- /dev/null
+<?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;
+}
+
+?>
--- /dev/null
+<?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");
+
+?>
--- /dev/null
+<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 ": ";\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) ?>"> \r
+ <?php print_string("grade");\r
+ echo ": ";\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> </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
--- /dev/null
+<?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: </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\"> ";
+ 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();
+
+?>
--- /dev/null
+<?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");
+
+?>
--- /dev/null
+<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 ": ";
+ 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) ?>"/>
+ </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) ?>" />±
+ <!-- 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");
+ }
+?>
+
--- /dev/null
+<?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();
+
+?>
--- /dev/null
+<?PHP // $Id$
+
+ print_heading_with_help(get_string("editingrandom", "quiz"), "random", "quiz");
+ require("random.html");
+
+?>
--- /dev/null
+<?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();
+
+?>
--- /dev/null
+<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
--- /dev/null
+<?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");
+
+?>
--- /dev/null
+<?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();
+
+?>
--- /dev/null
+<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 ": ";\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
--- /dev/null
+<?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");
+
+?>
--- /dev/null
+<?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();
+
+?>
--- /dev/null
+<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 ": ";\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) ?>"> \r
+ <?php print_string("grade");\r
+ echo ": ";\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> </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
--- /dev/null
+<?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");
+
+?>
--- /dev/null
+<?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: ";
+ 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();
+
+?>
--- /dev/null
+<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 ": ";\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
$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!");
}
/// 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
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!");
}
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");