get_string("gradehighest", "quiz"), GRADEAVERAGE => get_string("gradeaverage", "quiz"), ATTEMPTFIRST => get_string("attemptfirst", "quiz"), ATTEMPTLAST => get_string("attemptlast", "quiz")); define("SHORTANSWER", "1"); define("TRUEFALSE", "2"); define("MULTICHOICE", "3"); define("RANDOM", "4"); define("MATCH", "5"); define("RANDOMSAMATCH", "6"); define("DESCRIPTION", "7"); define("NUMERICAL", "8"); define("MULTIANSWER", "9"); $QUIZ_QUESTION_TYPE = array ( MULTICHOICE => get_string("multichoice", "quiz"), TRUEFALSE => get_string("truefalse", "quiz"), SHORTANSWER => get_string("shortanswer", "quiz"), NUMERICAL => get_string("numerical", "quiz"), MATCH => get_string("match", "quiz"), DESCRIPTION => get_string("description", "quiz"), RANDOM => get_string("random", "quiz"), RANDOMSAMATCH => get_string("randomsamatch", "quiz"), MULTIANSWER => get_string("multianswer", "quiz") ); define("QUIZ_PICTURE_MAX_HEIGHT", "600"); // Not currently implemented define("QUIZ_PICTURE_MAX_WIDTH", "600"); // Not currently implemented define("QUIZ_MAX_NUMBER_ANSWERS", "10"); define("QUIZ_MAX_EVENT_LENGTH", "432000"); // 5 days maximum $QUIZ_QTYPES= array(); /// QUIZ_QTYPES INITIATION ////////////////// class quiz_default_questiontype { function name() { return 'default'; } function save_question_options($question) { /// Given some question info and some data about the the answers /// this function parses, organises and saves the question /// It is used by question.php through ->save_question when /// saving new data from a form, and also by import.php when /// importing questions /// /// If this is an update, and old answers already exist, then /// these are overwritten using an update(). To do this, it /// it is assumed that the IDs in quiz_answers are in the same /// sort order as the new answers being saved. This should always /// be true, but it's something to keep in mind if fiddling with /// question.php /// /// Returns $result->error or $result->noticeyesno or $result->notice /// This default implementation must be overridden: $result->error = "Unsupported question type ($question->qtype)!"; return $result; } function save_question($question, $form, $course) { // As this function uses formcheck, it can only be used by // question.php // This default implementation is suitable for most // question types. // First, save the basic question itself $question->name = $form->name; $question->questiontext = $form->questiontext; $question->questiontextformat = $form->questiontextformat; if (empty($form->image)) { $question->image = ""; } else { $question->image = $form->image; } if (isset($form->defaultgrade)) { $question->defaultgrade = $form->defaultgrade; } if ($err = formcheck($question)) { notify(get_string("someerrorswerefound")); } else { if (!empty($question->id)) { // Question already exists $question->version ++; // Update version number of question if (!update_record("quiz_questions", $question)) { error("Could not update question!"); } } else { // Question is a new one $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) $question->version = 1; if (!$question->id = insert_record("quiz_questions", $question)) { error("Could not insert new question!"); } } // Now to save all the answers and type-specific options $form->id = $question->id; $form->qtype = $question->qtype; $form->category = $question->category; $result = $this->save_question_options($form); if (!empty($result->error)) { error($result->error); } if (!empty($result->notice)) { notice($result->notice, "question.php?id=$question->id"); } if (!empty($result->noticeyesno)) { notice_yesno($result->noticeyesno, "question.php?id=$question->id", "edit.php"); print_footer($course); exit; } redirect("edit.php"); } } /// Convenience function that is used within the question types only function extract_response_id($responsekey) { if (ereg('[0-9]'.$this->name().'([0-9]+)', $responsekey, $regs)) { return $regs[1]; } else { return false; } } function wrapped_questions($question) { /// Overridden only by question types, whose questions can /// wrap other questions. Two question types that do this /// are RANDOMSAMATCH and RANDOM /// If there are wrapped questions, than this method returns /// comma separated list of them... return false; } function convert_to_response_answer_field($questionresponse) { /// This function is very much the inverse of extract_response /// This function and extract_response, should be /// obsolete as soon as we get a better response storage /// Right now they are a bridge between a consistent /// response model and the old field answer in quiz_responses /// This is the default implemention... return implode(',', $questionresponse); } function get_answers($question) { // Returns the answers for the specified question // The default behaviour that signals that something is wrong return false; } function create_response($question, $nameprefix, $questionsinuse) { /// This rather smart solution works for most cases: $rawresponse->question = $question->id; $rawresponse->answer = ''; return $this->extract_response($rawresponse, $nameprefix); } function extract_response($rawresponse, $nameprefix) { /// This function is very mcuh the inverse of convert_to_response_answer /// This function and convert_to_response_answer, should be /// obsolete as soon as we get a better response storage /// Right now they are a bridge between a consistent /// response model and the old field answer in quiz_responses /// Default behaviour that works for singlton response question types /// like SHORTANSWER, NUMERICAL and TRUEFALSE return array($nameprefix => $rawresponse->answer); } function print_question_number_and_grading_details ($number, $grade, $actualgrade=false, $recentlyadded=false) { /// Print question number and grade: echo '

