moodle/mod/quiz/lib.php
moodler 54a67a5921 First cut at adding new "fixed" Match question type, which is manually
created.  Works OK after limited testing.

I've also renamed some strings to do with "Random Match", so that this
question type is now called "Random Short-Answer Match".

Later there will be a new 'Random Match' which randomly selects one of the
existing "Match" questions.
2003-03-30 16:46:50 +00:00

1559 lines
58 KiB
PHP

<?PHP // $Id$
/// Library of function for module quiz
/// CONSTANTS ///////////////////////////////////////////////////////////////////
define("GRADEHIGHEST", "1");
define("GRADEAVERAGE", "2");
define("ATTEMPTFIRST", "3");
define("ATTEMPTLAST", "4");
$QUIZ_GRADE_METHOD = array ( GRADEHIGHEST => get_string("gradehighest", "quiz"),
GRADEAVERAGE => get_string("gradeaverage", "quiz"),
ATTEMPTFIRST => get_string("attemptfirst", "quiz"),
ATTEMPTLAST => get_string("attemptlast", "quiz"));
define("SHORTANSWER", "1");
define("TRUEFALSE", "2");
define("MULTICHOICE", "3");
define("RANDOM", "4");
define("MATCH", "5");
define("RANDOMSAMATCH", "6");
$QUIZ_QUESTION_TYPE = array ( MULTICHOICE => get_string("multichoice", "quiz"),
TRUEFALSE => get_string("truefalse", "quiz"),
SHORTANSWER => get_string("shortanswer", "quiz"),
MATCH => get_string("match", "quiz"),
RANDOMSAMATCH => get_string("randomsamatch", "quiz") );
$QUIZ_FILE_FORMAT = array ( "custom" => get_string("custom", "quiz"),
"webct" => get_string("webct", "quiz"),
"qti" => get_string("qti", "quiz"),
"missingword" => get_string("missingword", "quiz") );
define("QUIZ_PICTURE_DEFAULT_HEIGHT", "200");
define("QUIZ_MAX_NUMBER_ANSWERS", "8");
/// FUNCTIONS ///////////////////////////////////////////////////////////////////
function quiz_add_instance($quiz) {
/// Given an object containing all the necessary data,
/// (defined by the form in mod.html) this function
/// will create a new instance and return the id number
/// of the new instance.
$quiz->created = time();
$quiz->timemodified = time();
$quiz->timeopen = make_timestamp($quiz->openyear, $quiz->openmonth, $quiz->openday,
$quiz->openhour, $quiz->openminute, 0);
$quiz->timeclose = make_timestamp($quiz->closeyear, $quiz->closemonth, $quiz->closeday,
$quiz->closehour, $quiz->closeminute, 0);
if (!$quiz->id = insert_record("quiz", $quiz)) {
return false; // some error occurred
}
// The grades for every question in this quiz are stored in an array
if ($quiz->grades) {
foreach ($quiz->grades as $question => $grade) {
if ($question and $grade) {
unset($questiongrade);
$questiongrade->quiz = $quiz->id;
$questiongrade->question = $question;
$questiongrade->grade = $grade;
if (!insert_record("quiz_question_grades", $questiongrade)) {
return false;
}
}
}
}
return $quiz->id;
}
function quiz_update_instance($quiz) {
/// Given an object containing all the necessary data,
/// (defined by the form in mod.html) this function
/// will update an existing instance with new data.
$quiz->timemodified = time();
$quiz->timeopen = make_timestamp($quiz->openyear, $quiz->openmonth, $quiz->openday,
$quiz->openhour, $quiz->openminute, 0);
$quiz->timeclose = make_timestamp($quiz->closeyear, $quiz->closemonth, $quiz->closeday,
$quiz->closehour, $quiz->closeminute, 0);
$quiz->id = $quiz->instance;
if (!update_record("quiz", $quiz)) {
return false; // some error occurred
}
// The grades for every question in this quiz are stored in an array
// Insert or update records as appropriate
$existing = get_records("quiz_question_grades", "quiz", $quiz->id, "", "question,grade,id");
if ($quiz->grades) {
foreach ($quiz->grades as $question => $grade) {
if ($question and $grade) {
unset($questiongrade);
$questiongrade->quiz = $quiz->id;
$questiongrade->question = $question;
$questiongrade->grade = $grade;
if (isset($existing[$question])) {
if ($existing[$question]->grade != $grade) {
$questiongrade->id = $existing[$question]->id;
if (!update_record("quiz_question_grades", $questiongrade)) {
return false;
}
}
} else {
if (!insert_record("quiz_question_grades", $questiongrade)) {
return false;
}
}
}
}
}
return true;
}
function quiz_delete_instance($id) {
/// Given an ID of an instance of this module,
/// this function will permanently delete the instance
/// and any data that depends on it.
if (! $quiz = get_record("quiz", "id", "$id")) {
return false;
}
$result = true;
if ($attempts = get_record("quiz_attempts", "quiz", "$quiz->id")) {
foreach ($attempts as $attempt) {
if (! delete_records("quiz_responses", "attempt", "$attempt->id")) {
$result = false;
}
}
}
if (! delete_records("quiz_attempts", "quiz", "$quiz->id")) {
$result = false;
}
if (! delete_records("quiz_grades", "quiz", "$quiz->id")) {
$result = false;
}
if (! delete_records("quiz_question_grades", "quiz", "$quiz->id")) {
$result = false;
}
if (! delete_records("quiz", "id", "$quiz->id")) {
$result = false;
}
return $result;
}
function quiz_user_outline($course, $user, $mod, $quiz) {
/// Return a small object with summary information about what a
/// user has done with a given particular instance of this module
/// Used for user activity reports.
/// $return->time = the time they did it
/// $return->info = a short text description
if ($grade = get_record("quiz_grades", "userid", $user->id, "quiz", $quiz->id)) {
if ($grade->grade) {
$result->info = get_string("grade").": $grade->grade";
}
$result->time = $grade->timemodified;
return $result;
}
return NULL;
return $return;
}
function quiz_user_complete($course, $user, $mod, $quiz) {
/// Print a detailed representation of what a user has done with
/// a given particular instance of this module, for user activity reports.
return true;
}
function quiz_print_recent_activity(&$logs, $isteacher=false) {
/// Given a list of logs, assumed to be those since the last login
/// this function prints a short list of changes related to this module
/// If isteacher is true then perhaps additional information is printed.
/// This function is called from course/lib.php: print_recent_activity()
global $CFG, $COURSE_TEACHER_COLOR;
$content = "";
return $content; // True if anything was printed, otherwise false
}
function quiz_cron () {
/// Function to be run periodically according to the moodle cron
/// This function searches for things that need to be done, such
/// as sending out mail, toggling flags etc ...
global $CFG;
return true;
}
function quiz_grades($quizid) {
/// Must return an array of grades, indexed by user, and a max grade.
$return->grades = get_records_menu("quiz_grades", "quiz", $quizid, "", "userid,grade");
$return->maxgrade = get_field("quiz", "grade", "id", "$quizid");
return $return;
}
/// SQL FUNCTIONS ////////////////////////////////////////////////////////////////////
function quiz_move_questions($category1, $category2) {
global $CFG;
return execute_sql("UPDATE {$CFG->prefix}quiz_questions
SET category = '$category2'
WHERE category = '$category1'",
false);
}
function quiz_get_question_grades($quizid, $questionlist) {
global $CFG;
return get_records_sql("SELECT question,grade
FROM {$CFG->prefix}quiz_question_grades
WHERE quiz = '$quizid'
AND question IN ($questionlist)");
}
function quiz_get_grade_records($quiz) {
/// Gets all info required to display the table of quiz results
/// for report.php
global $CFG;
return get_records_sql("SELECT qg.*, u.firstname, u.lastname, u.picture
FROM {$CFG->prefix}quiz_grades qg,
{$CFG->prefix}user u
WHERE qg.quiz = '$quiz->id'
AND qg.userid = u.id");
}
function quiz_get_answers($question) {
// Given a question, returns the correct answers and grades
global $CFG;
switch ($question->qtype) {
case SHORTANSWER: // Could be multiple answers
return get_records_sql("SELECT a.*, sa.usecase, g.grade
FROM {$CFG->prefix}quiz_shortanswer sa,
{$CFG->prefix}quiz_answers a,
{$CFG->prefix}quiz_question_grades g
WHERE sa.question = '$question->id'
AND sa.question = a.question
AND sa.question = g.question");
break;
case TRUEFALSE: // Should be always two answers
return get_records_sql("SELECT a.*, g.grade
FROM {$CFG->prefix}quiz_answers a,
{$CFG->prefix}quiz_question_grades g
WHERE a.question = '$question->id'
AND a.question = g.question");
break;
case MULTICHOICE: // Should be multiple answers
return get_records_sql("SELECT a.*, mc.single, g.grade
FROM {$CFG->prefix}quiz_multichoice mc,
{$CFG->prefix}quiz_answers a,
{$CFG->prefix}quiz_question_grades g
WHERE mc.question = '$question->id'
AND mc.question = a.question
AND mc.question = g.question");
break;
case RANDOM:
return false; // Not done yet
break;
case MATCH:
return get_records_sql("SELECT ms.*, g.grade
FROM {$CFG->prefix}quiz_match_sub ms,
{$CFG->prefix}quiz_question_grades g
WHERE ms.question = '$question->id'
AND ms.question = g.question");
break;
case RANDOMSAMATCH: // Could be any of many answers, return them all
return get_records_sql("SELECT a.*, g.grade
FROM {$CFG->prefix}quiz_questions q,
{$CFG->prefix}quiz_answers a,
{$CFG->prefix}quiz_question_grades g
WHERE q.category = '$question->category'
AND q.qtype = ".SHORTANSWER."
AND q.id = a.question
AND g.question = '$question->id'");
break;
default:
return false;
}
}
function quiz_get_attempt_responses($attempt, $quiz) {
// 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, r.answer
FROM {$CFG->prefix}quiz_responses r,
{$CFG->prefix}quiz_questions q
WHERE r.attempt = '$attempt->id'
AND q.id = r.question
AND q.id IN ($quiz->questions)")) {
notify("Could not find any responses for that attempt!");
return false;
}
foreach ($responses as $key => $response) {
$responses[$key]->answer = explode(",",$response->answer);
}
return $responses;
}
//////////////////////////////////////////////////////////////////////////////////////
/// Any other quiz functions go here. Each of them must have a name that
/// starts with quiz_
function quiz_print_comment($text) {
global $THEME;
echo "<SPAN CLASS=feedbacktext>".text_to_html($text, true, false)."</SPAN>";
}
function quiz_print_correctanswer($text) {
global $THEME;
echo "<P ALIGN=RIGHT><SPAN CLASS=highlight>$text</SPAN></P>";
}
function quiz_print_question_icon($question) {
// Prints a question icon
global $QUIZ_QUESTION_TYPE;
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;
}
echo "</A>\n";
}
function quiz_print_question($number, $questionid, $grade, $courseid,
$feedback=NULL, $response=NULL, $actualgrade=NULL, $correct=NULL) {
/// Prints a quiz question, any format
if (!$question = get_record("quiz_questions", "id", $questionid)) {
notify("Error: Question not found!");
}
if (empty($actualgrade)) {
$actualgrade = 0;
}
$stranswer = get_string("answer", "quiz");
$strmarks = get_string("marks", "quiz");
echo "<TABLE WIDTH=100% CELLSPACING=10><TR><TD NOWRAP WIDTH=100 VALIGN=top>";
echo "<P ALIGN=CENTER><B>$number</B></P>";
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);
echo "</TD><TD VALIGN=TOP>";
switch ($question->qtype) {
case SHORTANSWER:
if (!$options = get_record("quiz_shortanswer", "question", $question->id)) {
notify("Error: Missing question options!");
}
echo text_to_html($question->questiontext);
if ($question->image) {
print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
}
if ($response) {
$value = "VALUE=\"$response[0]\"";
} else {
$value = "";
}
echo "<P ALIGN=RIGHT>$stranswer: <INPUT TYPE=TEXT NAME=q$question->id SIZE=20 $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 text_to_html($question->questiontext);
if ($question->image) {
print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
}
$truechecked = "";
$falsechecked = "";
if (!empty($response[$true->id])) {
$truechecked = "CHECKED";
$feedbackid = $true->id;
} else if (!empty($response[$false->id])) {
$falsechecked = "CHECKED";
$feedbackid = $false->id;
}
$truecorrect = "";
$falsecorrect = "";
if ($correct) {
if (!empty($correct[$true->id])) {
$truecorrect = "CLASS=highlight";
}
if (!empty($correct[$false->id])) {
$falsecorrect = "CLASS=highlight";
}
}
echo "<TABLE ALIGN=right cellpadding=5><TR><TD align=right>$stranswer:&nbsp;&nbsp;";
echo "<TD $truecorrect>";
echo "<INPUT $truechecked TYPE=RADIO NAME=\"q$question->id\" VALUE=\"$true->id\">$true->answer";
echo "</TD><TD $falsecorrect>";
echo "<INPUT $falsechecked TYPE=RADIO NAME=\"q$question->id\" VALUE=\"$false->id\">$false->answer</P>";
echo "</TD></TR></TABLE><BR CLEAR=ALL>";
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 text_to_html($question->questiontext);
if ($question->image) {
print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
}
echo "<TABLE ALIGN=right>";
echo "<TR><TD valign=top>$stranswer:&nbsp;&nbsp;</TD><TD>";
echo "<TABLE>";
$answerids = explode(",", $options->answers);
foreach ($answerids as $key => $answerid) {
$answer = $answers[$answerid];
$qnumchar = chr(ord('a') + $key);
if (empty($feedback) or empty($response[$answerid])) {
$checked = "";
} else {
$checked = "CHECKED";
}
echo "<TR><TD valign=top>";
if ($options->single) {
echo "<INPUT $checked TYPE=RADIO NAME=q$question->id VALUE=\"$answer->id\">";
} else {
echo "<INPUT $checked TYPE=CHECKBOX NAME=q$question->id"."a$answer->id VALUE=\"$answer->id\">";
}
echo "</TD>";
if (empty($feedback) or empty($correct[$answer->id])) {
echo "<TD valign=top>$qnumchar. $answer->answer</TD>";
} else {
echo "<TD valign=top CLASS=highlight>$qnumchar. $answer->answer</TD>";
}
if (!empty($feedback)) {
echo "<TD valign=top>&nbsp;";
if (!empty($response[$answerid])) {
quiz_print_comment($feedback[$answerid]);
}
echo "</TD>";
}
echo "</TR>";
}
echo "</TABLE>";
echo "</TABLE>";
break;
case RANDOM:
echo "<P>Random questions not supported yet</P>";
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!");
}
echo text_to_html($question->questiontext);
if ($question->image) {
print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
}
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$question->id"."r$subquestion->id");
} else {
if (empty($response[$key])) {
echo "<td align=right valign=top>";
choose_from_menu($answers, "q$question->id"."r$subquestion->id");
} else {
if ($response[$key] == $correct[$key]) {
echo "<td align=right valign=top class=highlight>";
choose_from_menu($answers, "q$question->id"."r$subquestion->id", $response[$key]);
} else {
echo "<td align=right valign=top>";
choose_from_menu($answers, "q$question->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 text_to_html($question->questiontext);
if ($question->image) {
print_file_picture($question->image, $courseid, QUIZ_PICTURE_DEFAULT_HEIGHT);
}
/// 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;
}
}
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$question->id"."r$randomquestion->id");
} else {
if (!empty($correct[$key])) {
if ($randomanswers[$responseanswer[$key]] == $correct[$key]) {
echo "<span=highlight>";
choose_from_menu($randomanswers, "q$question->id"."r$randomquestion->id", $responseanswer[$key]);
echo "</span><br \>";
} else {
choose_from_menu($randomanswers, "q$question->id"."r$randomquestion->id", $responseanswer[$key]);
quiz_print_correctanswer($correct[$key]);
}
} else {
choose_from_menu($randomanswers, "q$question->id"."r$randomquestion->id", $responseanswer[$key]);
}
if (!empty($feedback[$key])) {
quiz_print_comment($feedback[$key]);
}
}
echo "</td></tr>";
}
echo "</table>";
break;
default:
notify("Error: Unknown question type!");
}
echo "</TD></TR></TABLE>";
}
function quiz_print_quiz_questions($quiz, $results=NULL) {
// Prints a whole quiz on one page.
if (empty($quiz->questions)) {
notify("No questions have been defined!", "view.php?id=$cm->id");
return false;
}
$questions = explode(",", $quiz->questions);
if (!$grades = get_records_list("quiz_question_grades", "question", $quiz->questions, "", "question,grade")) {
notify("No grades were found for these questions!");
return false;
}
$strconfirmattempt = addslashes(get_string("readytosend", "quiz"));
echo "<FORM METHOD=POST ACTION=attempt.php onsubmit=\"return confirm('$strconfirmattempt');\">";
echo "<INPUT TYPE=hidden NAME=q VALUE=\"$quiz->id\">";
foreach ($questions as $key => $questionid) {
$feedback = NULL;
$response = NULL;
$actualgrades = NULL;
$correct = NULL;
if (!empty($results)) {
if (!empty($results->feedback[$questionid])) {
$feedback = $results->feedback[$questionid];
}
if (!empty($results->response[$questionid])) {
$response = $results->response[$questionid];
}
if (!empty($results->grades[$questionid])) {
$actualgrades = $results->grades[$questionid];
}
if ($quiz->correctanswers) {
if (!empty($results->correct[$questionid])) {
$correct = $results->correct[$questionid];
}
}
}
print_simple_box_start("CENTER", "90%");
quiz_print_question($key+1, $questionid, $grades[$questionid]->grade, $quiz->course,
$feedback, $response, $actualgrades, $correct);
print_simple_box_end();
echo "<BR>";
}
if (empty($results)) {
echo "<CENTER><INPUT TYPE=submit VALUE=\"".get_string("savemyanswers", "quiz")."\"></CENTER>";
}
echo "</FORM>";
return true;
}
function quiz_get_default_category($courseid) {
if ($categories = get_records("quiz_categories", "course", $courseid, "id")) {
foreach ($categories as $category) {
return $category; // Return the first one (lowest id)
}
}
// Otherwise, we need to make one
$category->name = get_string("default", "quiz");
$category->info = get_string("defaultinfo", "quiz");
$category->course = $courseid;
$category->publish = 0;
if (!$category->id = insert_record("quiz_categories", $category)) {
notify("Error creating a default category!");
return false;
}
return $category;
}
function quiz_get_category_menu($courseid, $published=false) {
if ($published) {
$publish = "OR publish = '1'";
}
return get_records_select_menu("quiz_categories", "course='$courseid' $publish", "name ASC", "id,name");
}
function quiz_print_category_form($course, $current) {
// Prints a form to choose categories
if (!$categories = get_records_select("quiz_categories", "course='$course->id' OR publish = '1'", "name ASC")) {
if (!$category = quiz_get_default_category($course->id)) {
notify("Error creating a default category!");
return false;
}
$categories[$category->id] = $category;
}
foreach ($categories as $key => $category) {
if ($category->publish) {
if ($catcourse = get_record("course", "id", $category->course)) {
$category->name .= " ($catcourse->shortname)";
}
}
$catmenu[$category->id] = $category->name;
}
$strcategory = get_string("category", "quiz");
$strshow = get_string("show", "quiz");
$streditcats = get_string("editcategories", "quiz");
echo "<TABLE width=\"100%\"><TR><TD NOWRAP>";
echo "<FORM METHOD=POST ACTION=edit.php>";
echo "<B>$strcategory:</B>&nbsp;";
choose_from_menu($catmenu, "cat", "$current");
echo "<INPUT TYPE=submit VALUE=\"$strshow\">";
echo "</FORM>";
echo "</TD><TD align=right>";
echo "<FORM METHOD=GET ACTION=category.php>";
echo "<INPUT TYPE=hidden NAME=id VALUE=\"$course->id\">";
echo "<INPUT TYPE=submit VALUE=\"$streditcats\">";
echo "</FORM>";
echo "</TD></TR></TABLE>";
}
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
$questions = quiz_get_question_grades($quizid, $questionlist);
$list = explode(",", $questionlist);
$grades = array();
foreach ($list as $qid) {
if (isset($questions[$qid])) {
$grades[$qid] = $questions[$qid]->grade;
} else {
$grades[$qid] = 1;
}
}
return $grades;
}
function quiz_print_question_list($questionlist, $grades) {
// Prints a list of quiz questions in a small layout form with knobs
// $questionlist is comma-separated list
// $grades is an array of corresponding grades
global $THEME;
if (!$questionlist) {
echo "<P align=center>";
print_string("noquestions", "quiz");
echo "</P>";
return;
}
$order = explode(",", $questionlist);
if (!$questions = get_records_list("quiz_questions", "id", $questionlist)) {
error("No questions were found!");
}
$strorder = get_string("order");
$strquestionname = get_string("questionname", "quiz");
$strgrade = get_string("grade");
$strdelete = get_string("delete");
$stredit = get_string("edit");
$strmoveup = get_string("moveup");
$strmovedown = get_string("movedown");
$strsavegrades = get_string("savegrades", "quiz");
$strtype = get_string("type", "quiz");
for ($i=10; $i>=0; $i--) {
$gradesmenu[$i] = $i;
}
$count = 0;
$sumgrade = 0;
$total = count($order);
echo "<FORM METHOD=post ACTION=edit.php>";
echo "<TABLE BORDER=0 CELLPADDING=5 CELLSPACING=2 WIDTH=\"100%\">";
echo "<TR><TH WIDTH=\"*\" COLSPAN=3 NOWRAP>$strorder</TH><TH align=left WIDTH=\"100%\" NOWRAP>$strquestionname</TH><TH width=\"*\" NOWRAP>$strtype</TH><TH WIDTH=\"*\" NOWRAP>$strgrade</TH><TH WIDTH=\"*\" NOWRAP>$stredit</TH></TR>";
foreach ($order as $qnum) {
if (empty($questions[$qnum])) {
continue;
}
$count++;
echo "<TR BGCOLOR=\"$THEME->cellcontent\">";
echo "<TD>$count</TD>";
echo "<TD>";
if ($count != 1) {
echo "<A TITLE=\"$strmoveup\" HREF=\"edit.php?up=$qnum\"><IMG
SRC=\"../../pix/t/up.gif\" BORDER=0></A>";
}
echo "</TD>";
echo "<TD>";
if ($count != $total) {
echo "<A TITLE=\"$strmovedown\" HREF=\"edit.php?down=$qnum\"><IMG
SRC=\"../../pix/t/down.gif\" BORDER=0></A>";
}
echo "</TD>";
echo "<TD>".$questions[$qnum]->name."</TD>";
echo "<TD ALIGN=CENTER>";
quiz_print_question_icon($questions[$qnum]);
echo "</TD>";
echo "<TD>";
choose_from_menu($gradesmenu, "q$qnum", (string)$grades[$qnum], "");
echo "<TD>";
echo "<A TITLE=\"$strdelete\" HREF=\"edit.php?delete=$qnum\"><IMG
SRC=\"../../pix/t/delete.gif\" BORDER=0></A>&nbsp;";
echo "<A TITLE=\"$stredit\" HREF=\"question.php?id=$qnum\"><IMG
SRC=\"../../pix/t/edit.gif\" BORDER=0></A>";
echo "</TD>";
$sumgrade += $grades[$qnum];
}
echo "<TR><TD COLSPAN=5 ALIGN=right>";
echo "<INPUT TYPE=submit VALUE=\"$strsavegrades:\">";
echo "<INPUT TYPE=hidden NAME=setgrades VALUE=\"save\">";
echo "<TD ALIGN=LEFT BGCOLOR=\"$THEME->cellcontent\">";
echo "<B>$sumgrade</B>";
echo "</TD><TD></TD></TR>";
echo "</TABLE>";
echo "</FORM>";
return $sumgrade;
}
function quiz_print_cat_question_list($categoryid) {
// Prints a form to choose categories
global $THEME, $QUIZ_QUESTION_TYPE;
$strcategory = get_string("category", "quiz");
$strquestion = get_string("question", "quiz");
$straddquestions = get_string("addquestions", "quiz");
$strimportquestions = get_string("importquestions", "quiz");
$strnoquestions = get_string("noquestions", "quiz");
$strselect = get_string("select", "quiz");
$strselectall = get_string("selectall", "quiz");
$strcreatenewquestion = get_string("createnewquestion", "quiz");
$strquestionname = get_string("questionname", "quiz");
$strdelete = get_string("delete");
$stredit = get_string("edit");
$straddselectedtoquiz = get_string("addselectedtoquiz", "quiz");
$strtype = get_string("type", "quiz");
if (!$categoryid) {
echo "<P align=center>";
print_string("selectcategoryabove", "quiz");
echo "</P>";
return;
}
if (!$category = get_record("quiz_categories", "id", "$categoryid")) {
notify("Category not found!");
return;
}
echo "<CENTER>";
echo text_to_html($category->info);
echo "<TABLE><TR>";
echo "<TD valign=top><B>$straddquestions:</B></TD>";
echo "<TD valign=top align=right>";
echo "<FORM METHOD=GET ACTION=question.php>";
choose_from_menu($QUIZ_QUESTION_TYPE, "qtype", "", "");
echo "<INPUT TYPE=hidden NAME=category VALUE=\"$category->id\">";
echo "<INPUT TYPE=submit VALUE=\"$strcreatenewquestion\">";
helpbutton("questiontypes", $strcreatenewquestion, "quiz");
echo "</FORM>";
echo "<FORM METHOD=GET ACTION=import.php>";
echo "<INPUT TYPE=hidden NAME=category VALUE=\"$category->id\">";
echo "<INPUT TYPE=submit VALUE=\"$strimportquestions\">";
helpbutton("import", $strimportquestions, "quiz");
echo "</FORM>";
echo "</TR></TABLE>";
echo "</CENTER>";
if (!$questions = get_records("quiz_questions", "category", $category->id, "qtype ASC")) {
echo "<P align=center>";
print_string("noquestions", "quiz");
echo "</P>";
return;
}
$canedit = isteacher($category->course);
echo "<FORM METHOD=post ACTION=edit.php>";
echo "<TABLE BORDER=0 CELLPADDING=5 CELLSPACING=2 WIDTH=\"100%\">";
echo "<TR><TH width=\"*\" NOWRAP>$strselect</TH><TH width=\"100%\" align=left NOWRAP>$strquestionname</TH><TH WIDTH=\"*\" NOWRAP>$strtype</TH>";
if ($canedit) {
echo "<TH width=\"*\" NOWRAP>$stredit</TH>";
}
echo "</TR>";
foreach ($questions as $question) {
echo "<TR BGCOLOR=\"$THEME->cellcontent\">";
echo "<TD ALIGN=CENTER>";
echo "<INPUT TYPE=CHECKBOX NAME=q$question->id VALUE=\"1\">";
echo "</TD>";
echo "<TD>".$question->name."</TD>";
echo "<TD ALIGN=CENTER>";
quiz_print_question_icon($question);
echo "</TD>";
if ($canedit) {
echo "<TD>";
echo "<A TITLE=\"$strdelete\" HREF=\"question.php?id=$question->id&delete=$question->id\"><IMG
SRC=\"../../pix/t/delete.gif\" BORDER=0></A>&nbsp;";
echo "<A TITLE=\"$stredit\" HREF=\"question.php?id=$question->id\"><IMG
SRC=\"../../pix/t/edit.gif\" BORDER=0></A>";
echo "</TD></TR>";
}
echo "</TR>";
}
echo "<TR><TD COLSPAN=3>";
echo "<INPUT TYPE=submit NAME=add VALUE=\"<< $straddselectedtoquiz\">";
//echo "<INPUT TYPE=submit NAME=delete VALUE=\"XX Delete selected\">";
echo "<INPUT type=button onclick=\"checkall()\" value=\"$strselectall\">";
echo "</TD></TR>";
echo "</TABLE>";
echo "</FORM>";
}
function quiz_start_attempt($quizid, $userid, $numattempt) {
$attempt->quiz = $quizid;
$attempt->userid = $userid;
$attempt->attempt = $numattempt;
$attempt->timestart = time();
$attempt->timefinish = 0;
$attempt->timemodified = time();
return insert_record("quiz_attempts", $attempt);
}
function quiz_get_user_attempt_unfinished($quizid, $userid) {
// Returns an object containing an unfinished attempt (if there is one)
return get_record("quiz_attempts", "quiz", $quizid, "userid", $userid, "timefinish", 0);
}
function quiz_get_user_attempts($quizid, $userid) {
// Returns a list of all attempts by a user
return get_records_select("quiz_attempts", "quiz = '$quizid' AND userid = '$userid' AND timefinish > 0",
"attempt ASC");
}
function quiz_get_user_attempts_string($quiz, $attempts, $bestgrade) {
/// Returns a simple little comma-separated list of all attempts,
/// with each grade linked to the feedback report and with the best grade highlighted
$bestgrade = format_float($bestgrade);
foreach ($attempts as $attempt) {
$attemptgrade = format_float(($attempt->sumgrades / $quiz->sumgrades) * $quiz->grade);
if ($attemptgrade == $bestgrade) {
$userattempts[] = "<SPAN CLASS=highlight><A HREF=\"report.php?q=$quiz->id&attempt=$attempt->id\">$attemptgrade</A></SPAN>";
} else {
$userattempts[] = "<A HREF=\"report.php?q=$quiz->id&attempt=$attempt->id\">$attemptgrade</A>";
}
}
return implode(",", $userattempts);
}
function quiz_get_best_grade($quizid, $userid) {
/// Get the best current grade for a particular user in a quiz
if (!$grade = get_record("quiz_grades", "quiz", $quizid, "userid", $userid)) {
return 0;
}
return (round($grade->grade,0));
}
function quiz_save_best_grade($quiz, $userid) {
/// Calculates the best grade out of all attempts at a quiz for a user,
/// and then saves that grade in the quiz_grades table.
if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
notify("Could not find any user attempts");
return false;
}
$bestgrade = quiz_calculate_best_grade($quiz, $attempts);
$bestgrade = (($bestgrade / $quiz->sumgrades) * $quiz->grade);
if ($grade = get_record("quiz_grades", "quiz", $quiz->id, "userid", $userid)) {
$grade->grade = round($bestgrade, 2);
$grade->timemodified = time();
if (!update_record("quiz_grades", $grade)) {
notify("Could not update best grade");
return false;
}
} else {
$grade->quiz = $quiz->id;
$grade->userid = $userid;
$grade->grade = round($bestgrade, 2);
$grade->timemodified = time();
if (!insert_record("quiz_grades", $grade)) {
notify("Could not insert new best grade");
return false;
}
}
return true;
}
function quiz_calculate_best_grade($quiz, $attempts) {
/// Calculate the best grade for a quiz given a number of attempts by a particular user.
switch ($quiz->grademethod) {
case ATTEMPTFIRST:
foreach ($attempts as $attempt) {
return $attempt->sumgrades;
}
break;
case ATTEMPTLAST:
foreach ($attempts as $attempt) {
$final = $attempt->sumgrades;
}
return $final;
case GRADEAVERAGE:
$sum = 0;
$count = 0;
foreach ($attempts as $attempt) {
$sum += $attempt->sumgrades;
$count++;
}
return (float)$sum/$count;
default:
case GRADEHIGHEST:
$max = 0;
foreach ($attempts as $attempt) {
if ($attempt->sumgrades > $max) {
$max = $attempt->sumgrades;
}
}
return $max;
}
}
function quiz_save_attempt($quiz, $questions, $result, $attemptnum) {
/// Given a quiz, a list of attempted questions and a total grade
/// this function saves EVERYTHING so it can be reconstructed later
/// if necessary.
global $USER;
// First find the attempt in the database (start of attempt)
if (!$attempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
notify("Trying to save an attempt that was not started!");
return false;
}
if ($attempt->attempt != $attemptnum) { // Double check.
notify("Number of this attempt is different to the unfinished one!");
return false;
}
// Now let's complete this record and save it
$attempt->sumgrades = $result->sumgrades;
$attempt->timefinish = time();
$attempt->timemodified = time();
if (! update_record("quiz_attempts", $attempt)) {
notify("Error while saving attempt");
return false;
}
// Now let's save all the questions for this attempt
foreach ($questions as $question) {
$response->attempt = $attempt->id;
$response->question = $question->id;
$response->grade = $result->grades[$question->id];
if (!empty($question->answer)) {
$response->answer = implode(",",$question->answer);
} else {
$response->answer = "";
}
if (!insert_record("quiz_responses", $response)) {
notify("Error while saving response");
return false;
}
}
return true;
}
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)
if (!$questions) {
error("No questions!");
}
$result->sumgrades = 0;
foreach ($questions as $question) {
if (!$answers = quiz_get_answers($question)) {
error("No answers defined for question id $question->id!");
}
$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;
$bestshortanswer = 0;
foreach($answers as $answer) { // There might be multiple right answers
if ($answer->fraction > $bestshortanswer) {
$correct[$answer->id] = $answer->answer;
$bestshortanswer = $answer->fraction;
}
if (!$answer->usecase) { // Don't compare case
$answer->answer = strtolower($answer->answer);
$question->answer = strtolower($question->answer);
}
if ($question->answer == $answer->answer) {
$feedback[0] = $answer->feedback;
$grade = (float)$answer->fraction * $answer->grade;
}
}
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 * $answer->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 * $answer->grade;
continue;
} else {
$grade += (float)$answer->fraction * $answer->grade;
}
}
}
}
}
break;
case MATCH:
$matchcount = $totalcount = 0;
foreach ($question->answer as $questionanswer) { // Each answer is "questionid-answerid"
$totalcount++;
$qarr = explode('-', $questionanswer); // Extract question/answer.
if ($qarr[0] == $qarr[1]) {
$matchcount++;
$correct[$qarr[0]] = true;
$response[$qarr[0]] = $qarr[1];
$questiongrade = $answers[$qarr[0]]->grade;
} else {
$correct[$qarr[0]] = false;
$response[$qarr[0]] = $qarr[1];
}
}
$grade = $questiongrade * $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 * $answer->grade * $answerfraction;
}
}
}
break;
}
if ($grade < 0.0) { // No negative grades
$grade = 0.0;
}
$result->grades[$question->id] = round($grade, 2);
$result->sumgrades += $grade;
$result->feedback[$question->id] = $feedback;
$result->response[$question->id] = $response;
$result->correct[$question->id] = $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;
}
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
///
/// Returns $result->error or $result->notice
switch ($question->qtype) {
case SHORTANSWER:
// Delete all the old answers
// FIXME - instead of deleting, update existing answers
// so as not to break existing references to them
delete_records("quiz_answers", "question", $question->id);
delete_records("quiz_shortanswer", "question", $question->id);
$answers = array();
$maxfraction = -1;
// Insert all the new answers
foreach ($question->answer as $key => $dataanswer) {
if ($dataanswer != "") {
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];
}
}
}
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->notice = get_string("fractionsnomax", "quiz", $maxfraction);
return $result;
}
break;
case TRUEFALSE:
// FIXME - instead of deleting, update existing answers
// so as not to break existing references to them
delete_records("quiz_answers", "question", $question->id);
delete_records("quiz_truefalse", "question", $question->id);
$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;
}
$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;
}
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:
// FIXME - instead of deleting, update existing answers
// so as not to break existing references to them
delete_records("quiz_answers", "question", $question->id);
delete_records("quiz_multichoice", "question", $question->id);
$totalfraction = 0;
$maxfraction = -1;
$answers = array();
// Insert all the new answers
foreach ($question->answer as $key => $dataanswer) {
if ($dataanswer != "") {
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];
}
}
}
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->notice = get_string("fractionsnomax", "quiz", $maxfraction);
return $result;
}
} else {
$totalfraction = round($totalfraction,2);
if ($totalfraction != 1) {
$totalfraction = $totalfraction * 100;
$result->notice = get_string("fractionsaddwrong", "quiz", $totalfraction);
return $result;
}
}
break;
case MATCH:
delete_records("quiz_match", "question", $question->id);
delete_records("quiz_match_sub", "question", $question->id);
$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)) {
unset($answer);
$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 answer!";
return $result;
}
$subquestions[] = $subquestion->id;
}
}
if (count($subquestions) < 3) {
$result->notice = get_string("notenoughsubquestions", "quiz");
return $result;
}
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;
default:
$result->error = "Unsupported question type ($question->qtype)!";
return $result;
break;
}
return true;
}
?>