' . $number . '

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

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

"; } else { echo "$grade $strmarks

"; } } print_spacer(1,100); /// Print possible recently-added information: if ($recentlyadded) { echo ''; // Notify the user of this recently added question echo ''; echo get_string('recentlyaddedquestion', 'quiz'); echo ''; echo ''; } else { // The normal case echo ''; } } function print_question($currentnumber, $quiz, $question, $readonly, $resultdetails) { /// Note that this method must return the number of the next /// question, making it possible not to increase the number when /// overriding this method (as for qtype=DESCRIPTION). echo ''; echo '
'; $this->print_question_number_and_grading_details ($currentnumber, $quiz->grade ? $question->maxgrade : false, empty($resultdetails) ? false : $resultdetails->grade, $question->recentlyadded); $this->print_question_formulation_and_controls( $question, $quiz, $readonly, $resultdetails->answers, $resultdetails->correctanswers, quiz_qtype_nameprefix($question)); echo "
"; return $currentnumber + 1; } function print_question_formulation_and_controls($question, $quiz, $readonly, $answers, $correctanswers, $nameprefix) { /// This default implementation must be overridden by all /// question type implemenations, unless the default /// implementation of print_question has been overridden... notify('Error: Question formulation and input controls has not' .' been implemented for question type '.$this->name()); } function grade_response($question, $nameprefix) { // Analyzes $question->response[] and determines the result // The result is to be returned in this structure: // ->grade (The fraction of maxgrade awarded on the question) // ->answers (result answer records) // ->correctanswers (potential answer records for best ->response[]) error('grade_response has not been implemented for question type ' .$this->name()); } } quiz_load_questiontypes(); function quiz_load_questiontypes() { global $QUIZ_QTYPES; global $CFG; $qtypenames= get_list_of_plugins('mod/quiz/questiontypes'); foreach($qtypenames as $qtypename) { // Instanciates all plug-in question types $qtypefilepath= "questiontypes/$qtypename/questiontype.php"; // echo "Loading $qtypename
"; // Uncomment for debugging if (is_readable($qtypefilepath)) { require_once($qtypefilepath); } } } /// FUNCTIONS /////////////////////////////////////////////////////////////////// function quiz_add_instance($quiz) { /// 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. global $SESSION; unset($SESSION->modform); $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) { unset($questiongrade); $questiongrade->quiz = $quiz->id; $questiongrade->question = $question; $questiongrade->grade = $grade; if (!insert_record("quiz_question_grades", $questiongrade)) { return false; } } } } delete_records('event', 'modulename', 'quiz', 'instance', $quiz->id); // Just in case $event = NULL; $event->name = $quiz->name; $event->description = $quiz->intro; $event->courseid = $quiz->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'quiz'; $event->instance = $quiz->id; $event->eventtype = 'open'; $event->timestart = $quiz->timeopen; $event->visible = instance_is_visible('quiz', $quiz->id); $event->timeduration = ($quiz->timeclose - $quiz->timeopen); if ($event->timeduration > QUIZ_MAX_EVENT_LENGTH) { /// Long durations create two events $event2 = $event; $event->name .= ' ('.get_string('quizopens', 'quiz').')'; $event->timeduration = 0; $event2->timestart = $quiz->timeclose; $event2->eventtype = 'close'; $event2->timeduration = 0; $event2->name .= ' ('.get_string('quizcloses', 'quiz').')'; add_event($event2); } add_event($event); 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. global $SESSION; unset($SESSION->modform); $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) { 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; } } } } } delete_records('event', 'modulename', 'quiz', 'instance', $quiz->id); // Delete old and add new $event = NULL; $event->name = $quiz->name; $event->description = $quiz->intro; $event->courseid = $quiz->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'quiz'; $event->instance = $quiz->id; $event->eventtype = 'open'; $event->timestart = $quiz->timeopen; $event->visible = instance_is_visible('quiz', $quiz->id); $event->timeduration = ($quiz->timeclose - $quiz->timeopen); if ($event->timeduration > QUIZ_MAX_EVENT_LENGTH) { /// Long durations create two events $event2 = $event; $event->name .= ' ('.get_string('quizopens', 'quiz').')'; $event->timeduration = 0; $event2->timestart = $quiz->timeclose; $event2->eventtype = 'close'; $event2->timeduration = 0; $event2->name .= ' ('.get_string('quizcloses', 'quiz').')'; add_event($event2); } add_event($event); 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_records("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; } if (! delete_records('event', 'modulename', 'quiz', 'instance', $quiz->id)) { $result = false; } return $result; } function quiz_delete_course($course) { /// Given a course object, this function will clean up anything that /// would be leftover after all the instances were deleted /// In this case, all non-publish quiz categories and questions if ($categories = get_records_select("quiz_categories", "course = '$course->id' AND publish = '0'")) { foreach ($categories as $category) { if ($questions = get_records("quiz_questions", "category", $category->id)) { foreach ($questions as $question) { delete_records("quiz_answers", "question", $question->id); delete_records("quiz_match", "question", $question->id); delete_records("quiz_match_sub", "question", $question->id); delete_records("quiz_multianswers", "question", $question->id); delete_records("quiz_multichoice", "question", $question->id); delete_records("quiz_numerical", "question", $question->id); delete_records("quiz_randommatch", "question", $question->id); delete_records("quiz_responses", "question", $question->id); delete_records("quiz_shortanswer", "question", $question->id); delete_records("quiz_truefalse", "question", $question->id); } delete_records("quiz_questions", "category", $category->id); } } return delete_records("quiz_categories", "course", $course->id); } return true; } 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_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. $quiz = get_record("quiz", "id", $quizid); if (empty($quiz) or empty($quiz->grade)) { return NULL; } $return->grades = get_records_menu("quiz_grades", "quiz", $quizid, "", "userid,grade"); $return->maxgrade = get_field("quiz", "grade", "id", "$quizid"); return $return; } function quiz_get_participants($quizid) { /// Returns an array of users who have data in a given quiz /// (users with records in quiz_attempts, students) global $CFG; return get_records_sql("SELECT DISTINCT u.* FROM {$CFG->prefix}user u, {$CFG->prefix}quiz_attempts a WHERE a.quiz = '$quizid' and u.id = a.userid"); } function quiz_refresh_events($courseid = 0) { // This standard function will check all instances of this module // and make sure there are up-to-date events created for each of them. // If courseid = 0, then every quiz event in the site is checked, else // only quiz events belonging to the course specified are checked. // This function is used, in its new format, by restore_refresh_events() if ($courseid == 0) { if (! $quizzes = get_records("quiz")) { return true; } } else { if (! $quizzes = get_records("quiz", "course", $courseid)) { return true; } } $moduleid = get_field('modules', 'id', 'name', 'quiz'); foreach ($quizzes as $quiz) { $event = NULL; $event2 = NULL; $event2old = NULL; if ($events = get_records_select('event', "modulename = 'quiz' AND instance = '$quiz->id' ORDER BY timestart")) { $event = array_shift($events); if (!empty($events)) { $event2old = array_shift($events); if (!empty($events)) { foreach ($events as $badevent) { delete_records('event', 'id', $badevent->id); } } } } $event->name = addslashes($quiz->name); $event->description = addslashes($quiz->intro); $event->courseid = $quiz->course; $event->groupid = 0; $event->userid = 0; $event->modulename = 'quiz'; $event->instance = $quiz->id; $event->visible = instance_is_visible('quiz', $quiz->id); $event->timestart = $quiz->timeopen; $event->eventtype = 'open'; $event->timeduration = ($quiz->timeclose - $quiz->timeopen); if ($event->timeduration > QUIZ_MAX_EVENT_LENGTH) { /// Set up two events $event2 = $event; $event->name = addslashes($quiz->name).' ('.get_string('quizopens', 'quiz').')'; $event->timeduration = 0; $event2->name = addslashes($quiz->name).' ('.get_string('quizcloses', 'quiz').')'; $event2->timestart = $quiz->timeclose; $event2->eventtype = 'close'; $event2->timeduration = 0; if (empty($event2old->id)) { unset($event2->id); add_event($event2); } else { $event2->id = $event2old->id; update_event($event2); } } else if (!empty($event2->id)) { delete_event($event2->id); } if (empty($event->id)) { add_event($event); } else { update_event($event); } } return true; } /// 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 for a given question global $QUIZ_QTYPES; return $QUIZ_QTYPES[$question->qtype]->get_answers($question); } 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; /// 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: /// This line can be uncommented for debuging // echo "Last attempt is $lastattempt->id with number $lastattemptnum"; } } /// Set the additional question properties /// response, recentlyadded and grade foreach ($questions as $qid => $question) { if (isset($grades[$qid])) { $questions[$qid]->maxgrade = $grades[$qid]->grade; } else { $questions[$qid]->maxgrade = 0.0; } if (isset($rawresponses[$qid])) { $questions[$qid]->response = $QUIZ_QTYPES[$question->qtype] ->extract_response($rawresponses[$qid], quiz_qtype_nameprefix($question)); $questions[$qid]->recentlyadded = false; } else { $questions[$qid]->response = array(); $questions[$qid]->recentlyadded = !empty($rawresponses); } } 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 $questions; } function get_list_of_questions($questionlist) { /// Returns an ordered list of questions, including course for each global $CFG; return get_records_sql("SELECT q.*,c.course FROM {$CFG->prefix}quiz_questions q, {$CFG->prefix}quiz_categories c WHERE q.id in ($questionlist) AND q.category = c.id"); } ////////////////////////////////////////////////////////////////////////////////////// /// 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; echo "".format_text($text, true, false).""; } function quiz_print_correctanswer($text) { global $THEME; echo "

$text

"; } function quiz_print_question_icon($question, $editlink=true) { // Prints a question icon global $QUIZ_QUESTION_TYPE; global $QUIZ_QTYPES; if ($editlink) { echo "id\" title=\"" .$QUIZ_QUESTION_TYPE[$question->qtype]."\">"; } echo ''; if ($editlink) { echo "\n"; } } function quiz_print_possible_question_image($quizid, $question) { // Includes the question image if there is one global $CFG; if ($question->image) { echo 'wwwroot/mod/quiz/quizfile.php/$quizid/$question->id/$question->image"; } else { echo "$CFG->wwwroot/mod/quiz/quizfile.php?file=/$quizid/$question->id/$question->image"; } echo '" />'; } } function quiz_print_quiz_questions($quiz, $questions, $results=NULL, $shuffleorder=NULL) { // Prints a whole quiz on one page. global $QUIZ_QTYPES; /// Check arguments if (empty($questions)) { notify("No questions have been defined!"); return false; } if (!$shuffleorder) { if (!empty($quiz->shufflequestions)) { // Mix everything up $questions = swapshuffle_assoc($questions); } else { $shuffleorder = explode(",", $quiz->questions); // Use originally defined order } } if ($shuffleorder) { // Order has been defined, so reorder questions $oldquestions = $questions; $questions = array(); foreach ($shuffleorder as $key) { $questions[] = $oldquestions[$key]; // This loses the index key, but doesn't matter } } $strconfirmattempt = addslashes(get_string("readytosend", "quiz")); if (empty($quiz->grade)) { $onsubmit = ""; } else { $onsubmit = "onsubmit=\"return confirm('$strconfirmattempt');\""; } // BEGIN EDIT if($quiz->timelimit > 0) { ?> \n"; } // END EDIT echo "id\" />\n"; // $count = 0; $nextquestionnumber = 1; $questionorder = array(); // $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" ' : ''; foreach ($questions as $question) { $questionorder[] = $question->id; if ($results && isset($results->details[$question->id])) { $details = $results->details[$question->id]; } else { $details = false; } print_simple_box_start("center", "90%"); $nextquestionnumber = $QUIZ_QTYPES[$question->qtype]->print_question ($nextquestionnumber, $quiz, $question, $readonly, $details); print_simple_box_end(); echo "
"; } if (empty($readonly)) { if (!empty($quiz->shufflequestions)) { // Things have been mixed up, so pass the question order $shuffleorder = implode(',', $questionorder); echo "\n"; } if($quiz->timelimit > 0) { echo "\n"; echo "\n"; } else { echo "
\n\n
"; } } echo ""; return true; } function quiz_get_default_category($courseid) { /// Returns the current category 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; $category->stamp = make_unique_id_code(); 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) { /// Returns the list of categories $publish = ""; 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 ($catcourse = get_record("course", "id", $category->course)) { if ($category->publish) { $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 "
"; echo "$strcategory: "; echo ""; popup_form ("edit.php?cat=", $catmenu, "catmenu", $current, "choose", "", "", false, "self"); echo ""; echo "
"; echo "id\" />"; echo ""; echo "
"; echo "
"; } function quiz_category_select_menu($courseid,$published=false,$only_editable=false,$selected="") { /// displays a select menu of categories with appended coursenames /// optionaly non editable categories may be excluded /// added Howard Miller June '04 // get sql fragment for published $publishsql=""; if ($published) { $publishsql = "or publish=1"; } $categories = get_records_select("quiz_categories","course=$courseid $publishsql"); echo "\n"; } function quiz_get_category_coursename($category) { /// if the category is published, adds on the course /// name $cname=$category->name; if ($category->publish) { if ($catcourse=get_record("course","id",$category->id)) { $cname .= " ($catcourse->shortname) "; } } return $cname; } 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 if (empty($questionlist)) { return array(); } $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_gradesmenu_options($defaultgrade) { // Especially for multianswer questions it is often // desirable to have the grade of the question in a quiz // larger than the earlier maximum of 10 points. // This function makes quiz question list grade selector drop-down // have the maximum grade option set to the highest value between 10 // and the defaultgrade of the question. if ($defaultgrade && $defaultgrade>10) { $maxgrade = $defaultgrade; } else { $maxgrade = 10; } unset($gradesmenu); for ($i=$maxgrade ; $i>=0 ; --$i) { $gradesmenu[$i] = $i; } return $gradesmenu; } 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 "

"; print_string("noquestions", "quiz"); echo "

"; return; } $order = explode(",", $questionlist); if (!$questions = get_list_of_questions($questionlist)) { echo "

"; print_string("noquestions", "quiz"); echo "

"; return; } $strorder = get_string("order"); $strquestionname = get_string("questionname", "quiz"); $strgrade = get_string("grade"); $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"); $count = 0; $sumgrade = 0; $total = count($order); echo "
"; echo "\n"; echo "\n"; foreach ($order as $qnum) { if (empty($questions[$qnum])) { continue; } $question = $questions[$qnum]; $canedit = isteacheredit($question->course); $count++; echo "cellcontent\">"; echo ""; echo ""; echo ""; echo ""; echo ""; echo ""; $sumgrade += $grades[$qnum]; } echo "\n"; echo "
$strorder$strquestionname$strtype$strgrade$stredit
$count"; if ($count != 1) { echo ""; } echo ""; if ($count != $total) { echo ""; } echo "$question->name"; quiz_print_question_icon($question, $canedit); echo ""; if ($question->qtype == DESCRIPTION) { echo " \n"; } else { choose_from_menu(quiz_gradesmenu_options($question->defaultgrade), "q$qnum", (string)$grades[$qnum], ""); } echo ""; echo " "; if ($canedit) { echo "\n"; } echo "
\n"; echo "\n"; echo "\n"; echo "cellcontent\">\n"; echo "$sumgrade"; echo "\n
\n"; echo "
\n"; return $sumgrade; } function quiz_print_cat_question_list($categoryid, $quizselected=true) { // 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"); $strexportquestions = get_string("exportquestions", "quiz"); $strnoquestions = get_string("noquestions", "quiz"); $strselect = get_string("select", "quiz"); $strselectall = get_string("selectall", "quiz"); $strcreatenewquestion = get_string("createnewquestion", "quiz"); $strquestionname = get_string("questionname", "quiz"); $strdelete = get_string("delete"); $stredit = get_string("edit"); $straddselectedtoquiz = get_string("addselectedtoquiz", "quiz"); $strtype = get_string("type", "quiz"); $strcreatemultiple = get_string("createmultiple", "quiz"); if (!$categoryid) { echo "

"; print_string("selectcategoryabove", "quiz"); echo "

"; if ($quizselected) { echo "

"; print_string("addingquestions", "quiz"); echo "

"; } return; } if (!$category = get_record("quiz_categories", "id", "$categoryid")) { notify("Category not found!"); return; } echo "
"; echo format_text($category->info, FORMAT_MOODLE); echo ''; // check if editing of this category is allowed if (isteacheredit($category->course)) { echo ""; echo ''; echo ''; } else { echo ''; } echo ''; if (isteacheredit($category->course)) { echo ''; } echo '
$strcreatenewquestion:'; popup_form ("question.php?category=$category->id&qtype=", $QUIZ_QUESTION_TYPE, "addquestion", "", "choose", "", "", false, "self"); echo ''; helpbutton("questiontypes", $strcreatenewquestion, "quiz"); echo '
'; echo '
'; echo "id\" />"; echo ""; helpbutton("import", $strimportquestions, "quiz"); echo '
'; echo '
'; print_string("publishedit","quiz"); echo '
'; echo '
'; echo "id\" />"; echo ""; helpbutton("export", $strexportquestions, "quiz"); echo '
'; echo '
'; echo '
'; echo "id\" />"; echo ""; helpbutton("createmultiple", $strcreatemultiple, "quiz"); echo '
'; echo '
'; echo '
'; if (!$questions = get_records("quiz_questions", "category", $category->id, "qtype ASC")) { echo "

"; print_string("noquestions", "quiz"); echo "

"; return; } $canedit = isteacheredit($category->course); echo "
"; echo ""; echo ""; if ($quizselected) { echo ""; } echo ""; if ($canedit) { echo ""; } echo "\n"; foreach ($questions as $question) { echo "cellcontent\">\n"; if ($quizselected) { echo ""; } echo "\n"; echo "\n"; if ($canedit) { echo "\n";// deleted jm } echo "\n"; } if ($quizselected) { echo "\n"; } echo "
$strselect$strquestionname$strtype$stredit
"; echo "id\" value=\"1\" />\n"; echo "".$question->name."\n"; quiz_print_question_icon($question, $canedit); echo "\n"; echo "id&delete=$question->id\">\n "; echo "id\">"; echo "
"; echo "\n"; //echo ""; echo "\n"; echo "
\n"; echo "
\n"; } function quiz_start_attempt($quizid, $userid, $numattempt) { $attempt->quiz = $quizid; $attempt->userid = $userid; $attempt->attempt = $numattempt; $attempt->timestart = time(); $attempt->timefinish = 0; $attempt->timemodified = time(); $attempt->id = insert_record("quiz_attempts", $attempt); return $attempt; } function quiz_get_user_attempt_unfinished($quizid, $userid) { // Returns an object containing an unfinished attempt (if there is one) return get_record("quiz_attempts", "quiz", $quizid, "userid", $userid, "timefinish", 0); } function quiz_get_user_attempts($quizid, $userid) { // Returns a list of all attempts by a user return get_records_select("quiz_attempts", "quiz = '$quizid' AND userid = '$userid' AND timefinish > 0", "attempt ASC"); } function quiz_get_user_attempts_string($quiz, $attempts, $bestgrade) { /// Returns a simple little comma-separated list of all attempts, /// with each grade linked to the feedback report and with the best grade highlighted $bestgrade = format_float($bestgrade); foreach ($attempts as $attempt) { $attemptgrade = format_float(($attempt->sumgrades / $quiz->sumgrades) * $quiz->grade); if ($attemptgrade == $bestgrade) { $userattempts[] = "id&attempt=$attempt->id\">$attemptgrade"; } else { $userattempts[] = "id&attempt=$attempt->id\">$attemptgrade"; } } 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 ""; } 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_calculate_best_attempt($quiz, $attempts) { /// Return the attempt with the best grade for a quiz switch ($quiz->grademethod) { case ATTEMPTFIRST: foreach ($attempts as $attempt) { return $attempt; } break; case GRADEAVERAGE: // need to do something with it :-) case ATTEMPTLAST: foreach ($attempts as $attempt) { $final = $attempt; } return $final; default: case GRADEHIGHEST: $max = -1; foreach ($attempts as $attempt) { if ($attempt->sumgrades > $max) { $max = $attempt->sumgrades; $maxattempt = $attempt; } } return $maxattempt; } } function quiz_save_attempt($quiz, $questions, $result, $attemptnum) { /// Given a quiz, a list of attempted questions and a total grade /// this function saves EVERYTHING so it can be reconstructed later /// if necessary. global $USER; global $QUIZ_QTYPES; // First find the attempt in the database (start of attempt) if (!$attempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) { notify("Trying to save an attempt that was not started!"); return false; } 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->grade = $result->details[$question->id]->grade; $response->question = $question->id; 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 = ""; } if (!insert_record("quiz_responses", $response)) { notify("Error while saving response"); return false; } } return $attempt; } function quiz_extract_correctanswers($answers, $nameprefix) { /// Convinience function that is used by some single-response /// question-types for determining correct answers. $bestanswerfraction = 0.0; $correctanswers = array(); foreach ($answers as $answer) { if ($answer->fraction > $bestanswerfraction) { $correctanswers = array($nameprefix.$answer->id => $answer); $bestanswerfraction = $answer->fraction; } else if ($answer->fraction == $bestanswerfraction) { $correctanswers[$nameprefix.$answer->id] = $answer; } } return $correctanswers; } function quiz_grade_responses($quiz, $questions) { /// 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!"); } $result->sumgrades = 0.0; foreach ($questions as $qid => $question) { $resultdetails = $QUIZ_QTYPES[$question->qtype]->grade_response ($question, quiz_qtype_nameprefix($question)); // Negative grades will not do: if (((float)($resultdetails->grade)) <= 0.0) { $resultdetails->grade = 0.0; // Neither will extra credit: } else if (((float)($resultdetails->grade)) >= 1.0) { $resultdetails->grade = $question->maxgrade; } else { $resultdetails->grade *= $question->maxgrade; } // if time limit is enabled and exceeded, return zero grades if ($quiz->timelimit > 0) { if (($quiz->timelimit + 60) <= $quiz->timesincestart) { $resultdetails->grade = 0; } } $result->sumgrades += $resultdetails->grade; $resultdetails->grade = round($resultdetails->grade, 2); $result->details[$qid] = $resultdetails; } $fraction = (float)($result->sumgrades / $quiz->sumgrades); $result->percentage = format_float($fraction * 100.0); $result->grade = format_float($fraction * $quiz->grade); $result->sumgrades = round($result->sumgrades, 2); return $result; } 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 global $CFG; if ($quiz) { $quizselect = " AND cm.id = '$quiz'"; } else { $quizselect = ""; } if ($user) { $userselect = " AND u.id = '$user'"; } else { $userselect = ""; } $quizzes = get_records_sql("SELECT qa.*, q.name, u.firstname, u.lastname, u.picture, q.course, q.sumgrades as maxgrade, cm.instance, cm.section FROM {$CFG->prefix}quiz_attempts qa, {$CFG->prefix}quiz q, {$CFG->prefix}user u, {$CFG->prefix}course_modules cm WHERE qa.timefinish > '$sincetime' AND qa.userid = u.id $userselect AND qa.quiz = q.id $quizselect AND cm.instance = q.id AND cm.course = '$courseid' AND q.course = cm.course ORDER BY qa.timefinish ASC"); if (empty($quizzes)) return; foreach ($quizzes as $quiz) { if (empty($groupid) || ismember($groupid, $quiz->userid)) { $tmpactivity->type = "quiz"; $tmpactivity->defaultindex = $index; $tmpactivity->instance = $quiz->quiz; $tmpactivity->name = $quiz->name; $tmpactivity->section = $quiz->section; $tmpactivity->content->attemptid = $quiz->id; $tmpactivity->content->sumgrades = $quiz->sumgrades; $tmpactivity->content->maxgrade = $quiz->maxgrade; $tmpactivity->content->attempt = $quiz->attempt; $tmpactivity->user->userid = $quiz->userid; $tmpactivity->user->fullname = fullname($quiz); $tmpactivity->user->picture = $quiz->picture; $tmpactivity->timestamp = $quiz->timefinish; $activities[] = $tmpactivity; $index++; } } return; } function quiz_print_recent_mod_activity($activity, $course, $detail=false) { global $CFG, $THEME; echo ''; echo ""; echo "
cellcontent2\" class=\"forumpostpicture\" width=\"35\" valign=\"top\">"; print_user_picture($activity->user->userid, $course, $activity->user->picture); echo ""; if ($detail) { echo "modpixpath/$activity->type/icon.gif\" ". "height=16 width=16 alt=\"$activity->type\"> "; echo "wwwroot/mod/quiz/view.php?id=" . $activity->instance . "\">" . $activity->name . " - "; } if (isteacher($USER)) { $grades = "(" . $activity->content->sumgrades . " / " . $activity->content->maxgrade . ") "; echo "wwwroot/mod/quiz/review.php?q=" . $activity->instance . "&attempt=" . $activity->content->attemptid . "\">" . $grades . " "; echo get_string("attempt", "quiz") . " - " . $activity->content->attempt . "
"; } echo "wwwroot/user/view.php?id=" . $activity->user->userid . "&course=$course\">" . $activity->user->fullname . " "; echo " - " . userdate($activity->timestamp); echo "
"; return; } // this function creates default export filename function default_export_filename($course,$category) { //Take off some characters in the filename !! $takeoff = array(" ", ":", "/", "\\", "|"); $export_word = str_replace($takeoff,"_",strtolower(get_string("exportfilename","quiz"))); //If non-translated, use "export" if (substr($export_word,0,1) == "[") { $export_word= "export"; } //Calculate the date format string $export_date_format = str_replace(" ","_",get_string("exportnameformat","quiz")); //If non-translated, use "%Y%m%d-%H%M" if (substr($export_date_format,0,1) == "[") { $export_date_format = "%%Y%%m%%d-%%H%%M"; } //Calculate the shortname $export_shortname = clean_filename($course->shortname); if (empty($export_shortname) or $export_shortname == '_' ) { $export_shortname = $course->id; } //Calculate the category name $export_categoryname = clean_filename($category->name); //Calculate the final export filename //The export word $export_name = $export_word."-"; //The shortname $export_name .= strtolower($export_shortname)."-"; //The category name $export_name .= strtolower($export_categoryname)."-"; //The date format $export_name .= userdate(time(),$export_date_format,99,false); //The extension $export_name .= ".txt"; return $export_name; } // function to read all questions for category into big array // added by Howard Miller June 2004 function get_questions_category( $category ) { // questions will be added to an array $qresults = array(); // get the list of questions for the category $questions = get_records("quiz_questions","category",$category->id); // iterate through questions, getting stuff we need foreach($questions as $question) { $new_question = get_question_data( $question ); $qresults[] = $new_question; } return $qresults; } // function to read single question, parameter is object view of // quiz_categories record, results is a combined object // defined as follows... // ->id quiz_questions id // ->category category // ->name q name // ->questiontext // ->image // ->qtype see defines at the top of this file // ->stamp not too sure // ->version not sure // ----SHORTANSWER // ->usecase // ->answers array of answers // ----TRUEFALSE // ->trueanswer truefalse answer // ->falseanswer truefalse answer // ----MULTICHOICE // ->layout // ->single many or just one correct answer // ->answers array of answer objects // ----NUMERIC // ->min minimum answer span // ->max maximum answer span // ->answer single answer // ----MATCH // ->subquestions array of sub questions // ---->questiontext // ---->answertext function get_question_data( $question ) { // what to do next depends of question type (qtype) switch ($question->qtype) { case SHORTANSWER: $shortanswer = get_record("quiz_shortanswer","question",$question->id); $question->usecase = $shortanswer->usecase; $question->answers = get_exp_answers( $question->id ); break; case TRUEFALSE: if (!$truefalse = get_record("quiz_truefalse","question",$question->id)) { error( "quiz_truefalse record $question->id not found" ); } $question->trueanswer = get_exp_answer( $truefalse->trueanswer ); $question->falseanswer = get_exp_answer( $truefalse->falseanswer ); break; case MULTICHOICE: if (!$multichoice = get_record("quiz_multichoice","question",$question->id)) { error( "quiz_multichoice $question->id not found" ); } $question->layout = $multichoice->layout; $question->single = $multichoice->single; $question->answers = get_exp_answers( $multichoice->question ); break; case NUMERICAL: if (!$numeric = get_record("quiz_numerical","question",$question->id)) { error( "quiz_numerical $question->id not found" ); } $question->min = $numeric->min; $question->max = $numeric->max; $question->answer = get_exp_answer( $numeric->answer ); break; case MATCH: if (!$subquestions = get_records("quiz_match_sub","question",$question->id)) { error( "quiz_match_sub $question->id not found" ); } $question->subquestions = $subquestions; break; case DESCRIPTION: // nothing to do break; case MULTIANSWER: // nothing to do break; default: error("No handler for question type $question->qtype in get_question"); } return $question; } // function to return single answer // ->id answer id // ->question question number // ->answer // ->fraction // ->feedback function get_exp_answer( $id ) { if (!$answer = get_record("quiz_answers","id",$id )) { error( "quiz_answers record $id not found" ); } return $answer; } // function to return array of answers for export function get_exp_answers( $question_num ) { if (!$answers = get_records("quiz_answers","question",$question_num)) { error( "quiz_answers question $question_num not found" ); } return $answers; } ?>