diff --git a/admin/index.php b/admin/index.php index 88a33f8ae9c..36b90ef64a7 100644 --- a/admin/index.php +++ b/admin/index.php @@ -144,7 +144,7 @@ if (empty($_GET['confirmupgrade'])) { print_header($strdatabasechecking, $stradministration, $strdatabasechecking, "", "", false, " ", " "); - notice_yesno(get_string('upgradesure', 'admin', $a->newversion), 'index.php?confirmupgrade=yes', 'index.php'); + notice_yesno(get_string('upgradesure', 'admin', $a->newversion).'
'.get_string('upgradesure', 'quiz'), 'index.php?confirmupgrade=yes', 'index.php'); exit; } else { @@ -260,11 +260,11 @@ if (empty($CFG->siteidentifier)) { // Unique site identification code set_config('siteidentifier', random_string(32)); } - + /// Check if the guest user exists. If not, create one. if (! record_exists("user", "username", "guest")) { - $guest->auth = "manual"; - $guest->username = "guest"; + $guest->auth = "manual"; + $guest->username = "guest"; $guest->password = md5("guest"); $guest->firstname = addslashes(get_string("guestuser")); $guest->lastname = " "; diff --git a/lang/en/quiz.php b/lang/en/quiz.php index 3c9a018596c..c7b13ad7073 100644 --- a/lang/en/quiz.php +++ b/lang/en/quiz.php @@ -314,6 +314,7 @@ $string['uniform'] = 'decimals, from a uniform distribution'; $string['unit'] = 'Unit'; $string['unknowntype'] = 'Question type not supported at line $a. The question will be ignored'; $string['unsupportedformulafunction'] = 'The function $a is not supported'; +$string['upgradesure'] = '
In particular the quiz module will perform an extensive change of the quiz tables and this upgrade has not yet been sufficiently tested. You are very strongly urged to backup your database tables before proceeding.
'; $string['viewallanswers'] = 'View $a completed quizzes'; $string['viewallreports'] = 'View reports for $a attempts'; $string['warningsdetected'] = '$a warning(s) detected'; diff --git a/mod/quiz/attempt.php b/mod/quiz/attempt.php index 4ff150d769a..d0002c77c05 100644 --- a/mod/quiz/attempt.php +++ b/mod/quiz/attempt.php @@ -1,12 +1,31 @@ id, false, $cm); + $isteacher = isteacher($course->id); - -/// Set number for next attempt: - - if ($attempts = quiz_get_user_attempts($quiz->id, $USER->id)) { - $attemptnumber = 2; - foreach ($attempts as $attempt) { - if ($attempt->attempt >= $attemptnumber) { - $attemptnumber = $attempt->attempt + 1; - } - } - } else { +// Get number for the next or unfinished attempt + if(!$attemptnumber = (int)get_field_sql('SELECT MAX(attempt)+1 FROM ' . + "{$CFG->prefix}quiz_attempts WHERE quiz = '{$quiz->id}' AND " . + "userid = '{$USER->id}' AND timefinish > 0")) { $attemptnumber = 1; } - $strattemptnum = get_string("attempt", "quiz", $attemptnumber); + $strattemptnum = get_string('attempt', 'quiz', $attemptnumber); + $strquizzes = get_string("modulenameplural", "quiz"); + $popup = $isteacher ? 0 : $quiz->popup; // Controls whether this is shown in a javascript-protected window. - -// Print the page header - - if (!empty($quiz->popup)) { +/// Print the page header + if (!empty($popup)) { define('MESSAGE_WINDOW', true); // This prevents the message window coming up - print_header("$course->shortname: ".format_string($quiz->name), '', '', '', '', false, '', '', false, ''); - + print_header($course->shortname.': '.format_string($quiz->name), '', '', '', '', false, '', '', false, ''); + include('protect_js.php'); } else { - $strquizzes = get_string("modulenameplural", "quiz"); - $strquiz = get_string("modulename", "quiz"); + $strupdatemodule = isteacheredit($course->id) + ? update_module_button($cm->id, $course->id, get_string('modulename', 'quiz')) + : ""; print_header_simple(format_string($quiz->name), "", "id\">$strquizzes -> - id\">".format_string($quiz->name,true)." -> $strattemptnum", - "", "", true); + id\">".format_string($quiz->name)." -> $strattemptnum", + "", "", true, $strupdatemodule); } echo ''; // for overlib -/// Check availability + /// Print the quiz name heading and tabs for teacher + if ($isteacher) { + $currenttab = 'preview'; + include('tabs.php'); + } else { + print_heading(format_string($quiz->name)); + } - if ($quiz->attempts and $attempts and count($attempts) >= $quiz->attempts) { - error("Sorry, you've had $quiz->attempts attempts already.", "view.php?id=$cm->id"); +/// Check availability + if (isguest()) { + print_heading(get_string('guestsno', 'quiz')); + if (empty($popup)) { + print_footer($course); + } + exit; + } + + if ($quiz->attempts and $attemptnumber > $quiz->attempts) { + error(get_string('nomoreattempts', 'quiz'), "view.php?id={$cm->id}"); + } + + $timenow = time(); + if (($timenow < $quiz->timeopen || $timenow > $quiz->timeclose)) { + if ($isteacher) { + notify(get_string('notavailabletostudents', 'quiz')); + } else { + error(get_string('notavailable', 'quiz'), "view.php?id={$cm->id}"); + } } /// Check subnet access if ($quiz->subnet and !address_in_subnet(getremoteaddr(), $quiz->subnet)) { - error(get_string("subneterror", "quiz"), "view.php?id=$cm->id"); + if ($isteacher) { + notify(get_string('subnetnotice', 'quiz')); + } else { + error(get_string("subneterror", "quiz"), "view.php?id=$cm->id"); + } } /// Check password access if ($quiz->password and empty($_POST['q'])) { if (empty($_POST['quizpassword'])) { - - print_heading(format_string($quiz->name)); - print_heading(get_string("attempt", "quiz", $attemptnumber)); + if (trim(strip_tags($quiz->intro))) { print_simple_box(format_text($quiz->intro), "center"); } echo "
\n"; - + echo "
id\">\n"; print_simple_box_start("center"); - + echo "
\n"; print_string("requirepasswordmessage", "quiz"); echo "

\n"; @@ -103,9 +143,9 @@ print_simple_box_end(); echo "\n"; - - if (empty($quiz->popup)) { - print_footer($course); + + if (empty($popup)) { + print_footer(); } exit; @@ -113,226 +153,336 @@ if (strcmp($quiz->password, $_POST['quizpassword']) !== 0) { error(get_string("passworderror", "quiz"), "view.php?id=$cm->id"); } - unset($_POST['quizpassword']); /// needed so as not to confuse later code dealing with submitted answers } } - + +/// Load attempt or create a new attempt if there is no unfinished one + + if ($isteacher and $forcenew) { // teacher wants a new preview + // so we set a finish time on the current attempt (if any). + // It will then automatically be deleted below + set_field('quiz_attempts', 'timefinish', time(), 'quiz', $quiz->id, 'userid', $USER->id); + } + + $attempt = get_record('quiz_attempts', 'quiz', $quiz->id, + 'userid', $USER->id, 'timefinish', 0); + + if (!$attempt) { + // Check if this is a preview request from a teacher + // in which case the previous previews should be deleted + if ($isteacher) { + if ($oldattempts = get_records_select('quiz_attempts', "quiz = '$quiz->id' + AND userid = '$USER->id'")) { + delete_records('quiz_attempts', 'quiz', $quiz->id, 'userid', $USER->id); + delete_records('quiz_grades', 'quiz', $quiz->id, 'userid', $USER->id); + foreach ($oldattempts as $oldattempt) { + // there should only be one but we loop just in case + delete_records('quiz_states', 'attempt', $oldattempt->id); + delete_records('quiz_newest_states', 'attemptid', $oldattempt->id); + } + } + } + // Start a new attempt and initialize the question sessions + $attempt = quiz_create_attempt($quiz, $attemptnumber); + // If this is an attempt by a teacher mark it as a preview + if ($isteacher) { + $attempt->preview = 1; + } + // Save the attempt + if (!$attempt->id = insert_record('quiz_attempts', $attempt)) { + error('Could not create new attempt'); + } + // make log entries + if ($isteacher) { + add_to_log($course->id, 'quiz', 'preview', + "attempt.php?id=$cm->id", + "$quiz->id", $cm->id); + } else { + add_to_log($course->id, 'quiz', 'attempt', + "review.php?attempt=$attempt->id", + "$quiz->id", $cm->id); + } + } else { + // log continuation of attempt only if some time has lapsed + if ((time() - $attempt->timemodified) > 600) { // 10 minutes have elapsed + add_to_log($course->id, 'quiz', 'continue attempt', + "review.php?attempt=$attempt->id", + "$quiz->id", $cm->id); + } + } + + +/// Load all the questions and states needed by this script + + // list of questions needed by page + $pagelist = quiz_questions_on_page($attempt->layout, $page); + + // add all questions that are on the submitted form + if ($questionids) { + $questionlist = $pagelist.','.$questionids; + } else { + $questionlist = $pagelist; + } + + if (!$questionlist) { + error(get_string('noquestionsfound', 'quiz'), 'view.php?q='.$quiz->id); + } + + $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance". + " FROM {$CFG->prefix}quiz_questions q,". + " {$CFG->prefix}quiz_question_instances i". + " WHERE i.quiz = '$quiz->id' AND q.id = i.question". + " AND q.id IN ($questionlist)"; + + // Load the questions + if (!$questions = get_records_sql($sql)) { + error(get_string('noquestionsfound', 'quiz'), 'view.php?q='.$quiz->id); + } + + // Load the question type specific information + if (!quiz_get_question_options($questions)) { + error('Could not load question options'); + } + + // Restore the question sessions to their most recent states + // creating new sessions where required + if (!$states = quiz_restore_question_sessions($questions, $quiz, $attempt)) { + error('Could not restore question sessions'); + } + + +/// Process form data ///////////////////////////////////////////////// + + if ($responses = data_submitted() and empty($_POST['quizpassword'])) { + + // set the default event. This can be overruled by individual buttons. + $event = (array_key_exists('markall', $responses)) ? QUIZ_EVENTGRADE : + ($finishattempt ? QUIZ_EVENTCLOSE : QUIZ_EVENTSAVE); + + // Unset any variables we know are not responses + unset($responses->id); + unset($responses->q); + unset($responses->oldpage); + unset($responses->newpage); + unset($responses->review); + unset($responses->questionids); + unset($responses->saveattempt); // responses get saved anway + unset($responses->finishattempt); // same as $finishattempt + unset($responses->markall); + unset($responses->forcenewattempt); + + // extract responses + // $actions is an array indexed by the questions ids + $actions = quiz_extract_responses($questions, $responses, $event); + + // Process each question in turn + + $questionidarray = explode(',', $questionids); + foreach($questionidarray as $i) { + if (!isset($actions[$i])) { + $actions[$i]->responses = array('' => ''); + } + quiz_process_responses($questions[$i], $states[$i], $actions[$i], $quiz, $attempt); + quiz_save_question_session($questions[$i], $states[$i]); + } + + $attempt->timemodified = time(); + + // We have now finished processing form data + } + + +/// Finish attempt if requested + if ($finishattempt) { + + // Set the attempt to be finished + $attempt->timefinish = time(); + + // Find all the questions for this attempt for which the newest + // state is not also the newest graded state + if ($closequestions = get_records_select('quiz_newest_states', + "attemptid = $attempt->id AND new != newgraded", '', 'questionid, questionid')) { + + // load all the questions + $closequestionlist = implode(',', array_keys($closequestions)); + $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance". + " FROM {$CFG->prefix}quiz_questions q,". + " {$CFG->prefix}quiz_question_instances i". + " WHERE i.quiz = '$quiz->id' AND q.id = i.question". + " AND q.id IN ($closequestionlist)"; + if (!$closequestions = get_records_sql($sql)) { + error('Questions missing'); + } + + // Load the question type specific information + if (!quiz_get_question_options($closequestions)) { + error('Could not load question options'); + } + + // Restore the question sessions + if (!$closestates = quiz_restore_question_sessions($closequestions, $quiz, $attempt)) { + error('Could not restore question sessions'); + } + + foreach($closequestions as $key => $question) { + $action->event = QUIZ_EVENTCLOSE; + $action->responses = $closestates[$key]->responses; + quiz_process_responses($question, $closestates[$key], $action, $quiz, $attempt); + quiz_save_question_session($question, $closestates[$key]); + } + } + add_to_log($course->id, 'quiz', 'close attempt', + "review.php?attempt=$attempt->id", + "$quiz->id", $cm->id); + } + + +/// Update the quiz attempt and the overall grade for the quiz + if ($responses || $finishattempt) { + if (!update_record('quiz_attempts', $attempt)) { + error('Failed to save the current quiz attempt!'); + } + if (($attempt->attempt > 1 || $attempt->timefinish > 0) and !$attempt->preview) { + quiz_save_best_grade($quiz); + } + } + + if ($finishattempt) { + redirect('review.php?attempt='.$attempt->id); + } /// Get time limit if any. - $timelimit = $quiz->timelimit * 60; - $unattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id); - if($timelimit > 0) { - $timestart = $unattempt->timestart; - if($timestart) { - $timesincestart = time() - $timestart; + if ($timelimit > 0) { + $timestart = $attempt->timestart; + if ($timestart) { + $timesincestart = $timenow - $timestart; $timerstartvalue = $timelimit - $timesincestart; } else { $timerstartvalue = $timelimit; } + if ($timerstartvalue <= 0) { + $timerstartvalue = 1; + } + if(($timelimit + 60) <= $timesincestart) { + // To pass it on to quiz_grade_responses + $quiz->timesincestart = $timesincestart; + } } - if($timelimit and $timerstartvalue <= 0) { - $timerstartvalue = 1; - } - $timenow = time(); - $available = ($quiz->timeopen < $timenow and $timenow < $quiz->timeclose) || isteacher($course->id); +/// Print the quiz page //////////////////////////////////////////////////////// -/// Check to see if they are submitting answers - if ($rawanswers = data_submitted()) { - - $rawanswers = (array)$rawanswers; - - $shuffleorder = NULL; - - unset($rawanswers["q"]); // quiz id - if (! count($rawanswers) and ! $timelimit) { - print_heading(get_string("noanswers", "quiz")); - print_continue("attempt.php?q=$quiz->id"); - exit; - } - - if (!$questions = get_records_list("quiz_questions", "id", $quiz->questions)) { - error("No questions found!"); - } - - foreach ($rawanswers as $key => $value) { // Parse input for question->response - - if ($postedquestionid = quiz_extract_posted_id($key)) { - $questions[$postedquestionid]->response[$key] = trim($value); - - } else if ('shuffleorder' == $key) { - $shuffleorder = explode(",", $value); // Actual order questions were given in - - } else if ('navigation' == $key) { - $navigation = $value; // Determines next page to show in quiz - - } else { // Useful for debugging new question types. Must be last. - error("Unrecognizable input has been posted ($key -> $value)"); - } - } - - /// Retrieve ->maxgrade for all questions - If (!($grades = quiz_get_question_grades($quiz->id, $quiz->questions))) { - $grades = array(); - } - foreach ($grades as $qid => $grade) { - $questions[$qid]->maxgrade = $grade->grade; - } - - if (isset($timesincestart)) { - $quiz->timesincestart = $timesincestart; // To pass it on to quiz_grade_responses - } - - if (!$result = quiz_grade_responses($quiz, $questions, $unattempt->id)) { - error("Could not grade your quiz attempt!"); - } - - if ($attempt = quiz_save_attempt($quiz, $questions, $result, - $attemptnumber, 0 == $navigation)) { - if (empty($navigation)) { - add_to_log($course->id, "quiz", "submit", - "review.php?id=$cm->id&attempt=$attempt->id", "$quiz->id", $cm->id); - } - } else { - notice(get_string("alreadysubmitted", "quiz"), "view.php?id=$cm->id"); - if (empty($quiz->popup)) { - print_footer($course); - } - exit; - } - - if (empty($navigation)) { - /// Attempt has finished - - if (! quiz_save_best_grade($quiz, $USER->id)) { - error("Sorry! Could not calculate your best grade!"); - } - - if (empty($quiz->popup) and !$quiz->feedback) { - // No need to stop on this page, go directly to view.php - redirect('view.php?q='.$quiz->id); - } - - $strgrade = get_string("grade"); - $strscore = get_string("score", "quiz"); - - if ($quiz->grade) { - print_heading("$strscore: $result->sumgrades/$quiz->sumgrades ($result->percentage %)"); - print_heading("$strgrade: $result->grade/$quiz->grade"); - } - - /// continue button - use javascript to close down child window if in popup - include('attempt_close_js.php'); - - if ($quiz->feedback) { - $quiz->shuffleanswers = false; // Never shuffle answers in feedback - - /// Make sure to get all questions in case not all are shown - /// in quiz all the time. - $questions = quiz_get_attempt_questions($quiz, $attempt); - quiz_print_quiz_questions($quiz, $questions, - quiz_grade_responses($quiz, $questions, $unattempt->id), - $shuffleorder); - /// continue button - use javascript to close down child window if in popup - include('attempt_close_js.php'); - } - - if (empty($quiz->popup)) { - print_footer($course); - } - - exit; - } +/// Print the attempt number or preview heading + if ($isteacher) { + print_heading(get_string('previewquiz', 'quiz')); + unset($buttonoptions); + $buttonoptions['q'] = $quiz->id; + $buttonoptions['forcenew'] = true; + echo '
'; + print_single_button($CFG->wwwroot.'/mod/quiz/attempt.php', $buttonoptions, get_string('startagain', 'quiz')); + echo '
'; } else { - $navigation= 1; - $shuffleorder= NULL; + print_heading($strattemptnum); } - -/// Print the quiz page - - if (isguest()) { - print_heading(get_string("guestsno", "quiz")); - if (empty($quiz->popup)) { - print_footer($course); - } - exit; - } - -/// Actually seeing the questions marks the start of an attempt - - if (isset($unattempt) && $unattempt) { - $attempt = $unattempt; - - } else if ($attempt = quiz_start_attempt($quiz->id, $USER->id, $attemptnumber)) { - add_to_log($course->id, "quiz", "attempt", - "review.php?id=$cm->id&attempt=$attempt->id", "$quiz->id", $cm->id); - } else { - error("Sorry! Could not start the quiz (could not save starting time)"); - } - -/// First print the headings and so on - - print_heading(format_string($quiz->name)); - - if (!$available) { - error("Sorry, this quiz is not available", "view.php?id=$cm->id"); - } - - print_heading(get_string("attempt", "quiz", $attemptnumber)); - if (trim(strip_tags($quiz->intro))) { - print_simple_box(format_text($quiz->intro), "center"); - } - - /// Add the javascript timer in the title bar if the closing time appears close - $secondsleft = $quiz->timeclose - time(); if ($secondsleft > 0 and $secondsleft < 24*3600) { // less than a day remaining - include("jsclock.php"); + include('jsclock.php'); } -/// Include Javascript protection for this page if required +/// Start the form + if($quiz->timelimit > 0) { + // Make sure javascript is enabled for time limited quizzes + ?> + + + \n"; + } - if (!empty($quiz->popup)) { - include("protect_js.php"); + // Add a hidden field with the quiz id + echo '\n"; + +/// Print the navigation panel if required + $numpages = quiz_number_of_pages($attempt->layout); + if ($numpages > 1) { + ?> + + \n"; + quiz_print_navigation_panel($page, $numpages); + echo "
\n"; } /// Print all the questions - echo "
"; + // Add a hidden field with questionids + echo '\n"; - $questions = quiz_get_attempt_questions($quiz, $attempt, true); - if ($quiz->attemptonlast && $attemptnumber >= 2 and - $quiz->attempts == 0 || !$unattempt) { - // There are unlimited attempts or it is a new attempt. - // As the attempt also builds on the last, we can here - // have the student see the scores of the pre-entered - // responses that we here will have graded: - $result = quiz_grade_responses($quiz, $questions); - $result->attemptbuildsonthelast = true; - } else { - $result = NULL; - } - - // We do not show feedback or correct answers during an attempt: - $quiz->feedback = $quiz->correctanswers = false; - - if (!quiz_print_quiz_questions($quiz, $questions, - $result, $shuffleorder, $navigation)) { - print_continue("view.php?id=$cm->id"); + $pagequestions = explode(',', $pagelist); + $number = quiz_first_questionnumber($attempt->layout, $pagelist); + foreach ($pagequestions as $i) { + $options = quiz_get_renderoptions($quiz, $states[$i]); + // Print the question + if ($i > 0) { + echo "
\n"; + } + quiz_print_quiz_question($questions[$i], $states[$i], $number, $quiz, $options); + quiz_save_question_session($questions[$i], $states[$i]); + $number += $questions[$i]->length; } -/// If quiz is available and time limit is set include floating timer. +/// Print the submit buttons - if ($available and $timelimit > 0) { + $strconfirmattempt = addslashes(get_string("confirmclose", "quiz")); + $onclick = "return confirm('$strconfirmattempt')"; + echo "
\n"; + + echo "\n"; + if ($quiz->optionflags & QUIZ_ADAPTIVE) { + echo "\n"; + } + echo "\n"; + echo ''; + + echo "
"; + + // Print the navigation panel if required + if ($numpages > 1) { + echo "
\n"; + quiz_print_navigation_panel($page, $numpages); + echo '
'; + } + + // Finish the form + echo "\n"; + + // If time limit is set include floating timer. + if ($timelimit > 0) { require('jstimer.php'); } - if (empty($quiz->popup)) { + if (!$isteacher) { + include('attempt_close_js.php'); + } + + // Finish the page + if (empty($popup)) { print_footer($course); } diff --git a/mod/quiz/attempt_close_js.php b/mod/quiz/attempt_close_js.php index 767e50e86a7..7f62686aea8 100644 --- a/mod/quiz/attempt_close_js.php +++ b/mod/quiz/attempt_close_js.php @@ -1,15 +1,15 @@
-popup)) { + @@ -19,7 +19,7 @@ document.write(' $course->id ), get_string('continue')); + print_single_button("view.php", array( 'id' => $cm->id ), get_string('continue')); } ?>
diff --git a/mod/quiz/attempts.php b/mod/quiz/attempts.php new file mode 100644 index 00000000000..c83a60f6011 --- /dev/null +++ b/mod/quiz/attempts.php @@ -0,0 +1,354 @@ +libdir.'/tablelib.php'); + + optional_variable($id); // Course Module ID, or + optional_variable($q); // quiz ID + + if ($id) { + if (! $cm = get_record("course_modules", "id", $id)) { + error("Course Module ID was incorrect"); + } + + if (! $course = get_record("course", "id", $cm->course)) { + error("Course is misconfigured"); + } + + if (! $quiz = get_record("quiz", "id", $cm->instance)) { + error("Course module is incorrect"); + } + + } else { + if (! $quiz = get_record("quiz", "id", $q)) { + error("Course module is incorrect"); + } + if (! $course = get_record("course", "id", $quiz->course)) { + error("Course is misconfigured"); + } + if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) { + error("Course Module ID was incorrect"); + } + } + + require_login($course->id, false); + + if (!isteacher($course->id)) { + error("You are not allowed to use this script"); + } + + // if no questions have been set up yet redirect to edit.php + if (!$quiz->questions and isteacheredit($course->id)) { + redirect('edit.php?quizid='.$quiz->id); + } + + add_to_log($course->id, "quiz", "attempts", "attempts.php?id=$cm->id", "$quiz->id", "$cm->id"); + +/// Define some strings + + $strquizzes = get_string("modulenameplural", "quiz"); + $strquiz = get_string("modulename", "quiz"); + $strreallydel = addslashes(get_string('deleteattemptcheck','quiz')); + $strnoattempts = get_string('noattempts','quiz'); + $strtimeformat = get_string('strftimedatetime'); + $strreviewquestion = get_string('reviewresponse', 'quiz'); + +/// Print the page header + + print_header_simple(format_string($quiz->name), "", + "id\">$strquizzes + -> ".format_string($quiz->name), + "", "", true, update_module_button($cm->id, $course->id, $strquiz), navmenu($course, $cm)); + +/// Print the tabs + + $currenttab = 'attempts'; + include('tabs.php'); + +/// Deal with actions + + $action = optional_param('action', ''); + + switch($action) { + case 'delete': /// Some attempts need to be deleted + // the following needs to be improved to delete all associated data as well + + $attemptids = isset($_POST['attemptid']) ? $_POST['attemptid'] : array(); + if(!is_array($attemptids) || empty($attemptids)) { + break; + } + + foreach($attemptids as $num => $attemptid) { + if(empty($attemptid)) { + unset($attemptids[$num]); + } + } + + foreach($attemptids as $attemptid) { + if ($todelete = get_record('quiz_attempts', 'id', $attemptid)) { + + delete_records('quiz_attempts', 'id', $attemptid); + delete_records('quiz_states', 'attempt', $attemptid); + + // Search quiz_attempts for other instances by this user. + // If none, then delete record for this quiz, this user from quiz_grades + // else recalculate best grade + + $userid = $todelete->userid; + if (!record_exists('quiz_attempts', 'userid', $userid, 'quiz', $quiz->id)) { + delete_records('quiz_grades', 'userid', $userid,'quiz', $quiz->id); + } else { + quiz_save_best_grade($quiz, $userid); + } + } + } + break; + } + +/// Check to see if groups are being used in this quiz + if ($groupmode = groupmode($course, $cm)) { // Groups are being used + $currentgroup = setup_and_print_groups($course, $groupmode, "attempts.php?id=$cm->id&mode=overview"); + } else { + $currentgroup = false; + } + +/// Get all students + if ($currentgroup) { + $users = get_group_students($currentgroup); + } + else { + $users = get_course_students($course->id); + } + + if(empty($users)) { + print_heading($strnoattempts); + return true; + } + +/// Set table options + if(!isset($SESSION->quiz_overview_table)) { + $SESSION->quiz_overview_table = array('noattempts' => false, 'detailedmarks' => false); + } + + foreach($SESSION->quiz_overview_table as $option => $value) { + $urlparam = optional_param($option, NULL); + if($urlparam === NULL) { + $$option = $value; + } + else { + $$option = $SESSION->quiz_overview_table[$option] = $urlparam; + } + } + +/// Print display options + + echo '
'; + echo '
'; + echo '

'.get_string('displayoptions', 'quiz').': '; + echo ''; + echo ''; + echo ''; + echo ' '; + echo ' '; + echo ''; + echo '

'; + echo '
'; + echo '
'; + +/// Define table columns + $tablecolumns = array('checkbox', 'picture', 'fullname', 'timestart', 'duration'); + $tableheaders = array(NULL, '', get_string('fullname'), get_string('startedon', 'quiz'), get_string('attemptduration', 'quiz')); + + if ($quiz->grade) { + $tablecolumns[] = 'sumgrades'; + $tableheaders[] = get_string('grade', 'quiz').'/'.$quiz->grade; + } + + if($detailedmarks) { + // we want to display marks for all questions + // Start by getting all questions + $questionlist = quiz_questions_in_quiz($quiz->questions); + $questionids = explode(',', $questionlist); + $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance". + " FROM {$CFG->prefix}quiz_questions q,". + " {$CFG->prefix}quiz_question_instances i". + " WHERE i.quiz = '$quiz->id' AND q.id = i.question". + " AND q.id IN ($questionlist)"; + if (!$questions = get_records_sql($sql)) { + error('No questions found'); + } + $number = 1; + foreach($questionids as $key => $id) { + if ($questions[$id]->length) { + // Only print questions of non-zero length + $tablecolumns[] = '$'.$id; + $tableheaders[] = '#'.$number; + $questions[$id]->number = $number; + $number += $questions[$id]->length; + } else { + // get rid of zero length questions + unset($questions[$id]); + unset($questionids[$key]); + } + } + } + +/// Set up the table + + $table = new flexible_table('mod-quiz-report-overview-report'); + + $table->define_columns($tablecolumns); + $table->define_headers($tableheaders); + $table->define_baseurl($CFG->wwwroot.'/mod/quiz/attempts.php?id='.$cm->id); + + $table->sortable(true); + $table->collapsible(true); + $table->initialbars(count($users)>20); + + $table->column_suppress('picture'); + $table->column_suppress('fullname'); + + $table->column_class('picture', 'picture'); + + $table->set_attribute('cellspacing', '0'); + $table->set_attribute('id', 'attempts'); + $table->set_attribute('class', 'generaltable generalbox'); + + // Start working -- this is necessary as soon as the niceties are over + $table->setup(); + + +/// Construct the SQL + + if($where = $table->get_sql_where()) { + $where .= ' AND '; + } + + if($sort = $table->get_sql_sort()) { + $sortparts = explode(',', $sort); + $newsort = array(); + $firsttime = true; + foreach($sortparts as $sortpart) { + $sortpart = trim($sortpart); + if(substr($sortpart, 0, 1) == '$') { + if($firsttime) { + $qnum = intval(substr($sortpart, 1)); + $where .= '('.sql_isnull('qr.question').' OR qr.question = '.$qnum.') AND '; + $newsort[] = 'grade '.(strpos($sortpart, 'ASC')? 'ASC' : 'DESC'); + $firsttime = false; + } + } + else { + $newsort[] = $sortpart; + } + } + $sort = ' ORDER BY '.implode(', ', $newsort); + } + + $select = 'SELECT '.$db->Concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).' AS uvsa, u.id AS userid, u.firstname, u.lastname, u.picture, qa.id AS attempt, qa.sumgrades, qa.timefinish, qa.timestart, qa.timefinish - qa.timestart AS duration '; + $group = 'GROUP BY uvsa'; + $sql = 'FROM '.$CFG->prefix.'user u '. + 'LEFT JOIN '.$CFG->prefix.'quiz_attempts qa ON u.id = qa.userid '. + 'LEFT JOIN '.$CFG->prefix.'quiz_states qr ON qr.attempt = qa.id '. + 'WHERE '.$where.'u.id IN ('.implode(',', array_keys($users)).') AND ('.($noattempts ? sql_isnull('qa.quiz').' OR ' : '') . 'qa.quiz = '.$quiz->id.') '; + + + $total = count_records_sql('SELECT COUNT(DISTINCT('.$db->Concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).')) '.$sql); + $table->pagesize(10, $total); + + if($table->get_page_start() !== '' && $table->get_page_size() !== '') { + $limit = ' '.sql_paging_limit($table->get_page_start(), $table->get_page_size()); + } + else { + $limit = ''; + } + +/// Fetch the attempts + + $attempts = get_records_sql($select.$sql.$group.$sort.$limit); + +/// Build table rows + + if(!empty($attempts)) { + + foreach ($attempts as $attempt) { + + $picture = print_user_picture($attempt->userid, $course->id, $attempt->picture, false, true); + + $row = array( + '', + $picture, + ''.fullname($attempt).'', + empty($attempt->attempt) ? '-' : ''.userdate($attempt->timestart, $strtimeformat).'', + empty($attempt->attempt) ? '-' : + (empty($attempt->timefinish) ? get_string('unfinished', 'quiz') : + format_time($attempt->duration)) + ); + + if ($quiz->grade) { + $row[] = $attempt->sumgrades === NULL ? '-' : ''.format_float($attempt->sumgrades / $quiz->sumgrades * $quiz->grade,$quiz->decimalpoints).''; + } + + if($detailedmarks) { + if(empty($attempt->attempt)) { + foreach($questionids as $questionid) { + $row[] = '-'; + } + } + else { + foreach($questionids as $questionid) { + if ($gradedstateid = get_field('quiz_newest_states', 'newgraded', 'attemptid', $attempt->attempt, 'questionid', $questionid)) { + $grade = format_float(get_field('quiz_states', 'grade', 'id', $gradedstateid), $quiz->decimalpoints); + } else { + // This is an old-style attempt + $grade = format_float(get_field('quiz_states', 'grade', 'attempt', $attempt->attempt, 'question', $questionid), $quiz->decimalpoints); + } + $row[] = link_to_popup_window ('/mod/quiz/reviewquestion.php?state='.$gradedstateid.'&number='.$questions[$questionid]->number, 'reviewquestion', $grade, 450, 650, $strreviewquestion, 'none', true); + } + } + } + + $table->add_data($row); + } + +/// Start form + + echo '
'; + echo '
'; + echo ''; + +/// Print table + + $table->print_html(); + +/// Print "Select all" etc. + + echo ''; + echo '
'; + echo ''.get_string('selectall', 'quiz').' / '; + echo ''.get_string('selectnone', 'quiz').' '; + echo '  '; + $options = array('delete' => get_string('delete')); + $menu = str_replace(' 0) submitFormById(\'attemptsform\');', '', true)); + echo $menu; + echo ''; + echo ''; + echo '
'; + +/// Close form + echo '
'; + } + else { + print_heading(get_string('noattemptstoshow', 'quiz')); + } + + +/// Print footer + + print_footer($course); + +?> diff --git a/mod/quiz/backuplib.php b/mod/quiz/backuplib.php index eeea9867a17..2b686a7dfc9 100644 --- a/mod/quiz/backuplib.php +++ b/mod/quiz/backuplib.php @@ -90,7 +90,7 @@ // - quiz_question_grades // - quiz_attempts // - quiz_grades - // - quiz_responses + // - quiz_states // This step is the standard mod backup. (course dependent). //STEP 1. Backup categories/questions and associated structures @@ -618,31 +618,62 @@ return $status; } - //Backup quiz_question_grades contents (executed from quiz_backup_mods) - function backup_quiz_question_grades ($bf,$preferences,$quiz) { + //Backup quiz_question_instances contents (executed from quiz_backup_mods) + function backup_quiz_question_instances ($bf,$preferences,$quiz) { global $CFG; $status = true; - $quiz_question_grades = get_records("quiz_question_grades","quiz",$quiz,"id"); - //If there are question_grades - if ($quiz_question_grades) { + $quiz_question_instances = get_records("quiz_question_instances","quiz",$quiz,"id"); + //If there are question_instances + if ($quiz_question_instances) { //Write start tag - $status =fwrite ($bf,start_tag("QUESTION_GRADES",4,true)); - //Iterate over each question_grade - foreach ($quiz_question_grades as $que_gra) { - //Start question grade - $status =fwrite ($bf,start_tag("QUESTION_GRADE",5,true)); - //Print question_grade contents - fwrite ($bf,full_tag("ID",6,false,$que_gra->id)); - fwrite ($bf,full_tag("QUESTION",6,false,$que_gra->question)); - fwrite ($bf,full_tag("GRADE",6,false,$que_gra->grade)); - //End question grade - $status =fwrite ($bf,end_tag("QUESTION_GRADE",5,true)); + $status =fwrite ($bf,start_tag("QUESTION_INSTANCES",4,true)); + //Iterate over each question_instance + foreach ($quiz_question_instances as $que_ins) { + //Start question instance + $status =fwrite ($bf,start_tag("QUESTION_INSTANCE",5,true)); + //Print question_instance contents + fwrite ($bf,full_tag("ID",6,false,$que_ins->id)); + fwrite ($bf,full_tag("QUESTION",6,false,$que_ins->question)); + fwrite ($bf,full_tag("GRADE",6,false,$que_ins->grade)); + //End question instance + $status =fwrite ($bf,end_tag("QUESTION_INSTANCE",5,true)); } //Write end tag - $status =fwrite ($bf,end_tag("QUESTION_GRADES",4,true)); + $status =fwrite ($bf,end_tag("QUESTION_INSTANCES",4,true)); + } + return $status; + } + + //Backup quiz_question_versions contents (executed from quiz_backup_mods) + function backup_quiz_question_versions ($bf,$preferences,$quiz) { + + global $CFG; + + $status = true; + + $quiz_question_versions = get_records("quiz_question_versions","quiz",$quiz,"id"); + //If there are question_versions + if ($quiz_question_versions) { + //Write start tag + $status =fwrite ($bf,start_tag("QUESTION_VERSIONS",4,true)); + //Iterate over each question_version + foreach ($quiz_question_versions as $que_ver) { + //Start question version + $status =fwrite ($bf,start_tag("QUESTION_VERSION",5,true)); + //Print question_version contents + fwrite ($bf,full_tag("ID",6,false,$que_ver->id)); + fwrite ($bf,full_tag("OLDQUESTION",6,false,$que_ver->oldquestion)); + fwrite ($bf,full_tag("NEWQUESTION",6,false,$que_ver->newquestion)); + fwrite ($bf,full_tag("USERID",6,false,$que_ver->userid)); + fwrite ($bf,full_tag("TIMESTAMP",6,false,$que_ver->timestamp)); + //End question version + $status =fwrite ($bf,end_tag("QUESTION_VERSION",5,true)); + } + //Write end tag + $status =fwrite ($bf,end_tag("QUESTION_VERSIONS",4,true)); } return $status; } @@ -732,8 +763,8 @@ fwrite ($bf,full_tag("TIMESTART",6,false,$attempt->timestart)); fwrite ($bf,full_tag("TIMEFINISH",6,false,$attempt->timefinish)); fwrite ($bf,full_tag("TIMEMODIFIED",6,false,$attempt->timemodified)); - //Now write to xml the responses (in this attempt) - $status = backup_quiz_responses ($bf,$preferences,$attempt->id); + //Now write to xml the states (in this attempt) + $status = backup_quiz_states ($bf,$preferences,$attempt->id); //End attempt $status =fwrite ($bf,end_tag("ATTEMPT",5,true)); } @@ -743,16 +774,16 @@ return $status; } - //Backup quiz_responses contents (executed from backup_quiz_attempts) - function backup_quiz_responses ($bf,$preferences,$attempt) { + //Backup quiz_states contents (executed from backup_quiz_attempts) + function backup_quiz_states ($bf,$preferences,$attempt) { global $CFG; $status = true; - $quiz_responses = get_records("quiz_responses","attempt",$attempt,"id"); - //If there are responses - if ($quiz_responses) { + $quiz_states = get_records("quiz_states","attempt",$attempt,"id"); + //If there are states + if ($quiz_states) { //Write start tag $status =fwrite ($bf,start_tag("RESPONSES",6,true)); //Iterate over each response @@ -769,7 +800,7 @@ $status =fwrite ($bf,end_tag("RESPONSE",7,true)); } //Write end tag - $status =fwrite ($bf,end_tag("RESPONSES",6,true)); + $status =fwrite ($bf,end_tag("STATES",6,true)); } return $status; } diff --git a/mod/quiz/category.php b/mod/quiz/category.php index 4798ae5caca..42b9501fd76 100644 --- a/mod/quiz/category.php +++ b/mod/quiz/category.php @@ -25,6 +25,10 @@ "id\">".get_string('modulenameplural', 'quiz').''. ' -> '.get_string('editcategories', 'quiz')); + if (isset($SESSION->modform->instance) and $quiz = get_record('quiz', 'id', $SESSION->modform->instance)) { + include('tabs.php'); + } + /// CHECK FOR AND ACT UPON VARIABLES SUBMITTED VIA GET OR POST $qcobject = new quiz_category_object(); $qcobject->set_course($course); @@ -239,19 +243,14 @@ class quiz_category_object { * @param object course */ function output_edit_table() { - $this->edittable->head = array ($this->str->course, $this->str->category, $this->str->categoryinfo, $this->str->questions, $this->str->publish, + $this->edittable->head = array ($this->str->category, $this->str->categoryinfo, $this->str->questions, $this->str->publish, $this->str->delete, $this->str->order, $this->str->movecategoryto); $this->edittable->width = 200; $this->edittable->tablealign = 'center'; - // get list of short names to add to the course display. Only necessary if user has admin view. - if (isadmin()) { - $keys = implode(',', $this->get_course_ids($this->categories)); - $courses = get_records_select_menu('course', "id in ($keys)", '', 'id, shortname'); - } else { - $courses = $this->course->shortname; - } - $this->build_edit_table_body($this->categories, $courses); + $courses = $this->course->shortname; + + $this->build_edit_table_body($this->categories); print_table($this->edittable); } /** @@ -261,7 +260,7 @@ class quiz_category_object { * @param mixed courses String with shortname of course | array containing courseid=>shortname * @param int depth controls the indenting */ - function build_edit_table_body($categories, $courses, $depth = 0) { + function build_edit_table_body($categories, $depth = 0) { $countcats = count($categories); $count = 0; $first = true; @@ -275,10 +274,9 @@ class quiz_category_object { $up = $first ? false : true; $down = $last ? false : true; $first = false; - $courseshortname = is_string($courses) ? $courses : $courses[$category->course]; - $this->quiz_edit_category_row($category, $courseshortname, $depth, $up, $down); + $this->quiz_edit_category_row($category, $depth, $up, $down); if (isset($category->children)) { - $this->build_edit_table_body($category->children, $courses, $depth + 1); + $this->build_edit_table_body($category->children, $depth + 1); } } } @@ -309,16 +307,13 @@ class quiz_category_object { * @param boolean up can it be moved up? * @param boolean down can it be moved down? */ - function quiz_edit_category_row($category, $shortname, $depth, $up = false, $down = false) { + function quiz_edit_category_row($category, $depth, $up = false, $down = false) { global $USER; $fill = str_repeat($this->tab, $depth); $linkcss = $category->publish ? ' class="published"' : ' class="unpublished"'; /// Each section below adds a data cell to this table row - $this->edittable->align["$category->id.course"] = "left"; - $this->edittable->wrap["$category->id.course"] = "nowrap"; - $row["$category->id.course"] = $shortname; $this->edittable->align["$category->id.name"] = "left"; $this->edittable->wrap["$category->id.name"] = "nowrap"; @@ -526,7 +521,7 @@ class quiz_category_object { for ($index = count($levels) - 1; $index >= 0; $index--) { foreach($levels[$index] as $key) { $parentkey = $records[$key]->parent; - if (!($records[$key]->questioncount = count_records('quiz_questions', 'category', $records[$key]->id, 'hidden', 0))) { + if (!($records[$key]->questioncount = count_records('quiz_questions', 'category', $records[$key]->id, 'hidden', 0, 'parent', '0'))) { $records[$key]->questioncount = 0; } if ($parentkey == 0) { @@ -568,20 +563,11 @@ class quiz_category_object { */ function get_quiz_categories($parent=null, $sort="sortorder ASC") { - $admin = isadmin(); - if ($admin) { - if (is_null($parent)) { - $categories = get_records("quiz_categories", "", "", $sort); - } else { - $categories = get_records("quiz_categories", "parent", $parent, $sort); - } + if (is_null($parent)) { + $categories = get_records('quiz_categories', 'course', "{$this->course->id}", $sort); } else { - if (is_null($parent)) { - $categories = get_records('quiz_categories', 'course', "{$this->course->id}", $sort); - } else { - $select = "parent = '$parent' AND course = '{$this->course->id}'"; - $categories = get_records_select('quiz_categories', $select, $sort); - } + $select = "parent = '$parent' AND course = '{$this->course->id}'"; + $categories = get_records_select('quiz_categories', $select, $sort); } return $categories; } diff --git a/mod/quiz/config.html b/mod/quiz/config.html index f0bbc91d603..dcc772e9f95 100644 --- a/mod/quiz/config.html +++ b/mod/quiz/config.html @@ -1,5 +1,61 @@ dirroot . '/mod/quiz/locallib.php'); + + if (!isset($form->timeopen)) { + $form->timeopen = ""; + } + if (!isset($form->timeclose)) { + $form->timeclose = ""; + } + if (!isset($form->attempts)) { + $form->attempts = $CFG->quiz_attempts; + } + if (!isset($form->attemptonlast)) { + $form->attemptonlast = $CFG->quiz_attemptonlast; + } + if (!isset($form->grademethod)) { + $form->grademethod = $CFG->quiz_grademethod; + } + if (!isset($form->decimalpoints)) { + $form->decimalpoints = $CFG->quiz_decimalpoints; + } + if (!isset($form->review)) { + $form->review = $CFG->quiz_review; + } + if (!isset($form->questionsperpage)) { + $form->questionsperpage = $CFG->quiz_questionsperpage; + } + if (!isset($form->shufflequestions)) { + $form->shufflequestions = $CFG->quiz_shufflequestions; + } + if (!isset($form->shuffleanswers)) { + $form->shuffleanswers = $CFG->quiz_shuffleanswers; + } + if (!isset($form->grade)) { + $form->grade = $CFG->quiz_maximumgrade; + } + if (!isset($form->questions)) { + $form->questions = ""; + } + if (!isset($form->password)) { + $form->password = $CFG->quiz_password; + } + if (!isset($form->subnet)) { + $form->subnet = $CFG->quiz_subnet; + } + if (!isset($form->timelimit)) { + $form->timelimit = $CFG->quiz_timelimit; + } + if (!isset($form->popup)) { + $form->popup = $CFG->quiz_popup; + } + if (!isset($form->optionflags)) { + $form->optionflags = $CFG->quiz_optionflags; + } + if (!isset($form->penaltyscheme)) { + $form->penaltyscheme = $CFG->quiz_penaltyscheme; + } + ?>
@@ -75,6 +131,26 @@ + + @@ -85,19 +161,19 @@ - + @@ -111,13 +187,13 @@ } $perpage[0] = get_string('allinone', 'quiz'); - choose_from_menu($perpage, 'questionsperpage', $CFG->quiz_questionsperpage, ''); + choose_from_menu($perpage, 'questionsperpage', $form->questionsperpage, ''); helpbutton('questionsperpage', get_string('questionsperpage'), 'quiz'); ?> @@ -128,13 +204,13 @@ unset($options); $options[0] = get_string("no"); $options[1] = get_string("yes"); - choose_from_menu ($options, "quiz_shufflequestions", $CFG->quiz_shufflequestions, "", "", ""); + choose_from_menu ($options, "shufflequestions", $form->shufflequestions, "", "", ""); helpbutton("shufflequestions", get_string("shufflequestions","quiz"), "quiz"); ?> @@ -145,13 +221,13 @@ unset($options); $options[0] = get_string("no"); $options[1] = get_string("yes"); - choose_from_menu ($options, "quiz_shuffleanswers", $CFG->quiz_shuffleanswers, "", "", ""); + choose_from_menu ($options, "shuffleanswers", $form->shuffleanswers, "", "", ""); helpbutton("attempts", get_string("attemptsallowed","quiz"), "quiz"); ?> @@ -165,13 +241,13 @@ for ($i=2;$i<=6;$i++) { $options[$i] = "$i ".strtolower(get_string("attempts", "quiz")); } - choose_from_menu ($options, "quiz_attempts", $CFG->quiz_attempts, "", "", ""); + choose_from_menu ($options, "attempts", $form->attempts, "", "", ""); helpbutton("attempts", get_string("attemptsallowed","quiz"), "quiz"); ?> @@ -182,13 +258,13 @@ unset($options); $options[0] = get_string("no"); $options[1] = get_string("yes"); - choose_from_menu ($options, "quiz_attemptonlast", $CFG->quiz_attemptonlast, "", "", ""); + choose_from_menu ($options, "attemptonlast", $form->attemptonlast, "", "", ""); helpbutton("repeatattempts", get_string("eachattemptbuildsonthelast", "quiz"), "quiz"); ?> @@ -196,13 +272,41 @@ + + + + + + + + + + + + @@ -215,13 +319,13 @@ $options[1] = '1'; $options[2] = '2'; $options[3] = '3'; - choose_from_menu ($options, "quiz_decimalpoints", $CFG->quiz_decimalpoints, "", "", ""); + choose_from_menu ($options, "decimalpoints", $form->decimalpoints, "", "", ""); helpbutton("decimalpoints", get_string("decimalpoints","quiz"), "quiz"); ?> @@ -232,13 +336,13 @@ unset($options); $options[0] = get_string("no"); $options[1] = get_string("yes"); - choose_from_menu ($options, "quiz_showfeedback", $CFG->quiz_showfeedback, "", "", ""); + choose_from_menu ($options, "showfeedback", $form->showfeedback, "", "", ""); helpbutton("feedback", get_string("showfeedback","quiz"), "quiz"); ?> @@ -249,51 +353,21 @@ unset($options); $options[0] = get_string("no"); $options[1] = get_string("yes"); - choose_from_menu ($options, "quiz_showanswer", $CFG->quiz_showanswer, "", "", ""); + choose_from_menu ($options, "showanswer", $form->showanswer, "", "", ""); helpbutton("correctanswers", get_string("showcorrectanswer","quiz"), "quiz"); ?> - - + - - - - - - @@ -304,42 +378,65 @@ unset($options); $options[0] = get_string("no"); $options[1] = get_string("yes"); - choose_from_menu ($options, "quiz_popup", $CFG->quiz_popup, "", "", ""); + choose_from_menu ($options, "popup", $form->popup, "", "", ""); helpbutton("popup", get_string("popup","quiz"), "quiz"); ?> + + + + + + + diff --git a/mod/quiz/db/mysql.php b/mod/quiz/db/mysql.php index 18445615be7..f455e49ead5 100644 --- a/mod/quiz/db/mysql.php +++ b/mod/quiz/db/mysql.php @@ -4,8 +4,8 @@ function quiz_upgrade($oldversion) { // This function does anything necessary to upgrade // older versions to match current functionality - global $CFG; - include_once("$CFG->dirroot/mod/quiz/locallib.php"); + global $CFG, $QUIZ_QTYPES; + require_once("$CFG->dirroot/mod/quiz/locallib.php"); if ($oldversion < 2002101800) { execute_sql(" ALTER TABLE `quiz_attempts` ". @@ -208,7 +208,6 @@ function quiz_upgrade($oldversion) { if ($oldversion < 2004073001) { // Six new tables: - // One table for handling units for numerical questions modify_database ("", " CREATE TABLE `prefix_quiz_numerical_units` ( `id` int(10) unsigned NOT NULL auto_increment, @@ -218,7 +217,6 @@ function quiz_upgrade($oldversion) { PRIMARY KEY (`id`) ) TYPE=MyISAM COMMENT='Optional unit options for numerical questions'; "); - // Four tables for handling distribution and storage of // individual data for dataset dependent question types modify_database ("", " CREATE TABLE `prefix_quiz_attemptonlast_datasets` ( @@ -347,13 +345,297 @@ function quiz_upgrade($oldversion) { } if ($oldversion < 2005041304) { + // make random questions hidden modify_database('', "UPDATE prefix_quiz_questions SET hidden = '1' WHERE qtype ='".RANDOM."';"); } - + if ($oldversion < 2005042002) { table_column('quiz_answers', 'answer', 'answer', 'text', '', '', '', 'not null', ''); } + if ($oldversion < 2005042400) { + + // Changes to quiz table + + // The bits of the optionflags field will hold various option flags + table_column('quiz', '', 'optionflags', 'integer', '10', 'unsigned', '0', 'not null', 'timeclose'); + + // The penalty scheme + table_column('quiz', '', 'penaltyscheme', 'integer', '4', 'unsigned', '0', 'not null', 'optionflags'); + + // The review options are now all stored in the bits of the review field + table_column('quiz', 'review', 'review', 'integer', 10, 'unsigned', 0, 'not null', ''); + // We need to set the bits using the data in the existing fields + if ($quizzes = get_records('quiz')) { + foreach ($quizzes as $quiz) { + $review = (QUIZ_REVIEW_IMMEDIATELY & (QUIZ_REVIEW_RESPONSES + QUIZ_REVIEW_SCORES)); + if ($quiz->feedback) { + $review += (QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_FEEDBACK); + } + if ($quiz->correctanswers) { + $review += (QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_ANSWERS); + } + if ($quiz->review & 1) { + $review += QUIZ_REVIEW_CLOSED; + } + if ($quiz->review & 2) { + $review += QUIZ_REVIEW_OPEN; + } + set_field('quiz', 'review', $review, 'id', $quiz->id); + } + } + // update the default settings in $CFG + $review = (QUIZ_REVIEW_IMMEDIATELY & (QUIZ_REVIEW_RESPONSES + QUIZ_REVIEW_SCORES)); + if (!empty($CFG->quiz_feedback)) { + $review += (QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_FEEDBACK); + } + if (!empty($CFG->quiz_correctanswers)) { + $review += (QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_ANSWERS); + } + if (isset($CFG->quiz_review) and ($CFG->quiz_review & 1)) { + $review += QUIZ_REVIEW_CLOSED; + } + if (isset($CFG->quiz_review) and ($CFG->quiz_review & 2)) { + $review += QUIZ_REVIEW_OPEN; + } + set_config('quiz_review', $review); + + // We can now drop the fields whose data has been moved to the review field + execute_sql(" ALTER TABLE `{$CFG->prefix}quiz` DROP feedback"); + execute_sql(" ALTER TABLE `{$CFG->prefix}quiz` DROP correctanswers"); + + /// Changes to quiz_attempts table + + // The preview flag marks teacher previews + table_column('quiz_attempts', '', 'preview', 'tinyint', '2', 'unsigned', '0', 'not null', 'timemodified'); + + // The layout is the list of questions with inserted page breaks. + table_column('quiz_attempts', '', 'layout', 'text', '', '', '', 'not null', 'timemodified'); + // For old quiz attempts we will set this to the repaginated question list from $quiz->questions + + if ($quizzes) { + $quiz->questions = ($quiz->questionsperpage) ? quiz_repaginate($quiz->questions, $quiz->questionsperpage) : $quiz->questions; + foreach ($quizzes as $quiz) { + + // repaginate + if ($quiz->questionsperpage) { + $quiz->questions = quiz_repaginate($quiz->questions, $quiz->questionsperpage); + set_field('quiz', 'questions', $quiz->questions, 'id', $quiz->id); + } + set_field('quiz_attempts', 'layout', $quiz->questions, 'quiz', $quiz->id); + + // set preview flag + if ($teachers = get_course_teachers($quiz->course)) { + $teacherids = implode(',', array_keys($teachers)); + execute_sql("UPDATE {$CFG->prefix}quiz_attempts SET preview = 1 WHERE userid IN ($teacherids)"); + } + } + } + + /// Renaming tables + + // rename the quiz_question_grades table to quiz_question_instances + modify_database ('', 'ALTER TABLE prefix_quiz_question_grades RENAME prefix_quiz_question_instances;'); + + // rename the quiz_responses table quiz_states + modify_database ('', 'ALTER TABLE prefix_quiz_responses RENAME prefix_quiz_states;'); + + /// add columns to quiz_states table + + // The sequence number of the state. + table_column('quiz_states', '', 'seq_number', 'integer', '6', 'unsigned', '0', 'not null', 'originalquestion'); + // For existing states we leave this at 0 because in the old quiz code there was only one response allowed + + // The time the state was created. + table_column('quiz_states', '', 'timestamp', 'integer', '10', 'unsigned', '0', 'not null', 'answer'); + // For existing states we will below set this to the timemodified field of the attempt + + // The type of event that led to the creation of the state + table_column('quiz_states', '', 'event', 'integer', '4', 'unsigned', '0', 'not null', 'timestamp'); + + // The raw grade + table_column('quiz_states', '', 'raw_grade', 'varchar', '10', '', '', 'not null', 'grade'); + // For existing states (no penalties) this is equal to the grade + execute_sql("UPDATE {$CFG->prefix}quiz_states SET raw_grade = grade"); + + // The penalty that the response attracted + table_column('quiz_states', '', 'penalty', 'varchar', '10', '', '0.0', 'not null', 'raw_grade'); + // For existing states this can stay at 0 because penalties did not exist previously. + + /// New table for pointers to newest and newest graded states + + modify_database('', "CREATE TABLE `prefix_quiz_newest_states` ( + `id` int(10) unsigned NOT NULL auto_increment, + `attemptid` int(10) unsigned NOT NULL default '0', + `questionid` int(10) unsigned NOT NULL default '0', + `new` int(10) unsigned NOT NULL default '0', + `newgraded` int(10) unsigned NOT NULL default '0', + `sumpenalty` varchar(10) NOT NULL default '0.0', + PRIMARY KEY (`id`), + UNIQUE KEY `attemptid` (`attemptid`,`questionid`) + ) TYPE=MyISAM COMMENT='Gives ids of the newest open and newest graded states';"); + + /// Now upgrade states and newest_states where necessary + // to save time on large sites only do this for attempts that have not yet been finished. + if ($attempts = get_records_select('quiz_attempts', 'timefinish = 0')) { + foreach ($attempts as $attempt) { + quiz_upgrade_states($attempt); + } + } + + /// Entries for the log_display table + + modify_database('', " INSERT INTO prefix_log_display VALUES ('quiz', 'preview', 'quiz', 'name');"); + modify_database('', " INSERT INTO prefix_log_display VALUES ('quiz', 'start attempt', 'quiz', 'name');"); + modify_database('', " INSERT INTO prefix_log_display VALUES ('quiz', 'close attempt', 'quiz', 'name');"); + + /// Fix numerical question type + table_column('quiz_numerical', '', 'tolerance', 'varchar', '255', '', '0.0', 'not null', 'question'); + if($rows = get_records('quiz_numerical')) { + foreach ($rows as $row) { + $row->tolerance = ((float)$row->min + (float)$row->max)/2; + set_field('quiz_numerical', 'tolerance', $row->tolerance, 'id', $row->id); + } + } + // modify_database('', 'ALTER TABLE `prefix_quiz_numerical` DROP `answer`'); // There is no need for it at all + modify_database('', 'ALTER TABLE `prefix_quiz_numerical` DROP `min`'); // Replaced by tolerance + modify_database('', 'ALTER TABLE `prefix_quiz_numerical` DROP `max`'); // Replaced by tolerance + + /// Tables for Remote Questions + modify_database ('', "CREATE TABLE `prefix_quiz_rqp` ( + `id` int(10) unsigned NOT NULL auto_increment, + `question` int(10) unsigned NOT NULL default '0', + `type` int(10) unsigned NOT NULL default '0', + `source` longblob NOT NULL default '', + `format` varchar(255) NOT NULL default '', + `flags` tinyint(3) unsigned NOT NULL default '0', + `maxscore` int(10) unsigned NOT NULL default '1', + PRIMARY KEY (`id`), + KEY `question` (`question`) + ) TYPE=MyISAM COMMENT='Options for RQP questions';"); + + modify_database ('', "CREATE TABLE `prefix_quiz_rqp_type` ( + `id` int(10) unsigned NOT NULL auto_increment, + `name` varchar(255) NOT NULL default '', + `rendering_server` varchar(255) NOT NULL default '', + `cloning_server` varchar(255) NOT NULL default '', + `flags` tinyint(3) NOT NULL default '0', + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) + ) TYPE=MyISAM COMMENT='RQP question types and the servers to be used to process them';"); + + modify_database ('', "CREATE TABLE `prefix_quiz_rqp_states` ( + `id` int(10) unsigned NOT NULL auto_increment, + `stateid` int(10) unsigned NOT NULL default '0', + `responses` text NOT NULL default '', + `persistent_data` text NOT NULL default '', + `template_vars` text NOT NULL default '', + PRIMARY KEY (`id`) + ) TYPE=MyISAM COMMENT='RQP question type specific state information';"); + } + + if ($oldversion < 2005042900) { + + table_column('quiz_multianswers', '', 'sequence', 'varchar', '255', '', '', 'not null', 'question'); + table_column('quiz_numerical', '', 'answers', 'varchar', '255', '', '', 'not null', 'answer'); + modify_database('', 'UPDATE prefix_quiz_numerical SET answers = answer'); + table_column('quiz_questions', '', 'parent', 'integer', '10', 'unsigned', '0', 'not null', 'category'); + modify_database('', "UPDATE prefix_quiz_questions SET parent = id WHERE qtype ='".RANDOM."';"); + + // convert multianswer questions to the new model + if ($multianswers = get_records_sql("SELECT m.id, q.category, q.id AS parent, + q.name, q.questiontextformat, m.norm AS + defaultgrade, m.answertype AS qtype, + q.version, q.hidden, m.answers, + m.positionkey + FROM {$CFG->prefix}quiz_questions q, + {$CFG->prefix}quiz_multianswers m + WHERE q.qtype = '".MULTIANSWER."' + AND q.id = m.question + ORDER BY q.id ASC, m.positionkey ASC")) { + $multianswers = array_values($multianswers); + $n = count($multianswers); + $parent = $multianswers[0]->parent; + $sequence = array(); + + // turn reporting off temporarily to avoid one line output per set_field + global $db; + $olddebug = $db->debug; + // $db->debug = false; + for ($i = 0; $i < $n; $i++) { + $answers = $multianswers[$i]->answers; unset($multianswers[$i]->answers); + $pos = $multianswers[$i]->positionkey; unset($multianswers[$i]->positionkey); + + // create questions for all the multianswer victims + unset($multianswers[$i]->id); + $multianswers[$i]->length = 0; + $multianswers[$i]->questiontext = ''; + $multianswers[$i]->stamp = make_unique_id_code(); + $id = insert_record('quiz_questions', $multianswers[$i]); + $sequence[$pos] = $id; + + // update the answers table to point to these new questions + modify_database('', "UPDATE prefix_quiz_answers SET question = '$id' WHERE id IN ($answers);"); + // update the questiontype tables to point to these new questions + if (SHORTANSWER == $multianswers[$i]->qtype) { + modify_database('', "UPDATE prefix_quiz_shortanswer SET question = '$id' WHERE answers = '$answers';"); + } else if (NUMERICAL == $multianswers[$i]->qtype) { + if (strpos($answers, ',')) { + $numerical = get_records_list('quiz_numerical', 'answer', $answers); + // Get the biggest tolerance value + $tolerance = 0; + foreach ($numerical as $num) { + $tolerance = ($tolerance < $num->tolerance ? $num->tolerance : $tolerance); + } + delete_records_select('quiz_numerical', "answer IN ($answers)"); + $new = new stdClass; + $new->question = $id; + $new->tolerance = $tolerance; + $new->answers = $answers; + insert_record('quiz_numerical', $new); + unset($numerical, $new, $tolerance); + } else { + modify_database('', "UPDATE prefix_quiz_numerical SET question = '$id', answers = '$answers' WHERE answer IN ($answers);"); + } + } else if (MULTICHOICE == $multianswers[$i]->qtype) { + modify_database('', "UPDATE prefix_quiz_multichoice SET question = '$id' WHERE answers = '$answers';"); + } + + if (!isset($multianswers[$i+1]) || $parent != $multianswers[$i+1]->parent) { + delete_records('quiz_multianswers', 'question', $parent); + $multi = new stdClass; + $multi->question = $parent; + $multi->sequence = implode(',', $sequence); + insert_record('quiz_multianswers', $multi); + if (isset($multianswers[$i+1])) { + $parent = $multianswers[$i+1]->parent; + $sequence = array(); + } + } + } + $db->debug = $olddebug; + } + + // Remove redundant fields from quiz_multianswers + modify_database('', 'ALTER TABLE `prefix_quiz_multianswers` DROP `answers`'); + modify_database('', 'ALTER TABLE `prefix_quiz_multianswers` DROP `positionkey`'); + modify_database('', 'ALTER TABLE `prefix_quiz_multianswers` DROP `answertype`'); + modify_database('', 'ALTER TABLE `prefix_quiz_multianswers` DROP `norm`'); + + // Change numerical from answer to answers + modify_database('', 'ALTER TABLE `prefix_quiz_numerical` DROP `answer`'); + } + + if ($oldversion < 2005050300) { + // length of question determines question numbering. Currently all questions require one + // question number except for DESCRIPTION questions. + table_column('quiz_questions', '', 'length', 'integer', '10', 'unsigned', '1', 'not null', 'qtype'); + execute_sql("UPDATE {$CFG->prefix}quiz_questions SET length = 0 WHERE qtype = ".DESCRIPTION); + } + + if ($oldversion < 2005050500) { + table_column('quiz_questions', '', 'penalty', 'float', '', '', '0.1', 'not null', 'defaultgrade'); + } return true; } diff --git a/mod/quiz/db/mysql.sql b/mod/quiz/db/mysql.sql index 2a099223695..968fd6ac472 100644 --- a/mod/quiz/db/mysql.sql +++ b/mod/quiz/db/mysql.sql @@ -1,389 +1,484 @@ -# phpMyAdmin SQL Dump -# version 2.5.7 -# http://www.phpmyadmin.net -# -# Host: localhost -# Generation Time: Jul 30, 2004 at 02:52 PM -# Server version: 4.0.18 -# PHP Version: 4.3.7 -# Database : `moodle` -# -------------------------------------------------------- +-- phpMyAdmin SQL Dump +-- version 2.6.0-pl1 +-- http://www.phpmyadmin.net +-- +-- Host: localhost +-- Generation Time: May 04, 2005 at 06:27 PM +-- Server version: 4.0.15 +-- PHP Version: 4.3.3 +-- +-- Database: `upgrading` +-- -# -# Table structure for table `quiz` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz` ( - `id` int(10) unsigned NOT NULL auto_increment, - `course` int(10) unsigned NOT NULL default '0', - `name` varchar(255) NOT NULL default '', - `intro` text NOT NULL, - `timeopen` int(10) unsigned NOT NULL default '0', - `timeclose` int(10) unsigned NOT NULL default '0', - `attempts` smallint(6) NOT NULL default '0', - `attemptonlast` tinyint(4) NOT NULL default '0', - `feedback` tinyint(4) NOT NULL default '0', - `correctanswers` tinyint(4) NOT NULL default '1', - `grademethod` tinyint(4) NOT NULL default '1', - `decimalpoints` tinyint(4) NOT NULL default '2', - `review` tinyint(4) NOT NULL default '0', - `questionsperpage` int(10) NOT NULL default '0', - `shufflequestions` tinyint(4) NOT NULL default '0', - `shuffleanswers` tinyint(4) NOT NULL default '0', - `questions` text NOT NULL, - `sumgrades` int(10) NOT NULL default '0', - `grade` int(10) NOT NULL default '0', - `timecreated` int(10) unsigned NOT NULL default '0', - `timemodified` int(10) unsigned NOT NULL default '0', - `timelimit` int(2) unsigned NOT NULL default '0', - `password` varchar(255) NOT NULL default '', - `subnet` varchar(255) NOT NULL default '', - `popup` tinyint(4) NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `course` (`course`) +-- +-- Table structure for table `prefix_quiz` +-- + +CREATE TABLE prefix_quiz ( + id int(10) unsigned NOT NULL auto_increment, + course int(10) unsigned NOT NULL default '0', + name varchar(255) NOT NULL default '', + intro text NOT NULL, + timeopen int(10) unsigned NOT NULL default '0', + timeclose int(10) unsigned NOT NULL default '0', + optionflags int(10) unsigned NOT NULL default '0', + penaltyscheme int(4) unsigned NOT NULL default '0', + attempts smallint(6) NOT NULL default '0', + attemptonlast tinyint(4) NOT NULL default '0', + grademethod tinyint(4) NOT NULL default '1', + decimalpoints int(4) NOT NULL default '2', + review int(10) unsigned NOT NULL default '0', + questionsperpage int(10) NOT NULL default '0', + shufflequestions tinyint(4) NOT NULL default '0', + shuffleanswers tinyint(4) NOT NULL default '0', + questions text NOT NULL, + sumgrades int(10) NOT NULL default '0', + grade int(10) NOT NULL default '0', + timecreated int(10) unsigned NOT NULL default '0', + timemodified int(10) unsigned NOT NULL default '0', + timelimit int(2) unsigned NOT NULL default '0', + password varchar(255) NOT NULL default '', + subnet varchar(255) NOT NULL default '', + popup tinyint(4) NOT NULL default '0', + PRIMARY KEY (id), + KEY course (course) ) TYPE=MyISAM COMMENT='Main information about each quiz'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_answers` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_answers` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `answer` text NOT NULL, - `fraction` varchar(10) NOT NULL default '0.0', - `feedback` text NOT NULL, - PRIMARY KEY (`id`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_answers` +-- + +CREATE TABLE prefix_quiz_answers ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + answer text NOT NULL, + fraction varchar(10) NOT NULL default '0.0', + feedback text NOT NULL, + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Answers, with a fractional grade (0-1) and feedback'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_attemptonlast_datasets` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_attemptonlast_datasets` ( - `id` int(10) unsigned NOT NULL auto_increment, - `category` int(10) unsigned NOT NULL default '0', - `userid` int(10) unsigned NOT NULL default '0', - `datasetnumber` int(10) unsigned NOT NULL default '0', - PRIMARY KEY (`id`), - UNIQUE KEY `category` (`category`,`userid`) +-- +-- Table structure for table `prefix_quiz_attemptonlast_datasets` +-- + +CREATE TABLE prefix_quiz_attemptonlast_datasets ( + id int(10) unsigned NOT NULL auto_increment, + category int(10) unsigned NOT NULL default '0', + userid int(10) unsigned NOT NULL default '0', + datasetnumber int(10) unsigned NOT NULL default '0', + PRIMARY KEY (id), + UNIQUE KEY category (category,userid) ) TYPE=MyISAM COMMENT='Dataset number for attemptonlast attempts per user'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_attempts` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_attempts` ( - `id` int(10) unsigned NOT NULL auto_increment, - `quiz` int(10) unsigned NOT NULL default '0', - `userid` int(10) unsigned NOT NULL default '0', - `attempt` smallint(6) NOT NULL default '0', - `sumgrades` varchar(10) NOT NULL default '0.0', - `timestart` int(10) unsigned NOT NULL default '0', - `timefinish` int(10) unsigned NOT NULL default '0', - `timemodified` int(10) unsigned NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `quiz` (`quiz`), - KEY `userid` (`userid`) +-- +-- Table structure for table `prefix_quiz_attempts` +-- + +CREATE TABLE prefix_quiz_attempts ( + id int(10) unsigned NOT NULL auto_increment, + quiz int(10) unsigned NOT NULL default '0', + userid int(10) unsigned NOT NULL default '0', + attempt smallint(6) NOT NULL default '0', + sumgrades varchar(10) NOT NULL default '0.0', + timestart int(10) unsigned NOT NULL default '0', + timefinish int(10) unsigned NOT NULL default '0', + timemodified int(10) unsigned NOT NULL default '0', + layout text NOT NULL, + preview tinyint(3) unsigned NOT NULL default '0', + PRIMARY KEY (id), + KEY quiz (quiz), + KEY userid (userid) ) TYPE=MyISAM COMMENT='Stores various attempts on a quiz'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_calculated` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_calculated` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `answer` int(10) unsigned NOT NULL default '0', - `tolerance` varchar(20) NOT NULL default '0.0', - `tolerancetype` int(10) NOT NULL default '1', - `correctanswerlength` int(10) NOT NULL default '2', - `correctanswerformat` int(10) NOT NULL default '2', - PRIMARY KEY (`id`), - KEY `question` (`question`), - KEY `answer` (`answer`) +-- +-- Table structure for table `prefix_quiz_calculated` +-- + +CREATE TABLE prefix_quiz_calculated ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + answer int(10) unsigned NOT NULL default '0', + tolerance varchar(20) NOT NULL default '0.0', + tolerancetype int(10) NOT NULL default '1', + correctanswerlength int(10) NOT NULL default '2', + correctanswerformat int(10) NOT NULL default '2', + PRIMARY KEY (id), + KEY question (question), + KEY answer (answer) ) TYPE=MyISAM COMMENT='Options for questions of type calculated'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_categories` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_categories` ( - `id` int(10) unsigned NOT NULL auto_increment, - `course` int(10) unsigned NOT NULL default '0', - `name` varchar(255) NOT NULL default '', - `info` text NOT NULL, - `publish` tinyint(4) NOT NULL default '0', - `stamp` varchar(255) NOT NULL default '', - `parent` int(10) unsigned NOT NULL default '0', - `sortorder` int(10) unsigned NOT NULL default '999', - PRIMARY KEY (`id`), - KEY `course` (`course`) +-- +-- Table structure for table `prefix_quiz_categories` +-- + +CREATE TABLE prefix_quiz_categories ( + id int(10) unsigned NOT NULL auto_increment, + course int(10) unsigned NOT NULL default '0', + name varchar(255) NOT NULL default '', + info text NOT NULL, + publish tinyint(4) NOT NULL default '0', + stamp varchar(255) NOT NULL default '', + parent int(10) unsigned NOT NULL default '0', + sortorder int(10) unsigned NOT NULL default '999', + PRIMARY KEY (id), + KEY course (course) ) TYPE=MyISAM COMMENT='Categories are for grouping questions'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_dataset_definitions` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_dataset_definitions` ( - `id` int(10) unsigned NOT NULL auto_increment, - `category` int(10) unsigned NOT NULL default '0', - `name` varchar(255) NOT NULL default '', - `type` int(10) NOT NULL default '0', - `options` varchar(255) NOT NULL default '', - `itemcount` int(10) unsigned NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `category` (`category`) +-- +-- Table structure for table `prefix_quiz_dataset_definitions` +-- + +CREATE TABLE prefix_quiz_dataset_definitions ( + id int(10) unsigned NOT NULL auto_increment, + category int(10) unsigned NOT NULL default '0', + name varchar(255) NOT NULL default '', + type int(10) NOT NULL default '0', + options varchar(255) NOT NULL default '', + itemcount int(10) unsigned NOT NULL default '0', + PRIMARY KEY (id), + KEY category (category) ) TYPE=MyISAM COMMENT='Organises and stores properties for dataset items'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_dataset_items` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_dataset_items` ( - `id` int(10) unsigned NOT NULL auto_increment, - `definition` int(10) unsigned NOT NULL default '0', - `number` int(10) unsigned NOT NULL default '0', - `value` varchar(255) NOT NULL default '', - PRIMARY KEY (`id`), - KEY `definition` (`definition`) +-- +-- Table structure for table `prefix_quiz_dataset_items` +-- + +CREATE TABLE prefix_quiz_dataset_items ( + id int(10) unsigned NOT NULL auto_increment, + definition int(10) unsigned NOT NULL default '0', + number int(10) unsigned NOT NULL default '0', + value varchar(255) NOT NULL default '', + PRIMARY KEY (id), + KEY definition (definition) ) TYPE=MyISAM COMMENT='Individual dataset items'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_grades` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_grades` ( - `id` int(10) unsigned NOT NULL auto_increment, - `quiz` int(10) unsigned NOT NULL default '0', - `userid` int(10) unsigned NOT NULL default '0', - `grade` real NOT NULL default '0', - `timemodified` int(10) unsigned NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `quiz` (`quiz`), - KEY `userid` (`userid`) +-- +-- Table structure for table `prefix_quiz_grades` +-- + +CREATE TABLE prefix_quiz_grades ( + id int(10) unsigned NOT NULL auto_increment, + quiz int(10) unsigned NOT NULL default '0', + userid int(10) unsigned NOT NULL default '0', + grade double NOT NULL default '0', + timemodified int(10) unsigned NOT NULL default '0', + PRIMARY KEY (id), + KEY quiz (quiz), + KEY userid (userid) ) TYPE=MyISAM COMMENT='Final quiz grade (may be best of several attempts)'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_match` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_match` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `subquestions` varchar(255) NOT NULL default '', - PRIMARY KEY (`id`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_match` +-- + +CREATE TABLE prefix_quiz_match ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + subquestions varchar(255) NOT NULL default '', + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Defines fixed matching questions'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_match_sub` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_match_sub` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `questiontext` text NOT NULL, - `answertext` varchar(255) NOT NULL default '', - PRIMARY KEY (`id`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_match_sub` +-- + +CREATE TABLE prefix_quiz_match_sub ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + questiontext text NOT NULL, + answertext varchar(255) NOT NULL default '', + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Defines the subquestions that make up a matching question'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_multianswers` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_multianswers` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `answers` varchar(255) NOT NULL default '', - `positionkey` varchar(255) NOT NULL default '', - `answertype` smallint(6) NOT NULL default '0', - `norm` int(10) unsigned NOT NULL default '1', - PRIMARY KEY (`id`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_multianswers` +-- + +CREATE TABLE prefix_quiz_multianswers ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + sequence varchar(255) NOT NULL default '', + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Options for multianswer questions'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_multichoice` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_multichoice` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `layout` tinyint(4) NOT NULL default '0', - `answers` varchar(255) NOT NULL default '', - `single` tinyint(4) NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_multichoice` +-- + +CREATE TABLE prefix_quiz_multichoice ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + layout tinyint(4) NOT NULL default '0', + answers varchar(255) NOT NULL default '', + single tinyint(4) NOT NULL default '0', + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Options for multiple choice questions'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_numerical` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_numerical` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `answer` int(10) unsigned NOT NULL default '0', - `min` varchar(255) NOT NULL default '', - `max` varchar(255) NOT NULL default '', - PRIMARY KEY (`id`), - KEY `answer` (`answer`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_newest_states` +-- + +CREATE TABLE prefix_quiz_newest_states ( + id int(10) unsigned NOT NULL auto_increment, + attemptid int(10) unsigned NOT NULL default '0', + questionid int(10) unsigned NOT NULL default '0', + new int(10) unsigned NOT NULL default '0', + newgraded int(10) unsigned NOT NULL default '0', + sumpenalty varchar(10) NOT NULL default '0.0', + PRIMARY KEY (id), + UNIQUE KEY attemptid (attemptid,questionid) +) TYPE=MyISAM COMMENT='Gives ids of the newest open and newest graded states'; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `prefix_quiz_numerical` +-- + +CREATE TABLE prefix_quiz_numerical ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + tolerance varchar(255) NOT NULL default '0.0', + answers varchar(255) NOT NULL default '', + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Options for numerical questions'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_numerical_units` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_numerical_units` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `multiplier` decimal(40,20) NOT NULL default '1.00000000000000000000', - `unit` varchar(50) NOT NULL default '', - PRIMARY KEY (`id`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_numerical_units` +-- + +CREATE TABLE prefix_quiz_numerical_units ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + multiplier decimal(40,20) NOT NULL default '1.00000000000000000000', + unit varchar(50) NOT NULL default '', + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Optional unit options for numerical questions'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_question_datasets` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_question_datasets` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `datasetdefinition` int(10) unsigned NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `question` (`question`,`datasetdefinition`) +-- +-- Table structure for table `prefix_quiz_question_datasets` +-- + +CREATE TABLE prefix_quiz_question_datasets ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + datasetdefinition int(10) unsigned NOT NULL default '0', + PRIMARY KEY (id), + KEY question (question,datasetdefinition) ) TYPE=MyISAM COMMENT='Many-many relation between questions and dataset definitions'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_question_grades` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_question_grades` ( - `id` int(10) unsigned NOT NULL auto_increment, - `quiz` int(10) unsigned NOT NULL default '0', - `question` int(10) unsigned NOT NULL default '0', - `grade` smallint(6) NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `quiz` (`quiz`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_question_instances` +-- + +CREATE TABLE prefix_quiz_question_instances ( + id int(10) unsigned NOT NULL auto_increment, + quiz int(10) unsigned NOT NULL default '0', + question int(10) unsigned NOT NULL default '0', + grade smallint(6) NOT NULL default '0', + PRIMARY KEY (id), + KEY quiz (quiz), + KEY question (question) ) TYPE=MyISAM COMMENT='The grade for a question in a quiz'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_question_versions` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_question_versions` ( - `id` int(10) unsigned NOT NULL auto_increment, - `quiz` int(10) unsigned NOT NULL default '0', - `oldquestion` int(10) unsigned NOT NULL default '0', - `newquestion` int(10) unsigned NOT NULL default '0', - `userid` int(10) unsigned NOT NULL default '0', - `timestamp` int(10) unsigned NOT NULL default '0', - PRIMARY KEY (`id`) +-- +-- Table structure for table `prefix_quiz_question_versions` +-- + +CREATE TABLE prefix_quiz_question_versions ( + id int(10) unsigned NOT NULL auto_increment, + quiz int(10) unsigned NOT NULL default '0', + oldquestion int(10) unsigned NOT NULL default '0', + newquestion int(10) unsigned NOT NULL default '0', + userid int(10) unsigned NOT NULL default '0', + timestamp int(10) unsigned NOT NULL default '0', + PRIMARY KEY (id) ) TYPE=MyISAM COMMENT='The mapping between old and new versions of a question'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_questions` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_questions` ( - `id` int(10) NOT NULL auto_increment, - `category` int(10) NOT NULL default '0', - `name` varchar(255) NOT NULL default '', - `questiontext` text NOT NULL, - `questiontextformat` tinyint(2) NOT NULL default '0', - `image` varchar(255) NOT NULL default '', - `defaultgrade` int(10) unsigned NOT NULL default '1', - `qtype` smallint(6) NOT NULL default '0', - `stamp` varchar(255) NOT NULL default '', - `version` int(10) NOT NULL default '1', - `hidden` int(1) unsigned NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `category` (`category`) +-- +-- Table structure for table `prefix_quiz_questions` +-- + +CREATE TABLE prefix_quiz_questions ( + id int(10) NOT NULL auto_increment, + category int(10) NOT NULL default '0', + parent int(10) unsigned NOT NULL default '0', + name varchar(255) NOT NULL default '', + questiontext text NOT NULL, + questiontextformat tinyint(2) NOT NULL default '0', + image varchar(255) NOT NULL default '', + defaultgrade int(10) unsigned NOT NULL default '1', + penalty float NOT NULL default '0.1', + qtype smallint(6) NOT NULL default '0', + length int(10) unsigned NOT NULL default '1', + stamp varchar(255) NOT NULL default '', + version int(10) NOT NULL default '1', + hidden int(1) unsigned NOT NULL default '0', + PRIMARY KEY (id), + KEY category (category) ) TYPE=MyISAM COMMENT='The quiz questions themselves'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_randomsamatch` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_randomsamatch` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `choose` int(10) unsigned NOT NULL default '4', - PRIMARY KEY (`id`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_randomsamatch` +-- + +CREATE TABLE prefix_quiz_randomsamatch ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + choose int(10) unsigned NOT NULL default '4', + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Info about a random short-answer matching question'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_responses` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_responses` ( - `id` int(10) unsigned NOT NULL auto_increment, - `attempt` int(10) unsigned NOT NULL default '0', - `question` int(10) unsigned NOT NULL default '0', - `originalquestion` int(10) unsigned NOT NULL default '0', - `answer` text NOT NULL default '', - `grade` varchar(10) NOT NULL default '0.0', - PRIMARY KEY (`id`), - KEY `attempt` (`attempt`), - KEY `question` (`question`) -) TYPE=MyISAM COMMENT='Stores user responses to a quiz, and percentage grades'; -# -------------------------------------------------------- +-- +-- Table structure for table `prefix_quiz_rqp` +-- -# -# Table structure for table `quiz_shortanswer` -# +CREATE TABLE prefix_quiz_rqp ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + type int(10) unsigned NOT NULL default '0', + source longblob NOT NULL, + format varchar(255) NOT NULL default '', + flags tinyint(3) unsigned NOT NULL default '0', + maxscore int(10) unsigned NOT NULL default '1', + PRIMARY KEY (id), + KEY question (question) +) TYPE=MyISAM COMMENT='Options for RQP questions'; -CREATE TABLE `prefix_quiz_shortanswer` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `answers` varchar(255) NOT NULL default '', - `usecase` tinyint(2) NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `question` (`question`) +-- -------------------------------------------------------- + +-- +-- Table structure for table `prefix_quiz_rqp_states` +-- + +CREATE TABLE prefix_quiz_rqp_states ( + id int(10) unsigned NOT NULL auto_increment, + stateid int(10) unsigned NOT NULL default '0', + responses text NOT NULL, + persistent_data text NOT NULL, + template_vars text NOT NULL, + PRIMARY KEY (id) +) TYPE=MyISAM COMMENT='RQP question type specific state information'; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `prefix_quiz_rqp_type` +-- + +CREATE TABLE prefix_quiz_rqp_type ( + id int(10) unsigned NOT NULL auto_increment, + name varchar(255) NOT NULL default '', + rendering_server varchar(255) NOT NULL default '', + cloning_server varchar(255) NOT NULL default '', + flags tinyint(3) NOT NULL default '0', + PRIMARY KEY (id), + UNIQUE KEY name (name) +) TYPE=MyISAM COMMENT='RQP question types and the servers to be used to process the'; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `prefix_quiz_shortanswer` +-- + +CREATE TABLE prefix_quiz_shortanswer ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + answers varchar(255) NOT NULL default '', + usecase tinyint(2) NOT NULL default '0', + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Options for short answer questions'; -# -------------------------------------------------------- -# -# Table structure for table `quiz_truefalse` -# +-- -------------------------------------------------------- -CREATE TABLE `prefix_quiz_truefalse` ( - `id` int(10) unsigned NOT NULL auto_increment, - `question` int(10) unsigned NOT NULL default '0', - `trueanswer` int(10) unsigned NOT NULL default '0', - `falseanswer` int(10) unsigned NOT NULL default '0', - PRIMARY KEY (`id`), - KEY `question` (`question`) +-- +-- Table structure for table `prefix_quiz_states` +-- + +CREATE TABLE prefix_quiz_states ( + id int(10) unsigned NOT NULL auto_increment, + attempt int(10) unsigned NOT NULL default '0', + question int(10) unsigned NOT NULL default '0', + originalquestion int(10) unsigned NOT NULL default '0', + seq_number int(6) unsigned NOT NULL default '0', + answer text NOT NULL, + timestamp int(10) unsigned NOT NULL default '0', + event int(4) unsigned NOT NULL default '0', + grade varchar(10) NOT NULL default '0.0', + raw_grade varchar(10) NOT NULL default '', + penalty varchar(10) NOT NULL default '0.0', + PRIMARY KEY (id), + KEY attempt (attempt), + KEY question (question) +) TYPE=MyISAM COMMENT='Stores user responses to a quiz, and percentage grades'; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `prefix_quiz_truefalse` +-- + +CREATE TABLE prefix_quiz_truefalse ( + id int(10) unsigned NOT NULL auto_increment, + question int(10) unsigned NOT NULL default '0', + trueanswer int(10) unsigned NOT NULL default '0', + falseanswer int(10) unsigned NOT NULL default '0', + PRIMARY KEY (id), + KEY question (question) ) TYPE=MyISAM COMMENT='Options for True-False questions'; -# -------------------------------------------------------- - + INSERT INTO prefix_log_display VALUES ('quiz', 'add', 'quiz', 'name'); INSERT INTO prefix_log_display VALUES ('quiz', 'update', 'quiz', 'name'); INSERT INTO prefix_log_display VALUES ('quiz', 'view', 'quiz', 'name'); @@ -392,3 +487,6 @@ INSERT INTO prefix_log_display VALUES ('quiz', 'attempt', 'quiz', 'name'); INSERT INTO prefix_log_display VALUES ('quiz', 'submit', 'quiz', 'name'); INSERT INTO prefix_log_display VALUES ('quiz', 'review', 'quiz', 'name'); INSERT INTO prefix_log_display VALUES ('quiz', 'editquestions', 'quiz', 'name'); +INSERT INTO prefix_log_display VALUES ('quiz', 'preview', 'quiz', 'name'); +INSERT INTO prefix_log_display VALUES ('quiz', 'start attempt', 'quiz', 'name'); +INSERT INTO prefix_log_display VALUES ('quiz', 'close attempt', 'quiz', 'name'); diff --git a/mod/quiz/defaults.php b/mod/quiz/defaults.php index 79453c56f88..228c5cef8b4 100644 --- a/mod/quiz/defaults.php +++ b/mod/quiz/defaults.php @@ -4,37 +4,37 @@ // It defines default values for any important configuration variables $defaults = array ( - 'quiz_allowreview' => 1, + 'quiz_review' => hexdec('ffffff'), 'quiz_attemptonlast' => 0, 'quiz_attempts' => 0, 'quiz_grademethod' => '', 'quiz_decimalpoints' => 2, - 'quiz_maximumgrade' => 0, + 'quiz_maximumgrade' => 10, 'quiz_password' => '', 'quiz_popup' => 0, 'quiz_questionsperpage' => 0, - 'quiz_showanswer' => '', - 'quiz_showfeedback' => '', 'quiz_shuffleanswers' => 1, 'quiz_shufflequestions' => 0, 'quiz_subnet' => '', 'quiz_timelimit' => 0, - - 'quiz_fix_allowreview' => 0, + 'quiz_optionflags' => 1, + 'quiz_penaltyscheme' => 0, + 'quiz_adaptive' => 1, + + 'quiz_fix_review' => 0, 'quiz_fix_attemptonlast' => 0, 'quiz_fix_attempts' => 0, 'quiz_fix_grademethod' => 0, 'quiz_fix_decimalpoints' => 0, - 'quiz_fix_maximumgrade' => 0, 'quiz_fix_password' => 0, 'quiz_fix_popup' => 0, 'quiz_fix_questionsperpage' => 0, - 'quiz_fix_showanswer' => 0, - 'quiz_fix_showfeedback' => 0, 'quiz_fix_shuffleanswers' => 0, 'quiz_fix_shufflequestions' => 0, 'quiz_fix_subnet' => 0, - 'quiz_fix_timelimit' => 0 + 'quiz_fix_timelimit' => 0, + 'quiz_fix_adaptive' => 0, + 'quiz_fix_penaltyscheme' => 0 ); ?> diff --git a/mod/quiz/edit.php b/mod/quiz/edit.php index 734a0c36c6c..12f33310c56 100644 --- a/mod/quiz/edit.php +++ b/mod/quiz/edit.php @@ -1,7 +1,40 @@ modform = $modform; // Save the form in the current session } else if ($quizid) { - if (! $modform = get_record('quiz', 'id', $quizid)) { - error("The required quiz doesn't exist"); - } - $modform->instance = $modform->id; - $SESSION->modform = $modform; // Save the form in the current session + if (isset($SESSION->modform) and $SESSION->modform->id == $quizid) { + // modform for this quiz already exists, use it + $modform = $SESSION->modform; + } else { + // create new modform from database + if (! $modform = get_record('quiz', 'id', $quizid)) { + error("The required quiz doesn't exist"); + } + $modform->instance = $modform->id; + $SESSION->modform = $modform; // Save the form in the current session - $cm = get_coursemodule_from_instance('quiz', $modform->instance); - add_to_log($cm->course, 'quiz', 'editquestions', - "view.php?id=$cm->id", - "$quizid", $cm->id); + $cm = get_coursemodule_from_instance('quiz', $modform->instance); + $modform->cmid = $cm->id; + // We don't log all visits to this page but only those that recreate modform + add_to_log($cm->course, 'quiz', 'editquestions', + "view.php?id=$cm->id", + "$quizid", $cm->id); + } } else if ($courseid) { // Page retrieve through "Edit Questions" link - no quiz selected $modform->course = $courseid; @@ -39,18 +80,13 @@ add_to_log($courseid, 'quiz', 'editquestions', "index.php?id=$courseid"); } else { + // no quiz or course was specified so we need to use the stored modform if (!isset($SESSION->modform)) { - // We currently also get here after editing a question by - // following the edit link on the review page. Perhaps that should be fixed. - error(''); + error(get_string('wronguse', 'quiz'), '../../course/view.php?id='.$course->id); } - - // The data is obtained from a $SESSION variable. This is mostly for historic reasons. - // With the way things work now it would be just as possible to get the data from the database. $modform = $SESSION->modform; } - if (! $course = get_record("course", "id", $modform->course)) { error("This course doesn't exist"); } @@ -64,113 +100,122 @@ if (isset($modform->instance) && empty($modform->grades)) // Construct an array to hold all the grades. { - $modform->grades = quiz_get_all_question_grades($modform->questions, $modform->instance); + $modform->grades = quiz_get_all_question_grades($modform); } - /// Now, check for commands on this page and modify variables as necessary if (isset($_REQUEST['up']) and confirm_sesskey()) { /// Move the given question up a slot $questions = explode(",", $modform->questions); - if ($questions[0] <> $up) { - foreach ($questions as $key => $question) { - if ($up == $question) { - $swap = $questions[$key-1]; - $questions[$key-1] = $question; - $questions[$key] = $swap; - break; - } - } + if ($up > 0 and isset($questions[$up])) { + $prevkey = ($questions[$up-1] == 0) ? $up-2 : $up-1; + $swap = $questions[$prevkey]; + $questions[$prevkey] = $questions[$up]; + $questions[$up] = $swap; $modform->questions = implode(",", $questions); - } - if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { - error('Could not save question list'); + // Always have a page break at the end + $modform->questions = $modform->questions . ',0'; + // Avoid duplicate page breaks + $modform->questions = str_replace(',0,0', ',0', $modform->questions); + if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { + error('Could not save question list'); + } } } if (isset($_REQUEST['down']) and confirm_sesskey()) { /// Move the given question down a slot $questions = explode(",", $modform->questions); - if ($questions[count($questions)-1] <> $down) { - foreach ($questions as $key => $question) { - if ($down == $question) { - $swap = $questions[$key+1]; - $questions[$key+1] = $question; - $questions[$key] = $swap; - break; - } - } + if ($down < count($questions)) { + $nextkey = ($questions[$down+1] == 0) ? $down+2 : $down+1; + $swap = $questions[$nextkey]; + $questions[$nextkey] = $questions[$down]; + $questions[$down] = $swap; $modform->questions = implode(",", $questions); - } - if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { - error('Could not save question list'); + // Avoid duplicate page breaks + $modform->questions = str_replace(',0,0', ',0', $modform->questions); + if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { + error('Could not save question list'); + } } } if (isset($_REQUEST['addquestion']) and confirm_sesskey()) { /// Add a single question to the current quiz - - // add question to quiz->questions - $key = $_REQUEST['addquestion']; - $questions = array(); - if (!empty($modform->questions)) { - $questions = explode(",", $modform->questions); - } - if (!in_array($key, $questions)) { - $questions[] = $key; - } - $modform->questions = implode(",", $questions); - if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { - error('Could not save question list'); - } - - // update question grades - $questionrecord = get_record("quiz_questions", "id", $key); - if (!empty($questionrecord->defaultgrade)) { - $modform->grades[$key] = $questionrecord->defaultgrade; - } else if ($questionrecord->qtype == DESCRIPTION){ - $modform->grades[$key] = 0; - } else { - $modform->grades[$key] = 1; - } - quiz_questiongrades_update($modform->grades, $modform->instance); + quiz_add_quiz_question($_REQUEST['addquestion'], $modform); } if (isset($_REQUEST['add']) and confirm_sesskey()) { /// Add selected questions to the current quiz - $rawquestions = $_POST; - if (!empty($modform->questions)) { - $questions = explode(",", $modform->questions); - } - foreach ($rawquestions as $key => $value) { // Parse input for question ids + foreach ($_POST as $key => $value) { // Parse input for question ids if (substr($key, 0, 1) == "q") { - $key = substr($key,1); - if (!empty($questions)) { - foreach ($questions as $question) { - if ($question == $key) { - continue 2; - } - } - } - $questions[] = $key; - - $questionrecord = get_record("quiz_questions", "id", $key); - - if (!empty($questionrecord->defaultgrade)) { - $modform->grades[$key] = $questionrecord->defaultgrade; - } else if ($questionrecord->qtype == DESCRIPTION){ - $modform->grades[$key] = 0; - } else { - $modform->grades[$key] = 1; - } + quiz_add_quiz_question(substr($key,1), $modform); } } - if (!empty($questions)) { - $modform->questions = implode(",", $questions); + } + + if (isset($_REQUEST['addrandom']) and confirm_sesskey()) { /// Add random questions to the quiz + $recurse = optional_param('recurse', 0, PARAM_BOOL); + $categoryid = required_param('categoryid', PARAM_INT); + $randomcount = required_param('randomcount', PARAM_INT); + // load category + if (! $category = get_record('quiz_categories', 'id', $categoryid)) { + error('Category ID is incorrect'); + } + // find existing random questions in this category + $random = RANDOM; + if ($existingquestions = get_records_select('quiz_questions', "qtype = '$random' AND category = '$category->id'")) { + // now remove the ones that are already used in this quiz + if ($questionids = explode(',', $modform->questions)) { + foreach ($questionids as $questionid) { + unset($existingquestions[$questionid]); + } + } + // now take as many of these as needed + $i = 0; + while (($existingquestion = array_pop($existingquestions)) and ($i < $randomcount)) { + if ($existingquestion->questiontext == $recurse) { + // this question has the right recurse property, so use it + quiz_add_quiz_question($existingquestion->id, $modform); + $i++; + } + } + $randomcreate = $randomcount - $i; // the number of additional random questions needed. } else { - $modform->questions = ""; + $randomcreate = $randomcount; } - if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { - error('Could not save question list'); + + if ($randomcreate > 0) { + + $form->name = get_string('random', 'quiz') .' ('. $category->name .')'; + $form->questiontext = $recurse; // we use the questiontext field to store the info + // on whether to include questions in subcategories + $form->questiontextformat = 0; + $form->image = ''; + $form->defaultgrade = 1; + $form->hidden = 1; + for ($i=0; $i<$randomcreate; $i++) { + $form->stamp = make_unique_id_code(); // Set the unique code (not to be changed) + $question = new stdClass; + $question->category = $category->id; + $question->qtype = RANDOM; + $question = $QUIZ_QTYPES[RANDOM]->save_question($question, $form, $course); + if(!isset($question->id)) { + error('Could not insert new random question!'); + } + quiz_add_quiz_question($question->id, $modform); + } + } + } + + if (isset($_REQUEST['repaginate']) and confirm_sesskey()) { /// Re-paginate the quiz + if (isset($_REQUEST['questionsperpage'])) { + $modform->questionsperpage = required_param('questionsperpage', 1, PARAM_INT); + if (!set_field('quiz', 'questionsperpage', $modform->questionsperpage, 'id', $modform->id)) { + error('Could not save number of questions per page'); + } + } + $modform->questions = quiz_repaginate($modform->questions, $modform->questionsperpage); + if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->id)) { + error('Could not save layout'); } - quiz_questiongrades_update($modform->grades, $modform->instance); } if (isset($_REQUEST['move']) and confirm_sesskey()) { /// Move selected questions to new category @@ -180,8 +225,7 @@ if (!isteacheredit($tocategory->course)) { error(get_string('categorynoedit', 'quiz', $tocategory->name), 'edit.php'); } - $rawquestions = $_POST; - foreach ($rawquestions as $key => $value) { // Parse input for question ids + foreach ($_POST as $key => $value) { // Parse input for question ids if (substr($key, 0, 1) == "q") { $key = substr($key,1); if (!set_field('quiz_questions', 'category', $tocategory->id, 'id', $key)) { @@ -192,29 +236,7 @@ } if (isset($_REQUEST['delete']) and confirm_sesskey()) { /// Remove a question from the quiz - $questions = explode(",", $modform->questions); - foreach ($questions as $key => $question) { - if ($question == $delete) { - unset($questions[$key]); - unset($modform->grades[$question]); - // Delete all responses associated with all attempts for this question in the quiz. - if ($attempts = get_records('quiz_attempts', 'quiz', $modform->instance)) { - foreach ($attempts as $attempt) { - if (!delete_records('quiz_responses', 'question', $question, 'attempt', $attempt->id)) { - error('Could not delete all responses for this question'); - } - } - } - // Delete question from quiz_question_grades table - if (!delete_records('quiz_question_grades', 'quiz', $modform->instance, 'question', $question)) { - error('Could not delete the question from this quiz'); - } - } - } - $modform->questions = implode(",", $questions); - if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { - error('Could not save question list'); - } + quiz_delete_quiz_question($_REQUEST['delete'], $modform); } if (isset($_REQUEST['deleteselected'])) { // delete selected questions from the category @@ -224,16 +246,13 @@ if ($questionlist = explode(',', $deleteselected)) { // for each question either hide it if it is in use or delete it foreach ($questionlist as $questionid) { - if (record_exists('quiz_responses', 'question', $questionid) or - record_exists('quiz_responses', 'originalquestion', $questionid) or - record_exists('quiz_question_grades', 'question', $questionid)) { + if (record_exists('quiz_question_instances', 'question', $questionid) or + record_exists('quiz_states', 'originalquestion', $questionid)) { if (!set_field('quiz_questions', 'hidden', 1, 'id', $questionid)) { - error('Was not able to hide question'); + error('Was not able to hide question'); } } else { - if (!delete_records("quiz_questions", "id", $questionid)) { - error("An error occurred trying to delete question (id $questionid)"); - } + delete_records("quiz_questions", "id", $questionid); } } } @@ -253,9 +272,8 @@ if (substr($key, 0, 1) == "q") { $key = substr($key,1); $questionlist .= $key.','; - if (record_exists('quiz_responses', 'question', $key) or - record_exists('quiz_responses', 'originalquestion', $key) or - record_exists('quiz_question_grades', 'question', $key)) { + if (record_exists('quiz_question_instances', 'question', $key) or + record_exists('quiz_states', 'originalquestion', $key)) { $questionnames .= '* '; $inuse = true; } @@ -281,28 +299,44 @@ } } - if (isset($_REQUEST['setgrades']) and confirm_sesskey()) { /// The grades have been updated, so update our internal list + if (isset($_REQUEST['setgrades']) and confirm_sesskey()) { + /// The grades have been updated, so update our internal list $rawgrades = $_POST; unset($modform->grades); foreach ($rawgrades as $key => $value) { // Parse input for question -> grades if (substr($key, 0, 1) == "q") { $key = substr($key,1); $modform->grades[$key] = $value; + quiz_update_question_instance($modform->grades[$key], $key, $modform->instance); } } - if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { - error('Could not save question list'); + + // If rescaling is required save the new maximum + if (isset($_REQUEST['maxgrade'])) { + $modform->grade = optional_param('maxgrade', 0); + if (!set_field('quiz', 'grade', $modform->grade, 'id', $modform->instance)) { + error('Could not set new maximal grade for quiz'); + } } - quiz_questiongrades_update($modform->grades, $modform->instance); } if (isset($_REQUEST['cat'])) { /// coming from category selection drop-down menu $modform->category = $cat; } - if(isset($_REQUEST['displayoptions'])) { - $modform->recurse = isset($_REQUEST['recurse']) ? 1 : 0; - $modform->showhidden = isset($_REQUEST['showhidden']); + if(isset($_REQUEST['recurse'])) { + $SESSION->quiz_recurse = optional_param('recurse', 0, PARAM_BOOL); + } + if(isset($_REQUEST['showbreaks'])) { + $SESSION->quiz_showbreaks = optional_param('showbreaks', 0, PARAM_BOOL); + } + if(isset($_REQUEST['showhidden'])) { + $SESSION->quiz_showhidden = optional_param('showhidden', 0, PARAM_BOOL); + } + +/// Delete any teacher preview attempts if the quiz has been modified + if (isset($_REQUEST['delete']) or isset($_REQUEST['repaginate']) or isset($_REQUEST['addrandom']) or isset($_REQUEST['addquestion']) or isset($_REQUEST['up']) or isset($_REQUEST['down']) or isset($_REQUEST['add'])) { + delete_records('quiz_attempts', 'preview', '1', 'quiz', $modform->id); } /// all commands have been dealt with, now print the page @@ -311,43 +345,54 @@ $category = quiz_get_default_category($course->id); $modform->category = $category->id; } - if (!isset($modform->recurse)) { - $modform->recurse = 1; + if (!isset($SESSION->quiz_recurse)) { + $SESSION->quiz_recurse = 1; } - if (!isset($modform->showhidden)) { - $modform->showhidden = false; + if (!isset($SESSION->quiz_showhidden)) { + $SESSION->quiz_showhidden = false; + } + if (!isset($SESSION->quiz_showbreaks)) { + $SESSION->quiz_showbreaks = ($CFG->quiz_questionsperpage < 2) ? 0 : 1; } $SESSION->modform = $modform; // Print basic page layout. - if (isset($modform->instance) and record_exists_sql("SELECT * FROM {$CFG->prefix}quiz_attempts WHERE quiz = '$modform->instance' AND NOT (userid = '$USER->id') LIMIT 1")){ - // one column layout with table of questions used in this quiz + if (isset($modform->instance) and record_exists_sql("SELECT * FROM {$CFG->prefix}quiz_attempts WHERE quiz = '$modform->instance' AND preview = '0' LIMIT 1")){ + // one column layout with table of questions used in this quiz + $strupdatemodule = isteacheredit($course->id) + ? update_module_button($modform->cmid, $course->id, get_string('modulename', 'quiz')) + : ""; print_header_simple($streditingquiz, '', "id\">$strquizzes". - " -> instance\">".format_string($modform->name,true)."". - " -> $streditingquiz"); + " -> instance\">".format_string($modform->name).''. + " -> $streditingquiz", "", "", + true, $strupdatemodule); + + $currenttab = 'edit'; + $quiz = &$modform; + include('tabs.php'); + print_simple_box_start("center"); - print_heading(format_string($modform->name)); - $attemptcount = count_records_select("quiz_attempts", "quiz = '$modform->instance' AND timefinish > 0"); + + $attemptcount = count_records('quiz_attempts', 'quiz', $modform->instance, 'preview', 0); $strviewallanswers = get_string("viewallanswers","quiz",$attemptcount); $strattemptsexist = get_string("attemptsexist","quiz"); - $usercount = count_records("quiz_grades", "quiz", "$modform->instance"); - $strusers = get_string("users"); + $usercount = count_records_select('quiz_attempts', "quiz = '$modform->id' AND preview = '0'", 'COUNT(DISTINCT userid)'); + $strusers = $course->students; if (! $cm = get_coursemodule_from_instance("quiz", $modform->instance, $course->id)) { error("Course Module ID was incorrect"); } - notify("$strattemptsexist
id\">$strviewallanswers ($usercount $strusers)"); + notify("$strattemptsexist
id\">$strviewallanswers ($usercount $strusers)"); - $sumgrades = quiz_print_question_list($modform->questions, $modform->grades, false, $modform->instance); + $sumgrades = quiz_print_question_list($modform, false, $SESSION->quiz_showbreaks); if (!set_field('quiz', 'sumgrades', $sumgrades, 'id', $modform->instance)) { error('Failed to set sumgrades'); } print_simple_box_end(); - print_continue('view.php?q='.$modform->instance); print_footer($course); exit; } @@ -362,39 +407,46 @@ } else { // two column layout with quiz info in left column + $strupdatemodule = isteacheredit($course->id) + ? update_module_button($modform->cmid, $course->id, get_string('modulename', 'quiz')) + : ""; print_header_simple($streditingquiz, '', "id\">$strquizzes". - " -> instance\">".format_string($modform->name,true)."". - " -> $streditingquiz"); + " -> instance\">".format_string($modform->name).''. + " -> $streditingquiz", + "", "", true, $strupdatemodule); + + $currenttab = 'edit'; + $quiz = &$modform; + include('tabs.php'); + echo '
: - quiz_timelimit, get_string("minutes","quiz"), 'quiz_timelimit'); + timelimit, get_string("minutes","quiz"), 'timelimit'); helpbutton("timelimit", get_string("quiztimer","quiz"), "quiz"); ?> - - quiz_fix_timelimit ? "checked" : "") ?> /> + + fix_timelimit ? "checked" : "") ?> />
- - quiz_fix_questionsperpage ? "checked" : "") ?> /> + + fix_questionsperpage ? "checked" : "") ?> />
- - quiz_fix_shufflequestions ? "checked" : "") ?> /> + + fix_shufflequestions ? "checked" : "") ?> />
- - quiz_fix_shuffleanswers ? "checked" : "") ?> /> + + fix_shuffleanswers ? "checked" : "") ?> />
- - quiz_fix_attempts ? "checked" : "") ?> /> + + fix_attempts ? "checked" : "") ?> />
- - quiz_fix_attemptonlast ? "checked" : "") ?> /> + + fix_attemptonlast ? "checked" : "") ?> />
: quiz_grademethod, "", "", ""); + choose_from_menu ($QUIZ_GRADE_METHOD, "grademethod", $form->grademethod, "", "", ""); helpbutton("grademethod", get_string("grademethod","quiz"), "quiz"); ?> - - quiz_fix_grademethod ? "checked" : "") ?> /> + + fix_grademethod ? "checked" : "") ?> /> +
: + quiz_optionflags & QUIZ_ADAPTIVE, ""); + helpbutton("adaptive", get_string("adaptive","quiz"), "quiz"); + ?> + + + fix_adaptive ? "checked" : "") ?> /> +
: + penaltyscheme", ""); + helpbutton("penaltyscheme", get_string("penaltyscheme","quiz"), "quiz"); + ?> + + + fix_penaltyscheme ? "checked" : "") ?> />
- - quiz_fix_decimalpoints ? "checked" : "") ?> /> + + fix_decimalpoints ? "checked" : "") ?> />
- - quiz_fix_showfeedback ? "checked" : "") ?> /> + + fix_showfeedback ? "checked" : "") ?> />
- - quiz_fix_showanswer ? "checked" : "") ?> /> + + fix_showanswer ? "checked" : "") ?> />
: - quiz_allowreview, "", "", ""); - helpbutton("review2", get_string("allowreview","quiz"), "quiz"); - ?> - - - quiz_fix_allowreview ? "checked" : "") ?> /> -
: - =1; $i--) { - $grades[$i] = $i; - } - $grades[0] = get_string("nograde"); - choose_from_menu ($grades, "quiz_maximumgrade", $CFG->quiz_maximumgrade, "", "", ""); - helpbutton("maxgrade", get_string("maximumgrade"), "quiz"); - ?> - - - quiz_fix_maximumgrade ? "checked" : "") ?> /> + + fix_review ? "checked" : "") ?> />
- - quiz_fix_popup ? "checked" : "") ?> /> + + fix_popup ? "checked" : "") ?> />
: - + - - quiz_fix_password ? "checked" : "") ?> /> + + fix_password ? "checked" : "") ?> />
: - + - - quiz_fix_subnet ? "checked" : "") ?> /> + + fix_subnet ? "checked" : "") ?> />
: + =1; $i--) { + $grades[$i] = $i; + } + $grades[0] = get_string("nograde"); + choose_from_menu ($grades, "maximumgrade", $form->maximumgrade, "", "", ""); + helpbutton("maxgrade", get_string("maximumgrade"), "quiz"); + ?> +
+ " />
'; echo ''; echo '
'; print_simple_box_start("center", "100%"); - print_heading(format_string($modform->name)); - $sumgrades = quiz_print_question_list($modform->questions, $modform->grades, true, $modform->instance); + + $sumgrades = quiz_print_question_list($modform, true, $SESSION->quiz_showbreaks); if (!set_field('quiz', 'sumgrades', $sumgrades, 'id', $modform->instance)) { error('Failed to set sumgrades'); } print_simple_box_end(); - print_continue('view.php?q='.$modform->instance); + echo ''; } // non-quiz-specific column print_simple_box_start("center", "100%"); // starts with category selection form - quiz_print_category_form($course, $modform->category, $modform->recurse, $modform->showhidden); + quiz_print_category_form($course, $modform->category, $SESSION->quiz_recurse, $SESSION->quiz_showhidden); print_simple_box_end(); print_spacer(5,1); // continues with list of questions print_simple_box_start("center", "100%"); quiz_print_cat_question_list($course, $modform->category, - isset($modform->instance), $modform->recurse, $page, $perpage, $modform->showhidden); + isset($modform->instance), $SESSION->quiz_recurse, $page, $perpage, $SESSION->quiz_showhidden); print_simple_box_end(); if (!isset($modform->instance)) { print_continue("index.php?id=$modform->course"); - } else { - print_continue('view.php?q='.$modform->instance); } echo '
'; diff --git a/mod/quiz/editlib.php b/mod/quiz/editlib.php new file mode 100644 index 00000000000..070d8d8188e --- /dev/null +++ b/mod/quiz/editlib.php @@ -0,0 +1,581 @@ +questions); + + // only do something if this question exists + if (!isset($questions[$id])) { + return false; + } + + $question = $questions[$id]; + unset($questions[$id]); + // If we deleted the question at the top and it was followed by + // a page break then delete page break as well + if ($id == 0 and $questions[1] == 0) { + unset($questions[1]); + } + // if what we deleted was not a page break but a question then also + // delete associated grades, instances, attempts, states + if ($question != 0) { + unset($modform->grades[$question]); + // Delete all states associated with all attempts for this question in the quiz. + if ($attempts = get_records('quiz_attempts', 'quiz', $modform->instance)) { + foreach ($attempts as $attempt) { + delete_records('quiz_states', 'question', $question, 'attempt', $attempt->id); + delete_records('quiz_newest_states', 'questionid', $question, 'attemptid', $attempt->id); + } + } + // Delete all instances of the question in the quiz (there + // should only be one) + delete_records('quiz_question_instances', 'quiz', + $modform->instance, 'question', $question); + } + $modform->questions = implode(",", $questions); + // Avoid duplicate page breaks + $modform->questions = str_replace(',0,0', ',0', $modform->questions); + // save new questionlist in database + if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { + error('Could not save question list'); + } +} + + +/** +* Add a question to a quiz +* +* Adds a question to a quiz by updating $modform as well as the +* quiz and quiz_question_instances tables. It also adds a page break +* if required. +* @return boolean false if the question was already in the quiz +* @param int $id The id of the question to be added +* @param object $modform The extended quiz object as used by edit.php +* This is updated by this function +*/ +function quiz_add_quiz_question($id, &$modform) { + $questions = explode(",", $modform->questions); + + if (in_array($id, $questions)) { + return false; + } + + // remove ending page break if it is not needed + if ($breaks = array_keys($questions, 0)) { + // determine location of the last two page breaks + $end = end($breaks); + $last = prev($breaks); + $last = $last ? $last : -1; + if (!$modform->questionsperpage or (($end - $last -1) < $modform->questionsperpage)) { + array_pop($questions); + } + } + // add question + $questions[] = $id; + // add ending page break + $questions[] = 0; + + // Save new questionslist in database + $modform->questions = implode(",", $questions); + if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->id)) { + error('Could not save question list'); + } + + // update question grades + $questionrecord = get_record("quiz_questions", "id", $id); + if (!empty($questionrecord->defaultgrade)) { + $modform->grades[$id] = $questionrecord->defaultgrade; + } else if ($questionrecord->qtype == DESCRIPTION){ + $modform->grades[$id] = 0; + } else { + $modform->grades[$id] = 1; + } + quiz_update_question_instance($modform->grades[$id], $id, $modform->instance); + return true; +} + +/** +* Save changes to question instance +* +* Saves changes to the question grades in the quiz_question_instances table. +* It does not update 'sumgrades' in the quiz table. +* @return boolean Indicates success or failure. +* @param integer grade The maximal grade for the question +* @param integer $questionid The id of the question +* @param integer $quizid The id of the quiz to update / add the instances for. +*/ +function quiz_update_question_instance($grade, $questionid, $quizid) { + if ($instance = get_record("quiz_question_instances", "quiz", $quizid, 'question', $questionid)) { + $instance->grade = $grade; + return update_record('quiz_question_instances', $instance); + } else { + unset($instance); + $instance->quiz = $quizid; + $instance->question = $questionid; + $instance->grade = $grade; + return insert_record("quiz_question_instances", $instance); + } +} + +/** +* Prints a list of quiz questions in a small layout form with knobs +* +* @return int sum of maximum grades +* @param object $quiz This is not the standard quiz object used elsewhere but +* it contains the quiz layout in $quiz->questions and the grades in +* $quiz->grades +* @param boolean $allowdelete Indicates whether the delete icons should be displayed +* @param boolean $showbreaks Indicates whether the page breaks should be displayed +*/ +function quiz_print_question_list($quiz, $allowdelete=true, $showbreaks=true) { + global $USER, $CFG, $QUIZ_QTYPES; + + $strorder = get_string("order"); + $strquestionname = get_string("questionname", "quiz"); + $strgrade = get_string("grade"); + $strremove = get_string('remove', 'quiz'); + $stredit = get_string("edit"); + $straction = get_string("action"); + $strmoveup = get_string("moveup"); + $strmovedown = get_string("movedown"); + $strsavegrades = get_string("savegrades", "quiz"); + $strtype = get_string("type", "quiz"); + $strpreview = get_string("preview", "quiz"); + + if (!$quiz->questions) { + echo "

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

"; + return; + } + + $order = explode(",", $quiz->questions); + + if (!$questions = get_records_sql("SELECT q.*,c.course + FROM {$CFG->prefix}quiz_questions q, + {$CFG->prefix}quiz_categories c + WHERE q.id in ($quiz->questions) + AND q.category = c.id")) { + echo "

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

"; + return; + } + + $count = 0; + $qno = 1; + $sumgrade = 0; + $total = count($order); + echo ""; + echo "sesskey\" />"; + + print_simple_box_start('center', '100%', '#ffffff', 0); + echo "\n"; + echo "\n"; + foreach ($order as $qnum) { + + if ($qnum == 0) { // This is a page break + if ($showbreaks) { + echo ''; + echo ''; + echo ''; + } + $count++; + continue; + } + if (empty($questions[$qnum])) { + continue; + } + $question = $questions[$qnum]; + $canedit = isteacheredit($question->course); + + echo ""; + echo ""; + echo ""; + + if (!$quiz->shufflequestions) { + // Print and increment question number + echo ''; + $qno += $question->length; + } else { + echo ''; + } + + echo ""; + echo ""; + echo '"; + $count++; + $sumgrade += $quiz->grades[$qnum]; + } + + echo "\n"; + + echo "\n"; + + echo "\n"; + + echo "
$strorder$strquestionname$strtype$strgrade$straction
 '; + echo ''; + echo ''; + echo '

Page break
'; + if ($count != 1) { + echo "sesskey\">\"$strmoveup\""; + } + echo ' '; + if ($count < $total-1) { + echo "sesskey\">\"$strmovedown\""; + + echo "sesskey\"> + \"$strremove\""; + } + echo '
 
"; + if ($count != 0) { + echo "sesskey\">\"$strmoveup\""; + } + echo ""; + if ($count != $total-2) { + echo "sesskey\">\"$strmovedown\""; + } + echo "'.($question->length ? $qno : ' ').' $question->name"; + quiz_print_question_icon($question, $canedit); + echo "'; + if ($question->qtype == DESCRIPTION) { + echo " \n"; + } else { + echo ''; + } + echo ''; + + if ($canedit) { + $context = $quiz->id ? '&contextquiz='.$quiz->id : ''; + $quiz_id = $quiz->id ? '&quizid=' . $quiz->id : ''; + echo " + \"$strpreview\""; + echo " + \"$stredit\""; + if ($allowdelete) { + echo "sesskey\"> + \"$strremove\""; + } + } + echo "
\n"; + print_string('total'); + echo ": \n"; + echo "$sumgrade"; + echo " \n
\n"; + print_string('maximumgrade'); + echo ": \n"; + echo ''; + echo ''; + helpbutton("maxgrade", get_string("maximumgrade"), "quiz"); + echo "
\n"; + + echo "\n"; + echo "\n"; + echo " \n
\n"; + print_simple_box_end(); + echo "
\n"; + +/// Form to choose to show pagebreaks and to repaginate quiz + echo '
'; + echo ''; + echo ''; + print_string('showbreaks', 'quiz'); + echo ' '; + + if ($showbreaks) { + $perpage= array(); + for ($i=0; $i<=50; ++$i) { + $perpage[$i] = $i; + } + $perpage[0] = get_string('allinone', 'quiz'); + echo '
'; + echo ''; + print_string('repaginate', 'quiz', + choose_from_menu($perpage, 'questionsperpage', $quiz->questionsperpage, '', '', '', true)); + echo ' '; + } + + echo '
'; + + return $sumgrade; +} + +function quiz_print_category_form($course, $current, $recurse=1, $showhidden=false) { +/// Prints a form to choose categories + +/// Make sure the default category exists for this course + if (!$categories = get_records("quiz_categories", "course", $course->id, "id ASC")) { + if (!$category = quiz_get_default_category($course->id)) { + notify("Error creating a default category!"); + } + } + +/// Get all the existing categories now + if (!$categories = get_records_select("quiz_categories", "course = '{$course->id}' OR publish = '1'", "parent, sortorder, name ASC")) { + notify("Could not find any question categories!"); + return false; // Something is really wrong + } + $categories = add_indented_names($categories); + foreach ($categories as $key => $category) { + if ($catcourse = get_record("course", "id", $category->course)) { + if ($category->publish && $category->course != $course->id) { + $category->indentedname .= " ($catcourse->shortname)"; + } + $catmenu[$category->id] = $category->indentedname; + } + } + $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, "", "", "", false, "self"); + echo ""; + echo "
"; + echo "id\" />"; + echo ""; + echo "
"; + echo '
'; + echo '
'; + echo '
'; + echo ''; + echo ''; + print_string('recurse', 'quiz'); + // hide-feature + echo '
'; + echo ''; + echo ''; + print_string('showhidden', 'quiz'); + echo '
'; + echo ' '; + echo '
'; +} + + +/** +* Prints the table of questions in a category with interactions +* +* @param object $course The course object +* @param int $categoryid The id of the question category to be displayed +* @param boolean $quizselected True if we are in the context of a particular quiz +* @param int $recurse This is 1 if subcategories should be included, 0 otherwise +* @param int $page The number of the page to be displayed +* @param int $perpage Number of questions to show per page +* @param boolean $showhidden True if also hidden questions should be displayed +*/ +function quiz_print_cat_question_list($course, $categoryid, $quizselected=true, + $recurse=1, $page, $perpage, $showhidden=false) { + global $QUIZ_QUESTION_TYPE, $USER; + + $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"); + $strselectnone = get_string("selectnone", "quiz"); + $strcreatenewquestion = get_string("createnewquestion", "quiz"); + $strquestionname = get_string("questionname", "quiz"); + $strdelete = get_string("delete"); + $stredit = get_string("edit"); + $straction = get_string("action"); + $strrestore = get_string('restore'); + + $straddtoquiz = get_string("addtoquiz", "quiz"); + $strtype = get_string("type", "quiz"); + $strcreatemultiple = get_string("createmultiple", "quiz"); + $strpreview = get_string("preview","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 ''; + } + else { + echo ''; + } + + echo ''; + + echo '
$strcreatenewquestion:'; + popup_form ("question.php?category=$category->id&qtype=", $QUIZ_QUESTION_TYPE, "addquestion", + "", "choose", "", "", false, "self"); + echo ''; + helpbutton("questiontypes", $strcreatenewquestion, "quiz"); + echo '
'; + print_string("publishedit","quiz"); + echo '
'; + if (isteacheredit($category->course)) { + echo ''.$strimportquestions.''; + helpbutton("import", $strimportquestions, "quiz"); + echo ' | '; + } + echo ''.$strexportquestions.''; + helpbutton("export", $strexportquestions, "quiz"); + echo '
'; + + echo '
'; + + $categorylist = ($recurse) ? quiz_categorylist($category->id) : $category->id; + + // hide-feature + $showhidden = $showhidden ? '' : " AND hidden = '0'"; + if (!$questions = get_records_select('quiz_questions', "category IN ($categorylist) AND parent = '0' $showhidden", 'qtype, name ASC', '*', $page*$perpage, $perpage)) { + echo "

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

"; + return; + } + + $canedit = isteacheredit($category->course); + + echo '
'; + echo ''; + print_simple_box_start('center', '100%', '#ffffff', 0); + echo ''; + if ($canedit) { + echo ""; + } + echo " + "; + echo "\n"; + foreach ($questions as $question) { + if ($question->qtype == RANDOM) { + //continue; + } + echo "\n"; + if ($canedit) { + echo "\n"; + } + if ($question->hidden) { + echo '\n"; + } else { + echo "\n"; + } + echo "\n"; + echo "\n"; + } + echo "
$straction$strquestionname$strtype
\n"; + if ($quizselected) { + echo "id&sesskey=$USER->sesskey\">\"$straddtoquiz\" "; + } + echo "id','$strpreview','scrollbars=yes,resizable=yes,width=700,height=480', false)\">\"$strpreview\" "; + echo "id\">\"$stredit\" "; + // hide-feature + if($question->hidden) { + echo "id&hide=0&sesskey=$USER->sesskey\">\"$strrestore\""; + } else { + echo "id&delete=$question->id\">\"$strdelete\""; + } + echo " id\" value=\"1\" />"; + echo "'.$question->name."".$question->name."\n"; + quiz_print_question_icon($question, $canedit); + echo "
\n"; + print_simple_box_end(); + + echo '
'; + echo ''.$strselectall.' /'. + ' '.$strselectnone.''. + ''.get_string('withselected', 'quiz').':
'; + if ($quizselected) { + echo "\n"; + echo ''; + } + echo '\n"; + echo '\n"; + quiz_category_select_menu($course->id, false, true, $category->id); + echo "
"; + + $numquestions = count($questions); + + print_paging_bar($numquestions, $page, $perpage, + "edit.php?perpage=$perpage&"); + + if ($quizselected) { + for ($i=1;$i<=10; $i++) { + $randomcount[$i] = $i; + } + echo '
'; + print_string('addrandom', 'quiz', + choose_from_menu($randomcount, 'randomcount', '1', '', '', '', true)); + echo ''; + echo "id\" />"; + echo ' '; + helpbutton('random', get_string('random', 'quiz'), 'quiz'); + } + + echo "
\n"; +} + +?> diff --git a/mod/quiz/index.php b/mod/quiz/index.php index 69b572a3986..3752bccbd4b 100644 --- a/mod/quiz/index.php +++ b/mod/quiz/index.php @@ -1,6 +1,12 @@ students; if (isteacher($course->id)) { $gradecol = $strattempts; } else { $gradecol = $strbestgrade; } - + if ($course->format == "weeks") { $table->head = array ($strweek, $strname, $strquizcloses, $gradecol); $table->align = array ("center", "left", "left", "left"); - $table->size = array (10, "*", "*", "*"); + $table->size = array (10, "", "", ""); } else if ($course->format == "topics") { $table->head = array ($strtopic, $strname, $strquizcloses, $gradecol); $table->align = array ("center", "left", "left", "left"); - $table->size = array (10, "*", "*", "*"); + $table->size = array (10, "", "", ""); } else { $table->head = array ($strname, $strquizcloses, $gradecol); $table->align = array ("left", "left", "left"); - $table->size = array ("*", "*", "*"); + $table->size = array ("", "", ""); } $currentsection = ""; foreach ($quizzes as $quiz) { - if (!$quiz->visible) { + if (!$quiz->visible) { //Show dimmed if the mod is hidden $link = "coursemodule\">".format_string($quiz->name,true).""; } else { @@ -98,15 +104,15 @@ $closequiz = userdate($quiz->timeclose); if (isteacher($course->id)) { - if ($usercount = count_records('quiz_grades', 'quiz', $quiz->id)) { - $attemptcount = count_records_select('quiz_attempts', 'quiz = '.$quiz->id.' AND timefinish > 0'); - $strviewallanswers = get_string('viewallanswers', 'quiz', $attemptcount); - $gradecol = "id\">$strviewallanswers ($usercount $strusers)"; + if ($usercount = count_records_select('quiz_attempts', "quiz = '$quiz->id' AND preview = '0'", 'COUNT(DISTINCT userid)')) { + $attemptcount = count_records('quiz_attempts', 'quiz', $quiz->id, 'preview', 0); + $strviewallreports = get_string('viewallreports', 'quiz', $attemptcount); + $gradecol = "id\">$strviewallreports ($usercount $strusers)"; } else { $answercount = 0; $gradecol = ""; } - } else { + } else { if ($bestgrade === NULL || $quiz->grade == 0) { $gradecol = ""; } else { diff --git a/mod/quiz/jsclock.php b/mod/quiz/jsclock.php index 07c657f2237..44623b6eef0 100644 --- a/mod/quiz/jsclock.php +++ b/mod/quiz/jsclock.php @@ -1,67 +1,67 @@ - - - + + + diff --git a/mod/quiz/jstimer.php b/mod/quiz/jstimer.php index 2dc2ffa2ea1..031d38cf705 100644 --- a/mod/quiz/jstimer.php +++ b/mod/quiz/jstimer.php @@ -8,11 +8,6 @@ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); - diff --git a/mod/quiz/lang/en/help/quiz/adaptive.html b/mod/quiz/lang/en/help/quiz/adaptive.html new file mode 100644 index 00000000000..d4b967e1adc --- /dev/null +++ b/mod/quiz/lang/en/help/quiz/adaptive.html @@ -0,0 +1,14 @@ +

Allow adaptive questions

+ +

If you choose Yes for this option then an additional button is shown for each question. +If the student presses this button then the answer to that particular question is submitted to be scored and the mark achieved +is displayed to the student. If the question is an adaptive question then it is displayed in its new state that takes the +student's answer into account and will in many cases ask the student for another input. In the simplest adaptive questions +this new state may differ only in the feedback text and prompt the student to try again; in more +complicated question also the question text and even the interaction elements can change. +

+ +

For most adaptive questions the student will be able to interact with a single question repeatedly until he gets the right answer. +However a penalty will usually be subtracted from the students score for each wrong attempt. This is under the teacher's control. +The question may also close itself to new attempts. For example true/false questions will not allow +the student to answer again after the first attempt because the answer would be obvious.

\ No newline at end of file diff --git a/mod/quiz/lang/en/help/quiz/penalty.html b/mod/quiz/lang/en/help/quiz/penalty.html new file mode 100644 index 00000000000..9fa3d9a7884 --- /dev/null +++ b/mod/quiz/lang/en/help/quiz/penalty.html @@ -0,0 +1,3 @@ +

Penalty

+ +

You can specify what fraction of the achieved score should be subtracted for each wrong attempt. This is only relevant if the quiz is run in adaptive mode so that the student is allowed to have several attempts at the question. The penalty should be a number between 0 and 1. A penalty of 1 means that the student has to get the answer right on the first attempt to get any credit for it at all.

\ No newline at end of file diff --git a/mod/quiz/lang/en/help/quiz/penaltyscheme.html b/mod/quiz/lang/en/help/quiz/penaltyscheme.html new file mode 100644 index 00000000000..beab088d1b1 --- /dev/null +++ b/mod/quiz/lang/en/help/quiz/penaltyscheme.html @@ -0,0 +1,6 @@ +

Penalty scheme

+ +

If a question allows a student to have repeated attempts then the question may impose a penalty for wrong attempts. +This penalty is a number between 0 and 1 and denotes the fraction of the final score that should be deducted as a penalty. +If the student makes several wrong attempts then the penalties for the individual attempts are combined before +being used to calculate the final grade.

\ No newline at end of file diff --git a/mod/quiz/lang/en/help/quiz/questionsperpage.html b/mod/quiz/lang/en/help/quiz/questionsperpage.html new file mode 100644 index 00000000000..beef32112fc --- /dev/null +++ b/mod/quiz/lang/en/help/quiz/questionsperpage.html @@ -0,0 +1,4 @@ +

Questions per page

+ +

For longer quizzes it makes sense to stretch the quiz over several pages by limiting the number of questions per page. When adding questions to the quiz page breaks will automatically be inserted according to the setting you choose here. However you will also be able to move page breaks around by hand later on the editing page. +

diff --git a/mod/quiz/lang/en/help/quiz/rqp.html b/mod/quiz/lang/en/help/quiz/rqp.html new file mode 100644 index 00000000000..fb9f822995f --- /dev/null +++ b/mod/quiz/lang/en/help/quiz/rqp.html @@ -0,0 +1,5 @@ +

Remote Questions (RQP)

+ + +

The Remote Question Protocol may be used to include questions in a quiz with all processing occurring on a remote server. Questions may be included in any format for which a server is available without any change to Moodle.

+ diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php new file mode 100644 index 00000000000..56c927e51cc --- /dev/null +++ b/mod/quiz/lang/en/quiz.php @@ -0,0 +1,109 @@ +raw/$a->max.'; +$string['gradingdetailsnopenalty'] = 'Marks for this submission: $a->raw/$a->max. Counting all previous submissions this gives $a->cur/$a->max.'; +$string['gradingdetailszeropenalty'] = 'Marks for this submission: $a->raw/$a->max. You were not penalized for this submission. Counting all previous submissions this gives $a->cur/$a->max.'; +$string['gradingdetailspenalty'] = 'Marks for this submission: $a->raw/$a->max with a penalty of $a->penalty. Counting all previous submissions this gives $a->cur/$a->max.'; +$string['info'] = 'Info'; +$string['invalidsource'] = 'The source is not accepted as valid by the server $a->rendering_server.'; +$string['invalidsourcetype'] = 'Invalid source type.'; +$string['itemsource'] = 'Item Source'; +$string['itemsourceformat'] = 'Item Source Format'; +$string['itemrequirescloning'] = 'The item source supplied is for an item template but the server for the selected item type does not support cloning (or implicit cloning). Check that the correct item type was selected'; +$string['itemtype'] = 'Item Type'; +$string['itemtypes'] = 'Item Types'; +$string['lastanswer'] = 'Your last answer was'; +$string['managetypes'] = 'Manage question types and servers'; +$string['adaptive'] = 'Allow adaptive questions'; +$string['finishattempt'] = 'Submit all and finish'; +$string['markall'] = 'Submit page'; +$string['mark'] = 'Submit'; +$string['missingitemtypename'] = 'Missing name'; +$string['missingsource'] = 'Missing item source'; +$string['missingrenderingserver'] = 'Missing rendering server URL'; +$string['newattemptfail'] = 'Error: Could not start a new attempt at the quiz'; +$string['noattemptstoshow'] = 'There are no attempts to show'; +$string['noquestionsfound'] = 'No questions found'; +$string['notavailable'] = 'Sorry, this quiz is not available'; +$string['notavailabletostudents'] = 'Note: This quiz is not currently available to your students'; +$string['notimedependentitems'] = 'Time dependent items are not currently supported by the quiz module. As a work around, set a time limit for the whole quiz. Do you wish to choose a different item (or use the current item regardless)?'; +$string['notypes'] = 'No item types have been defined! The administrator must add some types and set the servers to be used to process them before RQP questions can be used'; +$string['notypeselected'] = 'No type selected'; +$string['numberabbr'] = '#'; +$string['outof'] = ' out of a maximum of '; +$string['penalty'] = 'Penalty'; +$string['penaltynone'] = 'None'; +$string['penaltymultiply'] = 'Multiplicative'; +$string['penaltysubtract'] = 'Additive'; +$string['penaltyscheme'] = 'Penalty scheme'; +$string['previewquiz'] = 'Preview quiz'; +$string['previous'] = 'Previous state'; +$string['readytosend'] = 'You are about to finalize this quiz attempt. Are you sure you want to continue?'; +$string['rendering'] = 'Rendering'; +$string['renderingserver'] = 'Rendering Server'; +$string['renderingserverconnectfailed'] = 'The rendering server $a failed to process an RQP request. Check that the URL is correct.'; +$string['renderingserverdoesnt'] = 'The rendering server $a does not support rendering!'; +$string['repaginate'] = 'Repaginate with $a questions per page'; +$string['replacementoptions'] = 'Replacement Options'; +$string['reports'] = 'Reports'; +$string['response'] = 'Response'; +$string['responses'] = 'Responses'; +$string['reviewimmediately'] = 'Immediately after the attempt'; +$string['reviewofattempt'] = 'Review of Attempt $a'; +$string['reviewopen'] = 'Later, while the quiz is still open'; +$string['reviewclosed'] = 'After the quiz is closed'; +$string['reviewoptions'] = 'Students may review'; +$string['reviewresponse'] = 'Review response'; +$string['rqp'] = 'Remote Question'; +$string['rqps'] = 'Remote Questions'; +$string['savenosubmit'] = 'Save without submitting'; +$string['scores'] = 'Scores'; +$string['servers'] = 'Servers'; +$string['showall'] = 'Show all questions on one page'; +$string['showbreaks'] = 'Show page breaks'; +$string['startagain'] = 'Start again'; +$string['startedon'] = 'Started on'; +$string['subnetnotice'] = 'This quiz has been locked so that it is only accessible from certain locations. Your computer is not on an allowed subnet. As teacher you are allowed to preview anyway.'; +$string['timestr'] = '%%H:%%M:%%S on %%d/%%m/%%y'; +$string['unfinished'] = 'open'; +$string['validate'] = 'Validate'; +$string['withselected'] = 'With selected'; +$string['wronguse'] = 'You can not use this page like that'; + +?> diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index 21ecf877693..ac4065af793 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -1,13 +1,70 @@ libdir.'/pagelib.php'); /// CONSTANTS /////////////////////////////////////////////////////////////////// + +/**#@+ +* Option flags for $quiz->optionflags +*/ +/** +* Whether the quiz is to be run in adaptive mode +*/ +define('QUIZ_ADAPTIVE', 1); + +/** +* Determines if when checking whether an answer should +* be checked against all previous ones to see if it is duplicate +*/ +define('QUIZ_IGNORE_DUPRESP', 2); + +/**#@-*/ + +/** +* If start and end date for the quiz are more than this many seconds apart +* they will be represented by two separate events in the calendar +*/ define("QUIZ_MAX_EVENT_LENGTH", "432000"); // 5 days maximum +/**#@+ +* The different review options are stored in the bits of $quiz->review +* These constants help to extract the options +*/ +/** +* The first 6 bits refer to the time immediately after the attempt +*/ +define('QUIZ_REVIEW_IMMEDIATELY', 64-1); +/** +* the next 6 bits refer to the time after the attempt but while the quiz is open +*/ +define('QUIZ_REVIEW_OPEN', 4096-64); +/** +* the final 6 bits refer to the time after the quiz closes +*/ +define('QUIZ_REVIEW_CLOSED', 262144-4096); + +// within each group of 6 bits we determine what should be shown +define('QUIZ_REVIEW_RESPONSES', 1+64+4096); // Show responses +define('QUIZ_REVIEW_SCORES', 2*4161); // Show scores +define('QUIZ_REVIEW_FEEDBACK', 4*4161); // Show feedback +define('QUIZ_REVIEW_ANSWERS', 8*4161); // Show correct answers +// Some handling of worked solutions is already in the code but not yet fully supported +// and not switched on in the user interface. +define('QUIZ_REVIEW_SOLUTIONS', 16*4161); // Show solutions +// the 6th bit is as yet unused +/**#@-*/ + /// FUNCTIONS /////////////////////////////////////////////////////////////////// function quiz_add_instance($quiz) { @@ -16,6 +73,8 @@ function quiz_add_instance($quiz) { /// will create a new instance and return the id number /// of the new instance. + quiz_process_options($quiz); + $quiz->created = time(); $quiz->timemodified = time(); $quiz->timeopen = make_timestamp($quiz->openyear, $quiz->openmonth, $quiz->openday, @@ -80,6 +139,8 @@ function quiz_update_instance($quiz) { /// (defined by the form in mod.html or edit.php) this function /// will update an existing instance with new data. + quiz_process_options($quiz); + $quiz->timemodified = time(); if (isset($quiz->openyear)) { // this would not be set if we come from edit.php $quiz->timeopen = make_timestamp($quiz->openyear, $quiz->openmonth, $quiz->openday, @@ -93,6 +154,9 @@ function quiz_update_instance($quiz) { return false; // some error occurred } + // Delete any preview attempts + delete_records('quiz_attempts', 'preview', '1', 'quiz', $quiz->id); + if (isset($quiz->optionsettingspref)) { set_user_preference('quiz_optionsettingspref', $quiz->optionsettingspref); } @@ -150,7 +214,7 @@ function quiz_delete_instance($id) { if ($attempts = get_records("quiz_attempts", "quiz", "$quiz->id")) { foreach ($attempts as $attempt) { - if (! delete_records("quiz_responses", "attempt", "$attempt->id")) { + if (! delete_records("quiz_states", "attempt", "$attempt->id")) { $result = false; } } @@ -164,7 +228,7 @@ function quiz_delete_instance($id) { $result = false; } - if (! delete_records("quiz_question_grades", "quiz", "$quiz->id")) { + if (! delete_records("quiz_question_instances", "quiz", "$quiz->id")) { $result = false; } @@ -235,13 +299,32 @@ function quiz_user_outline($course, $user, $mod, $quiz) { } + 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. + if ($attempts = get_records_select('quiz_attempts', "userid='$user->id' AND quiz='$quiz->id'", 'attempt ASC')) { + if ($quiz->grade != 0 && $grade = get_record('quiz_grades', 'userid', $user->id, 'quiz', $quiz->id)) { + echo get_string('grade').': '.format_float($grade->grade, $quiz->decimalpoints).'/'.$quiz->grade.'
'; + } + foreach ($attempts as $attempt) { + echo get_string('attempt', 'quiz').' '.$attempt->attempt.': '; + if ($attempt->timefinish == 0) { + print_string('unfinished'); + } else { + echo format_float($attempt->sumgrades, $quiz->decimalpoints).'/'.$quiz->sumgrades; + } + echo ' - '.userdate($attempt->timemodified).'
'; + } + } else { + print_string('noattempts', 'quiz'); + } + 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 @@ -481,4 +564,93 @@ function quiz_print_recent_mod_activity($activity, $course, $detail=false) { return; } +/** +* Pre-process the options form data +* +* Encode the review options from the setup form into the bits of $form->review +* and other options into $form->optionflags +* The form data is passed by reference and modified by this function +* @return integer +* @param object $form The variables set on the form. +*/ +function quiz_process_options(&$form) { + $optionflags = 0; + $review = 0; + + if (!empty($form->adaptive)) { + $optionflags += QUIZ_ADAPTIVE; + } + + if (isset($form->responsesimmediately)) { + $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY); + unset($form->responsesimmediately); + } + if (isset($form->responsesopen)) { + $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN); + unset($form->responsesopen); + } + if (isset($form->responsesclosed)) { + $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED); + unset($form->responsesclosed); + } + + if (isset($form->scoreimmediately)) { + $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY); + unset($form->scoreimmediately); + } + if (isset($form->scoreopen)) { + $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN); + unset($form->scoreopen); + } + if (isset($form->scoreclosed)) { + $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED); + unset($form->scoreclosed); + } + + if (isset($form->feedbackimmediately)) { + $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY); + unset($form->feedbackimmediately); + } + if (isset($form->feedbackopen)) { + $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN); + unset($form->feedbackopen); + } + if (isset($form->feedbackclosed)) { + $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED); + unset($form->feedbackclosed); + } + + if (isset($form->answersimmediately)) { + $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY); + unset($form->answersimmediately); + } + if (isset($form->answersopen)) { + $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN); + unset($form->answersopen); + } + if (isset($form->answersclosed)) { + $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED); + unset($form->answersclosed); + } + + if (isset($form->solutionsimmediately)) { + $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY); + unset($form->solutionsimmediately); + } + if (isset($form->solutionsopen)) { + $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN); + unset($form->solutionsopen); + } + if (isset($form->solutionsclosed)) { + $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED); + unset($form->solutionsclosed); + } + + $form->review = $review; + $form->optionflags = $optionflags; + +} + + + ?> diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 1846ca53260..1e0d926ab64 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -1,20 +1,64 @@ dirroot/mod/quiz/lib.php"); /// 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")); +/**#@+ +* Options determining how the grades from individual attempts are combined to give +* the overall grade for a user +*/ +define("QUIZ_GRADEHIGHEST", "1"); +define("QUIZ_GRADEAVERAGE", "2"); +define("QUIZ_ATTEMPTFIRST", "3"); +define("QUIZ_ATTEMPTLAST", "4"); +$QUIZ_GRADE_METHOD = array ( QUIZ_GRADEHIGHEST => get_string("gradehighest", "quiz"), + QUIZ_GRADEAVERAGE => get_string("gradeaverage", "quiz"), + QUIZ_ATTEMPTFIRST => get_string("attemptfirst", "quiz"), + QUIZ_ATTEMPTLAST => get_string("attemptlast", "quiz")); +/**#@-*/ +/**#@+ +* The different types of events that can create question states +*/ +define('QUIZ_EVENTOPEN', '0'); +define('QUIZ_EVENTNAVIGATE', '1'); +define('QUIZ_EVENTSAVE', '2'); +define('QUIZ_EVENTGRADE', '3'); +define('QUIZ_EVENTDUPLICATEGRADE', '4'); +define('QUIZ_EVENTVALIDATE', '5'); +define('QUIZ_EVENTCLOSE', '6'); +/**#@-*/ + +/**#@+ +* The different penalty schemes +*/ +define('QUIZ_PENALTYNONE', '0'); +define('QUIZ_PENALTYMULTIPLY', '1'); +define('QUIZ_PENALTYSUBTRACT', '2'); +/**#@-*/ + +/**#@+ +* The defined question types +* +* @todo It would be nicer to have a fully automatic plug-in system +*/ define("SHORTANSWER", "1"); define("TRUEFALSE", "2"); define("MULTICHOICE", "3"); @@ -25,10 +69,18 @@ define("DESCRIPTION", "7"); define("NUMERICAL", "8"); define("MULTIANSWER", "9"); define("CALCULATED", "10"); +define("RQP", "11"); +/**#@-*/ -// The $QUIZ_QUESTION_TYPE array holds the names of all the question types that the user should -// be able to create directly. Some internal question types like random questions are excluded. -// The complete list of question types can be found in $QUIZ_QTYPES. +/** +* Array of question types names translated to the user's language +* +* The $QUIZ_QUESTION_TYPE array holds the names of all the question types that the user should +* be able to create directly. Some internal question types like random questions are excluded. +* The complete list of question types can be found in {@link $QUIZ_QTYPES}. +*/ +// Note: Commented-out questiontypes are disabled, because they have not been +// upgraded to the new code yet. $QUIZ_QUESTION_TYPE = array ( MULTICHOICE => get_string("multichoice", "quiz"), TRUEFALSE => get_string("truefalse", "quiz"), SHORTANSWER => get_string("shortanswer", "quiz"), @@ -37,7 +89,8 @@ $QUIZ_QUESTION_TYPE = array ( MULTICHOICE => get_string("multichoice", "quiz") MATCH => get_string("match", "quiz"), DESCRIPTION => get_string("description", "quiz"), RANDOMSAMATCH => get_string("randomsamatch", "quiz"), - MULTIANSWER => get_string("multianswer", "quiz") + MULTIANSWER => get_string("multianswer", "quiz"), + RQP => get_string("rqp", "quiz") ); @@ -48,38 +101,151 @@ define("QUIZ_MAX_NUMBER_ANSWERS", "10"); define("QUIZ_CATEGORIES_SORTORDER", "999"); -define('QUIZ_REVIEW_AFTER', 1); -define('QUIZ_REVIEW_BEFORE', 2); - +/** +* Array holding question type objects +*/ $QUIZ_QTYPES= array(); -/// QUIZ_QTYPES INITIATION ////////////////// +/// Objects used by the quiz module ///////// + +/** +* Holds run-time information about a question used in a particular quiz +* +* In addition to the data from the quiz_questions table a question object +* has extra variables for data from the quiz_question_instances table +* and further run-time information. +*/ +class question extends object { + + /** + * The question id number + * @var integer int(10) + */ + var $id; + + /** + * The id of the question category in the quiz_categories table + * @var integer int(10) + */ + var $category; + + /** + * The name given to the question by the teacher. + * This name is not shown to the student + * @var string varchar(255) + */ + var $name; + + /** + * The text of the question shown to the student + * @var string text + */ + var $questiontext; + + /** + * The text format for the question text. + * Formats are defined at the top of {@link weblib.php} + * @var integer tinyint(2) + */ + var $questiontextformat; + + /** + * URL to the question image + * @var string varchar(255) + */ + var $image; + + /** + * The default maximal grade for the question. + * This can be changed by the teacher when putting the question + * into a particular quiz to {@link maxgrade} + * @var integer int(10) + */ + var $defaultgrade; + + /** + * The question type. + * @var integer smallint(6) + */ + var $qtype; + + /** + * A globally unique identifier used for backup for example + * @var string varchar(255) + */ + var $stamp; + + /** + * A version number. + * This is increased each time a question is modified + * Since the introduction of the quiz_question_versions table + * this field is no longer of any use + * @var int(10) + */ + var $version; + + /** + * A flag that determines whether the question should be shown + * in the list of questions on the right hand side of the + * quiz editing page. + * Usually hidden questions are old versions of questions + * that are kept around because there are already student responses + * for them + * @var integer int(1) + */ + var $hidden; + +} + + + +/// Question type class ////////////////////////////////////////////// + class quiz_default_questiontype { + /** + * Name of the question type + * + * The name returned should coincide with the name of the directory + * in which this questiontype is located + * @ return string + */ function name() { return 'default'; } + /** + * Checks whether a given file is used by a particular question + * + * This is used by {@see quizfile.php} to determine whether a file + * should be served to the user + * @return boolean + * @param question $question + * @param string $relativefilepath + */ function uses_quizfile($question, $relativefilepath) { // The default does only check whether the file is used as image: return $question->image == $relativefilepath; } + /** + * Saves options set by the teacher for a question + * + * Given some question info and some data about the answers + * this function parses, organises and saves the question + * It is used by {@link question.php} through {@link save_question()} when + * saving new data from a form, and also by {@link import.php} when + * importing questions + * @return object $result->error or $result->noticeyesno or $result->notice + * @param object $question + */ 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: @@ -87,6 +253,16 @@ class quiz_default_questiontype { return $result; } + /** + * Saves or updates a question after editing by a teacher + * + * This is used by {@link question.php} to save the data from the + * question editing form. + * @return object A {@link question} object + * @param object $question The question object which should be updated + * @param object $form The form submitted by the teacher + * @param object $course The course we are in + */ function save_question($question, $form, $course) { // This default implementation is suitable for most // question types. @@ -96,6 +272,9 @@ class quiz_default_questiontype { $question->name = trim($form->name); $question->questiontext = trim($form->questiontext); $question->questiontextformat = $form->questiontextformat; + $question->parent = isset($form->parent)? $form->parent : 0; + $question->length = $this->actual_number_of_questions($question); + $question->penalty = isset($form->penalty) ? $form->penalty : 0; if (empty($form->image)) { $question->image = ""; @@ -110,6 +289,10 @@ class quiz_default_questiontype { } } + if ($question->penalty > 1 or $question->penalty < 0) { + $question->errors['penalty'] = get_string('invalidpenalty', 'quiz'); + } + if (isset($form->defaultgrade)) { $question->defaultgrade = $form->defaultgrade; } @@ -152,217 +335,709 @@ class quiz_default_questiontype { return $question; } - /// 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; + /** + * Loads the question type specific options for the question. + * + * This function should load any question type specific options for the + * question from the database into the question object. This information + * should be contained in the $question->options field. A questiontype is + * free, however, to decide on a internal structure of the options field. + * @return bool Indicates success or failure. + * @param object $question The question object for the question. This object + * should be updated to include the question type + * specific information (it is passed by reference). + */ + function get_question_options(&$question) { + if (!isset($question->options)) { + $question->options = new object; } + // The default implementation attaches all answers for this question + if (!$question->options->answers = get_records('quiz_answers', 'question', + $question->id)) { + notify('Error: Missing question answers!'); + return false; + } + return true; } - - 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, then this method returns - /// comma separated list of them... - - return false; + /** + * Returns the number of question numbers which are used by the question + * + * This function should return the number of question numbers to be assigned + * to the question. Most question types will have length one; they will be + * assigned one number. The DESCRIPTION type, however does not use up a + * number and so has a length of zero. Other question types may wish to + * handle a bundle of questions and hence return a number greater than one. + * @return integer The number of question numbers which should be + * assigned to the question. + * @param object $question The question whose length is to be determined. + * Question type specific information is included. + */ + function actual_number_of_questions($question) { + // By default, each question is given one number + return 1; } - 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); + /** + * Creates empty session and response information for the question + * + * This function is called to start a question session. Empty question type + * specific session data (if any) and empty response data should be added to the + * state object. Session data is any data which must persist throughout the + * quiz attempt possibly with updates as the user interacts with the + * question. This function should NOT create new entries in the database for + * the session; a call to the {@link save_session_and_responses} member will + * occur to do this. + * @return bool Indicates success or failure. + * @param object $question The question for which the session is to be + * created. Question type specific information is + * included. + * @param object $state The state to create the session for. Note that + * this will not have been saved in the database so + * there will be no id. This object should be updated + * to include the question type specific information + * (it is passed by reference). In particular, empty + * responses must be created in the ->responses + * field. + * @param object $quiz The quiz for which the session is to be started. + * Questions may wish to initialise the session in + * different ways depending on quiz settings. + * @param object $attempt The quiz attempt for which the session is to be + * started. Questions may wish to initialise the + * session in different ways depending on the user id + * or time available for the attempt. + */ + function create_session_and_responses(&$question, &$state, $quiz, $attempt) { + // The default implementation should work for the legacy question types. + // Most question types with only a single form field for the student's response + // will use the empty string '' as the index for that one response. This will + // automatically be stored in and restored from the answer field in the + // quiz_states table. + $state->responses = array('' => ''); + return true; } - function get_answers($question) { - // Returns the answers for the specified question - - // The default behaviour that signals that something is wrong - return false; + /** + * Restores the session data and most recent responses for the given state + * + * This function should load any session data associated with the question + * session in the given state into the state object. It should also load + * the responses given (or generated) for the given state into the + * ->responses member of the state object. + * + * Question types with only a single form field for the student's response + * will not need not restore the responses; the value of the answer + * field in the quiz_states table is restored to ->responses[''] + * before this function is called. Question types with more response fields + * should override this method and set the ->responses field to an + * associative array of responses. + * @return bool Indicates success or failure. + * @param object $question The question object for the question including any + * question type specific information. + * @param object $state The saved state to load the session for. This + * object should be updated to include the question + * type specific session information and responses + * (it is passed by reference). + */ + function restore_session_and_responses(&$question, &$state) { + // The default implementation does nothing (successfully) + return true; } - 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); + /** + * Saves the session data and responses for the question in the newly created + * state + * + * This function should save the question type specific session data from the + * state object. In particular for most question types it should also save the + * responses from the ->responses member of the state object. The question type + * non-specific data for the state has already been saved in the quiz_states + * table and the state object contains the corresponding id and + * sequence number which may be used to index a question type specific table. + * + * Question types with only a single form field for the student's response + * which is contained in ->responses[''] will not have to save this response, + * it will already have been saved to the answer field of the quiz_states table. + * Question types with more response fields should override this method and save + * the responses in their own database tables. + * @return bool Indicates success or failure. + * @param object $question The question object for the question including + * the question type specific information. + * @param object $state The state for which the question type specific + * data and responses should be saved. + */ + function save_session_and_responses(&$question, &$state) { + // The default implementation does nothing (successfully) + return true; } + /** + * This function is obsolete. It is only used during the database + * upgrade + */ function extract_response($rawresponse, $nameprefix) { - /// This function is very much the inverse of convert_to_response_answer_field - /// This function and convert_to_response_answer_field, 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 function is obsolete. It is only used during the database + /// upgrade to version 2005030100 to extract the responses from the + /// legacy answers field. Question types written after this date do not + /// need to implement this member. - /// Default behaviour that works for singlton response question types - /// like SHORTANSWER, NUMERICAL and TRUEFALSE + /// Returning a single value indicates that the value should remain + /// stored in the legacy answer field in the quiz_states table (was + /// quiz_responses). - return array($nameprefix => $rawresponse->answer); + /// Question types which implement their own response storage with + /// a question type specific table must return an associative array + /// of responses (without the name prefix) and in this case the update + /// script will call the save_session_and_responses member to save + /// the responses using the new mechanism. + + /// Default behaviour that works for singleton response question types + /// like SHORTANSWER, NUMERICAL and TRUEFALSE and legacy question types + /// which have not changed their response storage model + + return $rawresponse->answer; } - function print_question_number_and_grading_details - ($number, $grade, $actualgrade=false, $recentlyadded=false, $questionid=0, $courseid=0) { - - /// Print question number and grade: - - global $CFG; - - static $streditquestions, $strmarks, $strrecentlyaddedquestion; - - if (!isset($streditquestions)) { - $streditquestions = get_string('editquestions', 'quiz'); - $strmarks = get_string('marks', 'quiz'); - $strrecentlyaddedquestion = get_string('recentlyaddedquestion', 'quiz'); - } - - echo '
' . $number . ''; - if ($questionid and isteacher($courseid)) { - echo '
( '; - link_to_popup_window ($CFG->wwwroot.'//mod/quiz/question.php?id='.$questionid, - 'editquestion', '#'.$questionid, 450, 550, $streditquestions); - echo ')'; - } - echo '
'; - - if (false !== $grade) { - //echo '

'; - echo '

'; - if (false !== $actualgrade) { - echo "$strmarks: $actualgrade/$grade
"; - } else { - echo "$grade $strmarks"; + /** + * Return a value or array of values which will give full marks if graded as + * the $state->responses field + * + * The correct answer to the question, or an example of a correct answer if + * there are many correct answers, is found and the value of the ->responses + * member of the state object which corresponds to that answer is returned. + * @return mixed An array of values giving the responses corresponding + * to the (or a) correct answer to the question. If the + * question type overrides the {@link grade_responses} + * member and does not wish to provide this information + * null can be returned. + * @param object $question The question for which the correct answer is to + * be retrieved. Question type specific information is + * available. + * @param object $state The state object that corresponds to the question, + * for which a correct answer is needed. Question + * type specific information is included. + */ + function get_correct_responses(&$question, &$state) { + /* The default implementation returns the response for the first answer + that gives full marks. */ + foreach ($question->options->answers as $answer) { + if (((int) $answer->fraction) === 1) { + return array('' => $answer->answer); } } - print_spacer(1,100); + return null; + } - /// Print possible recently-added information: + /** + * Prints the question including the number, grading details, content, + * feedback and interactions + * + * This function should print the question including the question number, + * grading details, content for the question, any feedback for the previously + * submitted responses and the interactions. The default implementation calls + * various other methods to print each of these parts and most question types + * will just override those methods. + * @todo Use CSS stylesheet + * @param object $question The question to be rendered. Question type + * specific information is included. The + * maximum possible grade is in ->maxgrade. The name + * prefix for any named elements is in ->name_prefix. + * @param object $state The state to render the question in. The grading + * information is in ->grade, ->raw_grade and + * ->penalty. The currently responses are in + * ->responses. This will be an associative array + * (except in the case of no responses submitted when + * this will be an empty string rather than an empty + * array; this might occur when radio buttons are the + * only interactions for a question and none are + * selected for example). The last graded state is in + * ->last_graded (hence the most recently graded + * responses are in ->last_graded->responses). The + * question type specific information is also + * included. + * @param integer $number The number for this question. This is passed by + * reference and should be increased by this method + * to the number of the next question. + * @param object $quiz The quiz to which the question belongs. The + * question will likely be rendered differently + * depending on the quiz settings. + * @param object $options An object describing the rendering options. + */ + function print_question(&$question, &$state, &$number, $quiz, $options) { + /* The default implementation should work for most question types + provided the member functions it calls are overridden where required. + The question number is printed in the first cell of a table. - if ($recentlyadded) { - echo ''; - // Notify the user of this recently added question - echo ''.$strrecentlyaddedquestion.''; - echo ''; + The main content is printed below in the top row of the second column + using {@link print_question_formulation_and_controls}. + The grading details are printed in the second row in the second column + using {@print_question_grading_details}. + The {@link print_question_submit_buttons} member is invoked to add a third + row containing the submit button(s) when $options->readonly is false. */ - } else { // The normal case - echo ''; + print_simple_box_start('center', '90%'); + echo ''; + if ($options->readonly) { + echo '
'; + } else { + echo ''; + } + + // Print question number + echo '' . $number . ''; + if (isteacher($quiz->course)) { + echo ' ( '; + link_to_popup_window ('/mod/quiz/question.php?id=' . $question->id, + 'editquestion', $question->id, 450, 550, get_string('edit', 'quiz')); + echo ')'; + } + if ($question->maxgrade and $options->scores) { + echo '
'; + echo get_string('marks', 'quiz').':
 '; + echo ('' === $state->last_graded->grade) ? '--' : format_float($state->last_graded->grade, $quiz->decimalpoints); + echo '/'.$question->maxgrade.'
'; + } + + echo '
'; + + $this->print_question_formulation_and_controls($question, $state, + $quiz, $options); + + echo '
'; + + if ($options->scores) { + $this->print_question_grading_details($question, $state, $quiz, $options); + } + + if (QUIZ_EVENTDUPLICATEGRADE == $state->event) { + echo ' '; + print_string('duplicateresponse', 'quiz'); + } + + if(!$options->readonly) { + echo '
'; + $this->print_question_submit_buttons($question, $state, + $quiz, $options); + } + + if(isset($options->history) and $options->history) { + if ($options->history == 'all') { + // show all states + $states = get_records_select('quiz_states', "attempt = '$state->attempt' AND question = '$question->id' AND event > '0'", 'seq_number DESC'); + } else { + // show only graded states + $states = get_records_select('quiz_states', "attempt = '$state->attempt' AND question = '$question->id' AND event = '".QUIZ_EVENTGRADE."'", 'seq_number DESC'); + } + if (count($states) > 1) { + $strreviewquestion = get_string('reviewresponse', 'quiz'); + unset($table); + $table->head = array ( + get_string('numberabbr', 'quiz'), + get_string('action', 'quiz'), + get_string('response', 'quiz'), + get_string('time'), + get_string('score', 'quiz'), + get_string('penalty', 'quiz'), + get_string('grade', 'quiz'), + ); + $table->align = array ('center', 'center', 'left', 'left', 'left', 'left', 'left'); + $table->size = array ('', '', '', '', '', '', ''); + $table->width = '100%'; + foreach ($states as $st) { + $b = ($state->id == $st->id) ? '' : ''; + $be = ($state->id == $st->id) ? '' : ''; + $table->data[] = array ( + ($state->id == $st->id) ? ''.$st->seq_number.'' : link_to_popup_window ('/mod/quiz/reviewquestion.php?state='.$st->id.'&number='.$number, 'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion, 'none', true), + $b.get_string('event'.$st->event, 'quiz').$be, + $b.$this->response_summary($st).$be, + $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be, + $b.format_float($st->raw_grade, $quiz->decimalpoints).$be, + $b.format_float($st->penalty, $quiz->decimalpoints).$be, + $b.format_float($st->grade, $quiz->decimalpoints).$be + ); + } + echo '
'; + print_table($table); + } + } + echo '
'; + print_simple_box_end(); + } + + + /** + * Summary of the student response + * + * This function returns a short string of no more than 80 characters that + * summarizes the student's response + * @return string + * @param object $state + */ + function response_summary($state) { + // This should almost certainly be overridden + return substr($state->answer, 0, 80); + } + + + /** + * Prints the score obtained and maximum score available plus any penalty + * information + * + * This function should print a summary of the scoring in the most recently + * graded state (the question may not have been submitted for marking at + * the current state). The default implementation should be suitable for most + * question types. + * @param object $question The question for which the grading details are + * to be rendered. Question type specific information + * is included. The maximum possible grade is in + * ->maxgrade. + * @param object $state The state. In particular the grading information + * is in ->grade, ->raw_grade and ->penalty. + * @param object $quiz The quiz to which the question belongs. The + * grading details may be rendered differently + * depending on the quiz settings. + * @param object $options An object describing the rendering options. The + * fields are: + * ->readonly Review / interactive mode + * ->feedback Show feedback + * ->validation Show how the response was + * interpreted + * ->correct_responses Show solutions + * These are all boolean values. + */ + function print_question_grading_details(&$question, &$state, $quiz, $options) { + /* The default implementation prints the number of marks if no attempt + has been made. Otherwise it displays the grade obtained out of the + maximum grade available and a warning if a penalty was applied for the + attempt and displays the overall grade obtained counting all previous + responses (and penalties) */ + + if (!empty($question->maxgrade)) { + echo '
'; + if (!('' === $state->last_graded->grade)) { + // Display the grading details from the last graded state + $grade->cur = format_float($state->last_graded->grade, $quiz->decimalpoints); + $grade->max = $question->maxgrade; + $grade->raw = format_float($state->last_graded->raw_grade, $quiz->decimalpoints); + if (QUIZ_EVENTCLOSE == $state->event) { + /* No further attempts are possible so don't bother + displaying the penalty */ + print_string('gradingdetailsnopenalty', 'quiz', $grade); + } else if ('' !== $state->last_graded->penalty && ((float) + $state->last_graded->penalty) > 0.0) { + // A penalty was applied so display it + $grade->penalty = $state->last_graded->penalty; + print_string('gradingdetailspenalty', 'quiz', $grade); + } else if ($state->last_graded->raw_grade >= + $question->maxgrade) { + /* No penalty was applied because the response was + correct so don't bother noting that no penalty was + applied for the attempt */ + print_string('gradingdetailsnopenalty', 'quiz', $grade); + } else { + /* No penalty was applied even though the answer was + not correct (eg. a syntax error) so tell the student + that they were not penalised for the attempt */ + print_string('gradingdetailszeropenalty', 'quiz', $grade); + } + } + 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, - isset($question->recentlyadded) ? $question->recentlyadded : false, - $question->id, $quiz->course); - - $this->print_question_formulation_and_controls( - $question, $quiz, $readonly, - empty($resultdetails) ? false : $resultdetails->answers, - empty($resultdetails) ? false : $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... + /** + * Prints the main content of the question including any interactions + * + * This function should print the main content of the question including the + * interactions for the question in the state given (unless the readonly + * option is set). The last graded responses should be printed or indicated + * and (except when the readonly option is set) the current responses should + * be selected or filled in. Any names (eg. for any form elements) should be + * prefixed with the unique prefix for the question in + * $question->name_prefix. This method is called from the print_question + * method by default; the question type may override print_question so that + * this method is not used. + * @param object $question The question to be rendered. Question type + * specific information is included. The name + * prefix for any named elements is in ->name_prefix. + * @param object $state The state to render the question in. The grading + * information is in ->grade, ->raw_grade and + * ->penalty. The current responses are in + * ->responses. This will be an associative array + * (except in the case of no responses submitted when + * this will be an empty string rather than an empty + * array; this might occur when radio buttons are the + * only interactions for a question and none are + * selected for example). The last graded state is in + * ->last_graded (hence the most recently graded + * responses are in ->last_graded->responses). The + * question type specific information is in $state->options. + * @param object $quiz The quiz to which the question belongs. The + * question will likely be rendered differently + * depending on the quiz settings. + * @param object $options An object describing the rendering options. + * The fields are: + * ->readonly Review / interactive mode + * ->feedback Show feedback for the graded + * responses + * ->validation Show how the current responses + * responses were interpreted + * ->correct_responses Show solutions + * These are all boolean values. + */ + function print_question_formulation_and_controls(&$question, &$state, $quiz, $options) { + /* This default implementation prints an error and must be overridden + by all question type implementations, 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 actual_number_of_questions($question) { - /// Used for the feature number-of-questions-per-page - /// to determine the actual number of questions wrapped - /// by this question. The default is ONE! - return 1; + /** + * Prints the submit button(s) for the question in the given state + * + * This function should print the submit button(s) for the question in the + * given state. The name of any button created should be prefixed with the + * unique prefix for the question in $question->name_prefix. The suffix + * 'mark' is reserved for the single question mark button and the suffix + * 'validate' is reserved for the single question validate button (for + * question types which support it). Other suffixes will result in a response + * of that name in $state->responses which the printing and grading methods + * can then use. + * @param object $question The question for which the submit button(s) are to + * be rendered. Question type specific information is + * included. The name prefix for any + * named elements is in ->name_prefix. + * @param object $state The state to render the buttons for. The + * question type specific information is also + * included. + * @param object $quiz The quiz to which the question belongs. The + * choice of buttons will likely depend on the quiz + * settings. + * @param object $options An object describing the rendering options. The + * fields are: + * ->readonly Review / interactive mode + * ->feedback Show feedback + * ->validation Show how the response was + * interpreted + * ->correct_responses Show solutions + * These are all boolean values. + */ + function print_question_submit_buttons(&$question, &$state, $quiz, $options) { + /* The default implementation should be suitable for most question + types. It prints a mark button in the case where individual marking is + allowed in the quiz. */ + + if($quiz->optionflags & QUIZ_ADAPTIVE) { + echo ''; + } } - 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[]) + /** + * Renders the question for printing and returns the LaTeX source produced + * + * This function should render the question suitable for a printed problem + * or solution sheet in LaTeX and return the rendered output. + * @return string The LaTeX output. + * @param object $question The question to be rendered. Question type + * specific information is included. + * @param object $state The state to render the question in. The + * question type specific information is also + * included. + * @param object $quiz The quiz to which the question belongs. The + * question will likely be rendered differently + * depending on the quiz settings. + * @param string $type Indicates if the question or the solution is to be + * rendered with the values 'question' and + * 'solution'. + */ + function get_texsource(&$question, &$state, $quiz, $type) { + // The default implementation simply returns a string stating that + // the question is only available online. - error('grade_response has not been implemented for question type ' - .$this->name()); + return get_string('onlineonly', 'texsheet'); } + /** + * Compares two question states for equivalence of the student's responses + * + * The responses for the two states must be examined to see if they represent + * equivalent answers to the question by the student. This method will be + * invoked for each of the previous states of the question before grading + * occurs. If the student is found to have already attempted the question + * with equivalent responses then the attempt at the question is ignored; + * grading does not occur and the state does not change. Thus they are not + * penalized for this case. + * @return boolean + * @param object $question The question for which the states are to be + * compared. Question type specific information is + * included. + * @param object $state The state of the question. The grading + * information is in ->grade, ->raw_grade and + * ->penalty. The currently responses are in + * ->responses. For legacy question types with only + * one response and use only the name prefix for the + * name of the interaction this will be a single + * value. Otherwise it will be an associative array + * (except in the case of no responses submitted + * when this will be an empty string rather than an + * empty array; this might occur when radio buttons + * are the only interactions for a question and none + * are selected for example). The last graded state + * is in ->last_graded (hence the most recently + * graded responses are in + * ->last_graded->responses). The question type + * specific information is also included. + * @param object $teststate The state whose responses are to be + * compared. The state will be of the same age or + * older than $state. + */ + function compare_responses(&$question, $state, $teststate) { + // The default implementation performs a comparison of the response + // arrays. The ordering of the arrays does not matter. + // Question types may wish to override this (eg. to ignore trailing + // white space or to make "7.0" and "7" compare equal). + return $state->responses == $teststate->responses; + } + + /** + * Performs response processing and grading + * + * This function should perform response processing and grading and update + * the state accordingly. + * @return boolean Indicates success or failure. + * @param object $question The question to be graded. Question type + * specific information is included. + * @param object $state The state of the question to grade. The grading + * information is in ->grade, ->raw_grade and + * ->penalty. The currently responses are in + * ->responses. It will be an associative array + * (except in the case of no responses submitted when + * this will be an empty string rather than an empty + * array; this might occur when radio buttons are the + * only interactions for a question and none are + * selected for example). The last graded state is in + * ->last_graded (hence the most recently graded + * responses are in ->last_graded->responses). The + * question type specific information is also + * included. The ->raw_grade and ->penalty fields + * must be updated. The ->grade field is computed + * automatically. The cumulative penalty must be set + * in ->penalty by adding to the penalty from the + * most recently graded state. The method is able to + * close the question session (preventing any further + * attempts at this question) by setting + * $state->event to QUIZ_EVENTCLOSE. + * @param object $quiz The quiz to which the question belongs. The + * question might be graded differently depending on + * the quiz settings. + */ + function grade_responses(&$question, &$state, $quiz) { + /* The default implementation uses the comparison method to check if + the responses given are equivalent to the responses for each answer + in turn and sets the marks and penalty accordingly. This works for the + most simple question types. */ + + $teststate = clone($state); + foreach($question->options->answers as $answer) { + $teststate->responses[''] = $answer->answer; + + if($this->compare_responses($question, $state, $teststate)) { + $state->raw_grade = min(max((float) $answer->fraction, + 0.0), 1.0) * $question->maxgrade; + break; + } + } + if (empty($state->raw_grade)) { + $state->raw_grade = 0.0; + } + $state->penalty = 0; + // Only allow one attempt at the question + $state->event = QUIZ_EVENTCLOSE; + return true; + } + + + /** + * Includes configuration settings for the question type on the quiz admin + * page + * + * Returns an array of objects describing the options for the question type + * to be included on the quiz module admin page. + * Configuration options can be included by setting the following fields in + * the object: + * ->name The name of the option within this question type. + * The full option name will be constructed as + * "quiz_{$this->name()}_$name", the human readable name + * will be displayed with get_string($name, 'quiz'). + * ->code The code to display the form element, help button, etc. + * i.e. the content for the central table cell. Be sure + * to name the element "quiz_{$this->name()}_$name" and + * set the value to $CFG->{"quiz_{$this->name()}_$name"}. + * ->help Name of the string from the quiz module language file + * to be used for the help message in the third column of + * the table. An empty string (or the field not set) + * means to leave the box empty. + * Links to custom settings pages can be included by setting the following + * fields in the object: + * ->name The name of the link text string. + * get_string($name, 'quiz') will be called. + * ->link The filename part of the URL for the link. The full URL + * is contructed as + * "$CFG->wwwroot/mod/quiz/questiontypes/{$this->name()}/$link?sesskey=$sesskey" + * [but with the relavant calls to the s and rawurlencode + * functions] where $sesskey is the sesskey for the user. + * @return array Array of objects describing the configuration options to + * be included on the quiz module admin page. + */ function get_config_options() { - // Returns an array of objects describing the options for the question type - // to be included on the quiz module admin page - // - // Configuration options can be included by setting the following fields in - // the object: - // ->name (The name of the option within this question type - // - the full option name will be constructed as - // "quiz_{$this->name()}_$name", the human readable name - // will be displayed with get_string($name, 'quiz')) - // ->code (The code to display the form element, help button, etc. - // i.e. the content for the central table cell. Be sure - // to name the element "quiz_{$this->name()}_$name" and - // set the value to $CFG->{"quiz_{$this->name()}_$name"}) - // ->help (Name of the string from the quiz module language file - // to be used for the help message in the third column of - // the table. An empty string (or the field not set) - // means to leave the box empty) - // - // Links to custom settings pages can be included by setting the following - // fields in the object: - // ->name (The name of the link text string - - // get_string($name, 'quiz') will be called) - // ->link (The filename part of the URL for the link - // - the full URL is contructed as - // "$CFG->wwwroot/mod/quiz/questiontypes/{$this->name()}/$link?sesskey=$sesskey" - // [but with the relavant calls to the s and rawurlencode - // functions] where $sesskey is the sesskey for the user) - // No options by default + return false; } + /** + * Returns true if the editing wizard is finished, false otherwise. The + * default implementation returns true, which is suitable for all question- + * types that only use one editing form. This function is used in + * question.php to decide whether we can regrade any states of the edited + * question and redirect to edit.php. + * + * The dataset dependent question-type, which is extended by the calculated + * question-type, overwrites this method because it uses multiple pages (i.e. + * a wizard) to set up the question and associated datasets. + * + * @param object $form The data submitted by the previous page. + * + * @return boolean Whether the wizard's last page was submitted or not. + */ + function finished_edit_wizard(&$form) { + //In the default case there is only one edit page. + return true; + } + function print_replacement_options($question, $course, $quizid='0') { - // This function is used near the end of the question edit forms in all questiontypes + // This function is used near the end of the question edit forms in all question types // It prints the table of quizzes in which the question is used - // containing the checkboxes to allow the teacher to replace the old question version + // containing checkboxes to allow the teacher to replace the old question version // no need to display replacement options if the question is new if(empty($question->id)) { return true; } - // get quizzes using the question (using the question_grades table) + // get quizzes using the question (using the question_instances table) $quizlist = array(); - if(!$grades = get_records('quiz_question_grades', 'question', $question->id)) { - $grades = array(); + if(!$instances = get_records('quiz_question_instances', 'question', $question->id)) { + $instances = array(); } - foreach($grades as $grade) { - $quizlist[$grade->quiz] = $grade->quiz; + foreach($instances as $instance) { + $quizlist[$instance->quiz] = $instance->quiz; } $quizlist = implode(',', $quizlist); if(empty($quizlist) or !$quizzes = get_records_list('quiz', 'id', $quizlist)) { @@ -394,10 +1069,9 @@ class quiz_default_questiontype { // find how many different students have already attempted this quiz $students = array(); - if($attempts = get_records('quiz_attempts', 'quiz', $quiz->id)) { + if($attempts = get_records_select('quiz_attempts', "quiz = '$quiz->id' AND preview = '0'")) { foreach($attempts as $attempt) { - if (!isteacher($course->id, $attempt->userid) // Ignore teacher attempts - and record_exists('quiz_responses', 'attempt', $attempt->id, 'question', $question->id, 'originalquestion', 0)) { + if (record_exists('quiz_states', 'attempt', $attempt->id, 'question', $question->id, 'originalquestion', 0)) { $students[$attempt->userid] = 1; } } @@ -417,8 +1091,8 @@ class quiz_default_questiontype { } function print_question_form_end($question, $submitscript='') { - // This function is used at the end of the question edit forms in all questiontypes - // It prints the submit buttons and the standard hidden form fields + // This function is used at the end of the question edit forms in all question types + // It prints the submit, copy, and cancel buttons and the standard hidden form fields global $USER; echo ' @@ -438,7 +1112,16 @@ class quiz_default_questiontype { } +/// QUIZ_QTYPES INITIATION ////////////////// + quiz_load_questiontypes(); + +/** +* Loads the questiontype.php file for each question type +* +* These files in turn instantiate the corresponding question type class +* and adds it to the $QUIZ_QTYPES array +*/ function quiz_load_questiontypes() { global $QUIZ_QTYPES; global $CFG; @@ -457,8 +1140,18 @@ function quiz_load_questiontypes() { -/// SQL FUNCTIONS //////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////// +/// Any other quiz functions go here. Each of them must have a name that +/// starts with quiz_ + +/** +* Move all questions in $category1 to $category2 +* +* @return boolean indicate Success/Failure +* @param $category1 the id of the category to move away from +* @param $category2 the id of the category to move to +*/ function quiz_move_questions($category1, $category2) { global $CFG; return execute_sql("UPDATE {$CFG->prefix}quiz_questions @@ -467,246 +1160,647 @@ function quiz_move_questions($category1, $category2) { 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)"); +/** +* Construct name prefixes for question form element names +* +* Construct the name prefix that should be used for example in the +* names of form elements created by questions for inclusion in the +* quiz page. This is called by {@link quiz_get_question_options()} +* to set $question->name_prefix. +* This name prefix includes the question id which can be +* extracted from it with {@link quiz_get_id_from_name_prefix()}. +* +* @return string +* @param integer $id The question id +*/ +function quiz_make_name_prefix($id) { + return 'resp' . $id . '_'; } -function quiz_questiongrades_update($grades, $quizid) { - // this is called from edit.php to store changes to the question grades - // in the quiz_question_grades table. It does not update 'sumgrades' in the quiz table. - $existing = get_records("quiz_question_grades", "quiz", $quizid, "", "question,grade,id"); - foreach ($grades as $question => $grade) { - unset($questiongrade); - $questiongrade->quiz = $quizid; - $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; - } +/** +* Extract question id from the prefix of form element names +* +* @return integer The question id +* @param string $name The name that contains a prefix that was +* constructed with {@link quiz_make_name_prefix()} +*/ +function quiz_get_id_from_name_prefix($name) { + if (!preg_match('/^resp([0-9]+)_/', $name, $matches)) + return false; + return (integer) $matches[1]; +} + +/** +* Updates the question objects in an array with the question type specific +* information for each one by calling {@link get_question_options()} +* +* The get_question_options method of the question type of each question in the +* array is called to add the options field to the question object. +* @return bool Indicates success or failure. +* @param array $questions The array of question objects to be updated. +*/ +function quiz_get_question_options(&$questions) { + global $QUIZ_QTYPES; + + // get the keys of the input array + $keys = array_keys($questions); + // update each question object + foreach ($keys as $i) { + // set name prefix + $questions[$i]->name_prefix = quiz_make_name_prefix($i); + + if (!$QUIZ_QTYPES[$questions[$i]->qtype]->get_question_options($questions[$i])) + return false; + } + return true; +} + +/** +* Creates an object to represent a new attempt at a quiz +* +* Creates an attempt object to represent an attempt at the quiz by the current +* user starting at the current time. The ->id field is not set. The object is +* NOT written to the database. +* @todo Allow new attempt to be based on previous attempt +* @todo Allow question shuffling +* @return object The newly created attempt object. +* @param object $quiz The quiz to create an attempt for. +* @param integer $attemptnumber The sequence number for the attempt. +*/ +function quiz_create_attempt($quiz, $attemptnumber) { + global $USER; + + $timenow = time(); + $attempt->quiz = $quiz->id; + $attempt->userid = $USER->id; + $attempt->attempt = $attemptnumber; + $attempt->sumgrades = 0.0; + $attempt->preview = 0; + $attempt->timestart = $timenow; + $attempt->timefinish = 0; + $attempt->timemodified = $timenow; + if ($quiz->shufflequestions) { + $attempt->layout = quiz_repaginate($quiz->questions, $quiz->questionsperpage, true); + } else { + $attempt->layout = $quiz->questions; + } + return $attempt; +} + +/** +* Loads the most recent state of each question session from the database +* or create new one. +* +* For each question the most recent session state for the current attempt +* is loaded from the quiz_states table and the question type specific data and +* responses are added by calling {@link quiz_restore_state()} which in turn +* calls {@link restore_session_and_responses()} for each question. +* If no states exist for the question instance an empty state object is +* created representing the start of a session and empty question +* type specific information and responses are created by calling +* {@link create_session_and_responses()}. +* @todo Allow new attempt to be based on last attempt +* +* @return array An array of state objects representing the most recent +* states of the question sessions. +* @param array $questions The questions for which sessions are to be restored or +* created. +* @param object $quiz The quiz to which the questions belong. +* @param object $attempt The quiz attempt for which the question sessions are +* to be restored or created. +*/ +function quiz_restore_question_sessions(&$questions, $quiz, $attempt) { + global $CFG, $QUIZ_QTYPES; + + // get the question ids + $ids = array_keys($questions); + $questionlist = implode(',', $ids); + + // The question field must be listed first so that it is used as the + // array index in the array returned by get_records_sql + $statefields = 'n.questionid as question, s.id, s.attempt, s.originalquestion, s.seq_number,'. + ' s.answer, s.event, s.grade, s.raw_grade, s.penalty, n.sumpenalty'; + // Load the newest states for the questions + $sql = "SELECT $statefields". + " FROM {$CFG->prefix}quiz_states s,". + " {$CFG->prefix}quiz_newest_states n". + " WHERE s.id = n.new". + " AND n.attemptid = '$attempt->id'". + " AND n.questionid IN ($questionlist)"; + $states = get_records_sql($sql); + + // Load the newest graded states for the questions + $sql = "SELECT $statefields". + " FROM {$CFG->prefix}quiz_states s,". + " {$CFG->prefix}quiz_newest_states n". + " WHERE s.id = n.newgraded". + " AND n.attemptid = '$attempt->id'". + " AND n.questionid IN ($questionlist)"; + $gradedstates = get_records_sql($sql); + + // loop through all questions and set the last_graded states + foreach ($ids as $i) { + if (isset($states[$i])) { + quiz_restore_state($questions[$i], $states[$i]); + if (isset($gradedstates[$i])) { + quiz_restore_state($questions[$i], $gradedstates[$i]); + $states[$i]->last_graded = $gradedstates[$i]; + } else { + error('No graded state could be found!'); } } else { - if (!insert_record("quiz_question_grades", $questiongrade)) { + // Create an empty state object + $states[$i] = new object; + $states[$i]->attempt = $attempt->id; + $states[$i]->question = (int) $i; + $states[$i]->seq_number = 0; + $states[$i]->timestamp = $attempt->timestart; + $states[$i]->event = ($attempt->timefinish) ? QUIZ_EVENTCLOSE : QUIZ_EVENTOPEN; + $states[$i]->grade = ''; + $states[$i]->raw_grade = ''; + $states[$i]->penalty = ''; + $states[$i]->sumpenalty = '0.0'; + $states[$i]->responses = array('' => ''); + // Prevent further changes to the session from incrementing the + // sequence number + $states[$i]->changed = true; + + // Create the empty question type specific information + if (!$QUIZ_QTYPES[$questions[$i]->qtype] + ->create_session_and_responses($questions[$i], $states[$i], $quiz, $attempt)) { return false; } + $states[$i]->last_graded = clone($states[$i]); } } + + return $states; } -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 +/** +* Creates the run-time fields for the states +* +* Extends the state objects for a question by calling +* {@link restore_session_and_responses()} or it creates a new one by +* calling {@link create_session_and_responses()} +* @return boolean Represents success or failure +* @param array $questions The questions for which states are needed +* @param array $states The states as loaded from the database, indexed +* by question id +*/ +function quiz_restore_state(&$question, &$state) { global $QUIZ_QTYPES; - return $QUIZ_QTYPES[$question->qtype]->get_answers($question); + // initialise response to the value in the answer field + $state->responses = array('' => $state->answer); + unset($state->answer); + + // Set the changed field to false; any code which changes the + // question session must set this to true and must increment + // ->seq_number; it can do this by calling + // quiz_mark_session_change. The quiz_save_question_session + // function will save the new state object database if the field is + // set to true. + $state->changed = false; + + // Load the question type specific data + return $QUIZ_QTYPES[$question->qtype] + ->restore_session_and_responses($question, $state); + } -function quiz_get_attempt_questions($quiz, $attempt, $attempting = false) { - /// Returns the questions of the quiz attempt in a format used for - /// grading and printing them... - /// - /// $attempting should be set to true if this function is called in - /// order to create an attempt page and false if it is called to create - /// a review page. - /// - /// 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 - +/** +* Saves the current state of the question session to the database +* +* The state object representing the current state of the session for the +* question is saved to the quiz_states table with ->responses[''] saved +* to the answer field of the database table. +* The question type specific data is then saved. +* @return boolean Indicates success or failure. +* @param object $question The question for which session is to be saved. +* @param object $state The state information to be saved. In particular the +* most recent responses are in ->responses. The object +* is updated to hold the new ->id. +*/ +function quiz_save_question_session(&$question, &$state) { global $QUIZ_QTYPES; - global $CFG; + // Check if the state has changed + if (!$state->changed && isset($state->id)) { + return true; + } + // Set the legacy answer field + $state->answer = isset($state->responses['']) ? $state->responses[''] : ''; - ///////////////////////// - /// Get the questions: - ///////////////////////// - if (!($questions = - get_records_list('quiz_questions', 'id', $quiz->questions))) { - notify('Error when reading questions from the database!'); + // Save the state + unset($state->id); + if (!$state->id = insert_record('quiz_states', $state)) { + unset($state->id); + unset($state->answer); return false; } + unset($state->answer); - //////////////////////////////////////////// - /// Determine ->maxgrade for all questions - //////////////////////////////////////////// - If (!($grades = quiz_get_question_grades($quiz->id, $quiz->questions))) { - $grades = array(); + // this is the most recent state + if (!record_exists('quiz_newest_states', 'attemptid', + $state->attempt, 'questionid', $question->id)) { + $new->attemptid = $state->attempt; + $new->questionid = $question->id; + $new->new = $state->id; + $new->newgraded = $state->id; + $new->sumpenalty = '0.0'; + if (!insert_record('quiz_newest_states', $new)) { + error('Could not insert entry in quiz_newest_states'); + } + } else { + set_field('quiz_newest_states', 'new', $state->id, 'attemptid', + $state->attempt, 'questionid', $question->id); } - foreach ($questions as $qid => $question) { - if (isset($grades[$qid])) { - $questions[$qid]->maxgrade = $grades[$qid]->grade; - } else { - $questions[$qid]->maxgrade = 0.0; + if (quiz_state_is_graded($state)) { + // this is also the most recent graded state + if ($newest = get_record('quiz_newest_states', 'attemptid', + $state->attempt, 'questionid', $question->id)) { + $newest->newgraded = $state->id; + $newest->sumpenalty = $state->sumpenalty; + update_record('quiz_newest_states', $newest); } } - ////////////////////////////////////////////////////////////// - /// Determine attributes ->response and ->recentlyadded (hard) - ////////////////////////////////////////////////////////////// + // Save the question type specific state information and responses + if (!$QUIZ_QTYPES[$question->qtype]->save_session_and_responses( + $question, $state)) { + return false; + } + // Reset the changed flag + $state->changed = false; + return true; +} - /// Get all existing responses on this attempt - $rawresponses = get_records_sql(" - SELECT question, answer, attempt - FROM {$CFG->prefix}quiz_responses - WHERE attempt = '$attempt->id' "); +/** +* Determines whether a state has been graded by looking at the event field +* +* @return boolean true if the state has been graded +* @param object $state +*/ +function quiz_state_is_graded($state) { + return ($state->event == QUIZ_EVENTGRADE or $state->event == QUIZ_EVENTCLOSE); +} - /// The setting for ->recentlyadded depends on whether this is - /// a test attempt or just a review - if ($attempting) { - /// This is a test attempt so there is a need to create responses - /// in case there are none existing. - /// Further - the attribute recentlyadded is determined from - /// whether the question has a response in the previous attempt, - /// which might be used in case the attemptonlast quiz option - /// is true. +/** +* Updates a state object for the next new state to record the fact that the +* question session has changed +* +* If the question session is not already marked as having changed (via the +* ->changed field of the state object), then this is done, the sequence +* number in ->seq_number is incremented and the timestamp in ->timestamp is +* updated. This should be called before or after any code which changes the +* question session. +* @param object $state The state object representing the state of the session. +*/ +function quiz_mark_session_change(&$state) { + if (!$state->changed) { + $state->changed = true; + $state->seq_number++; + $state->timestamp = time(); + } +} - $prevattempt = $attempt->attempt; - $prevresponses= array(); - while (--$prevattempt) { - $prevresponses = get_records_sql(" - SELECT r.question, r.answer, r.attempt, r.grade - FROM {$CFG->prefix}quiz_responses r, {$CFG->prefix}quiz_attempts a - WHERE a.quiz='$quiz->id' AND a.userid='$attempt->userid' - AND a.attempt='$prevattempt' AND r.attempt=a.id "); - if (!empty($prevresponses)) { + +/** +* Extracts responses from submitted form +* +* TODO: Finish documenting this +* @return array array of action objects, indexed by question ids. +* @param array $questions an array containing at least all questions that are used on the form +* @param array $responses +* @param integer $defaultevent +*/ +function quiz_extract_responses($questions, $responses, $defaultevent) { + + $actions = array(); + foreach ($responses as $key => $response) { + // Get the question id from the response name + if (false !== ($quid = quiz_get_id_from_name_prefix($key))) { + // check if this is a valid id + if (!isset($questions[$quid])) { + error('Form contained question that is not in questionids'); + } + + // Remove the name prefix from the name + $key = substr($key, strlen($questions[$quid]->name_prefix)); + if (false === $key) { + $key = ''; + } + + // Check for question validate and mark buttons & set events + if ($key === 'validate') { + $actions[$quid]->event = QUIZ_EVENTVALIDATE; + } else if ($key === 'mark') { + $actions[$quid]->event = QUIZ_EVENTGRADE; + } else { + $actions[$quid]->event = $defaultevent; + } + + // Update the state with the new response + $actions[$quid]->responses[$key] = $response; + } + } + return $actions; +} + + +/** +* For a given question instance we walk the complete history of states for +* each user and recalculate the grades as we go along. +* +* This is used when a question in an existing quiz is changed and old student +* responses need to be marked with the new version of a question. +* +* TODO: Finish documenting this +* @return boolean Indicates success/failure +* @param object $question A question object +* @param array $quizlist An array of quiz ids, in which the question should +* be regraded. If quizlist is the empty array, all +* quizzes are affected. +*/ +function quiz_regrade_question_in_quizzes($question, $quizlist) { + + $quizlist = implode(',', $quizlist); + if (empty($quizlist)) { // assume that all quizzes are affected + if (! $instances = get_records('quiz_question_instances', + 'question', $question->id)) { + // No instances were found, so it successfully regraded all of them + return true; + } + $quizlist = implode(',', array_map(create_function('$val', + 'return $val->quiz;'), $instances)); + unset($instances); + } + + // Get all affected quizzes + if (! $quizzes = get_records_list('quiz', 'id', $quizlist)) { + error('Couldn\'t get quizzes for regrading!'); + } + + foreach ($quizzes as $quiz) { + // All the attempts that need to be changed + if (! $attempts = get_records('quiz_attempts', 'quiz', $quiz->id)) { + error("Couldn't get question instance for regrading!"); + } + $attempts = array_values($attempts); + if (! $instance = get_record('quiz_question_instances', + 'quiz', $quiz->id, 'question', $question->id)) { + error("Couldn't get question instance for regrading!"); + } + $question->maxgrade = $instance->grade; + for ($i = 0; $i < count($attempts); $i++) { + if ($states = get_records_select('quiz_states', + "attempt = '{$attempts[$i]->id}' ". + "AND question = '{$question->id}'", + 'seq_number ASC')) { + $states = array_values($states); + + $attempts[$i]->sumgrades -= $states[count($states)-1]->grade; + + // Initialise the replaystate + quiz_restore_state($question, $states[0]); + $replaystate = clone($states[0]); + $replaystate->last_graded = clone($states[0]); + for($j = 1; $j < count($states); $j++) { + quiz_restore_state($question, $states[$j]); + $action = new stdClass; + $action->responses = $states[$j]->responses; + // Close the last state of a finished attempt + if (((count($states) - 1) === $j) && ($attempts[$i]->timefinish > 0)) { + $action->event = QUIZ_EVENTCLOSE; + + // Grade instead of closing, quiz_process_responses will then + // work out whether to close it + } else if (QUIZ_EVENTCLOSE == $states[$j]->event) { + $action->event = QUIZ_EVENTGRADE; + + // By default take the event that was saved in the database + } else { + $action->event = $states[$j]->event; + } + + // Reprocess (regrade) responses + quiz_process_responses($question, $replaystate, $action, $quiz, + $attempts[$i]); + $replaystate->id = $states[$j]->id; + update_record('quiz_states', $replaystate); + } + update_record('quiz_attempts', $attempts[$i]); + quiz_save_best_grade($quiz, $attempts[$i]->userid); + } + } + } +} + +/** +* Processes an array of student responses, grading and saving them as appropriate +* +* @return boolean Indicates success/failure +* @param object $question Full question object, passed by reference +* @param object $state Full state object, passed by reference +* @param object $action object with the fields ->responses which +* is an array holding the student responses and +* ->action which specifies the action, e.g., QUIZ_EVENTGRADE +* @param object $quiz The quiz object +* @param object $attempt The attempt is passed by reference so that +* during grading its ->sumgrades field can be updated +* +* @todo There is a variable $quiz->ignoredupresp which makes the function go through +* all previous states when checking if a response is duplicated. There is no user +* interface for this yet. +*/ +function quiz_process_responses(&$question, &$state, $action, $quiz, &$attempt) { + global $QUIZ_QTYPES; + + // make sure these are gone! + unset($action->responses['mark'], $action->responses['validate']); + + // Check the question session is still open + if (QUIZ_EVENTCLOSE == $state->event) { + return true; + } + // If $action->event is not set that implies saving + if (! isset($action->event)) { + $action->event = QUIZ_EVENTSAVE; + } + // Check if we are grading the question; compare against last graded + // responses, not last given responses in this case + if (QUIZ_EVENTGRADE == $action->event || QUIZ_EVENTCLOSE == $action->event) { + $state->responses = $state->last_graded->responses; + } + // Check for unchanged responses (exactly unchanged, not equivalent). + // We also have to catch questions that the student has not yet attempted + $sameresponses = (($state->responses == $action->responses) or + ($state->responses == array(''=>'') && array_keys(array_count_values($action->responses))===array(''))); + + if ($sameresponses && isset($action->event) and QUIZ_EVENTCLOSE != $action->event + and QUIZ_EVENTVALIDATE != $action->event) { + return true; + } + + // Roll back grading information to last graded state and set the new + // responses + $newstate = clone($state->last_graded); + $newstate->responses = $action->responses; + $newstate->seq_number = $state->seq_number; + $newstate->changed = false; + $newstate->last_graded = $state->last_graded; + $state = $newstate; + + // Set the event to the action we will perform. The question type specific + // grading code may override this by setting it to QUIZ_EVENTCLOSE if the + // attempt at the question causes the session to close + $state->event = $action->event; + + if (QUIZ_EVENTSAVE == $action->event || QUIZ_EVENTVALIDATE == $action->event) { + // Grade the response but don't update the overall grade + $QUIZ_QTYPES[$question->qtype]->grade_responses( + $question, $state, $quiz); + // Force the event to save or validate (even if the grading caused the + // state to close) + $state->event = $action->event; + + } else if (QUIZ_EVENTGRADE == $action->event) { + + // Work out if the current responses (or equivalent responses) were + // already given in + // a. the last graded attempt + // b. any other graded attempt + if($QUIZ_QTYPES[$question->qtype]->compare_responses( + $question, $state, $state->last_graded)) { + $state->event = QUIZ_EVENTDUPLICATEGRADE; + } else { + if ($quiz->optionflags & QUIZ_IGNORE_DUPRESP) { + /* Walk back through the previous graded states looking for + one where the responses are equivalent to the current + responses. If such a state is found, set the current grading + details to those of that state and set the event to + QUIZ_EVENTDUPLICATEGRADE */ + quiz_search_for_duplicate_responses($question, $state); + } + // If we did not find a duplicate, perform grading + if (QUIZ_EVENTDUPLICATEGRADE != $state->event) { + // Decrease sumgrades by previous grade and then later add new grade + $attempt->sumgrades -= (float)$state->last_graded->grade; + + $QUIZ_QTYPES[$question->qtype]->grade_responses( + $question, $state, $quiz); + // Calculate overall grade using correct penalty method + quiz_apply_penalty($question, $state, $quiz); + // Update the last graded state (don't simplify!) + unset($state->last_graded); + $state->last_graded = clone($state); + unset($state->last_graded->changed); + + $attempt->sumgrades += (float)$state->last_graded->grade; + } + } + } else if (QUIZ_EVENTCLOSE == $action->event) { + // decrease sumgrades by previous grade and then later add new grade + $attempt->sumgrades -= (float)$state->last_graded->grade; + + // Only mark if they haven't been marked already + if (!$sameresponses) { + $QUIZ_QTYPES[$question->qtype]->grade_responses( + $question, $state, $quiz); + // Calculate overall grade using correct penalty method + quiz_apply_penalty($question, $state, $quiz); + } + // Force the state to close (as the attempt is closing) + $state->event = QUIZ_EVENTCLOSE; + // If there is no valid grade, set it to zero + if ('' === $state->grade) { + $state->raw_grade = 0; + $state->penalty = 0; + $state->grade = 0; + } + // Update the last graded state (don't simplify!) + unset($state->last_graded); + $state->last_graded = clone($state); + unset($state->last_graded->changed); + + $attempt->sumgrades += (float)$state->last_graded->grade; + } + quiz_mark_session_change($state); + $attempt->timemodified = time(); + return true; +} + +/** +* Compare current responses to all previous graded responses +* +* This is used by {@link quiz_process_responses()} to determine whether +* to ignore the marking request for the current response. However this +* check against all previous graded responses is only performed if +* the QUIZ_IGNORE_DUPRESP bit in $quiz->optionflags is set +* @return boolean Indicates if a state with duplicate responses was +* found. +* @param object $question +* @param object $state +*/ +function quiz_search_for_duplicate_responses(&$question, &$state) { + // get all previously graded question states + global $QUIZ_QTYPES; + if (!$oldstates = get_records('quiz_question_states', "event = '" . + QUIZ_EVENTGRADE . "' AND " . "question = '" . $question->id . + "'", 'seq_number DESC')) { + return false; + } + foreach ($oldstates as $oldstate) { + if ($QUIZ_QTYPES[$question->qtype]->restore_session_and_responses( + $question, $oldstate)) { + if(!$QUIZ_QTYPES[$question->qtype]->compare_responses( + $question, $state, $oldstate)) { + $state->event = QUIZ_EVENTDUPLICATEGRADE; break; } } - - $questionsinuse = $quiz->questions; // used if responses must be created - foreach ($questions as $qid => $question) { - if ($questions[$qid]->recentlyadded = - $prevattempt && empty($prevresponses[$qid])) { - /* No action */ - - } else if ($prevattempt && $quiz->attemptonlast - && empty($rawresponses[$qid])) { - /// Store the previous response on this attempt! - $rawresponses[$qid] = $prevresponses[$qid]; - $rawresponses[$qid]->attempt = $attempt->id; - $rawresponses[$qid]->id = - insert_record("quiz_responses", $rawresponses[$qid]) - or error("Unable to create attemptonlast response for question $qid"); - } - - /* Extract possible response and its wrapped questions */ - if (!empty($rawresponses[$qid])) { - $questions[$qid]->response = $QUIZ_QTYPES[$question->qtype] - ->extract_response($rawresponses[$qid], - quiz_qtype_nameprefix($question)); - /// Catch any additional wrapped questions: - if ($wrapped = $QUIZ_QTYPES[$question->qtype] - ->wrapped_questions($questions[$question->id], - quiz_qtype_nameprefix($question))) { - $questionsinuse .= ",$wrapped"; - } - } - } - - /// Make sure all the questions will have responses: - foreach ($questions as $question) { - if (empty($question->response)) { - /// No response on this question - - $nameprefix = quiz_qtype_nameprefix($question); - $questions[$question->id]->response = - $QUIZ_QTYPES[$question->qtype]->create_response - ($question, $nameprefix, $questionsinuse); - - ////////////////////////////////////////////// - // Saving the newly created response before - // continuing with the quiz... - ////////////////////////////////////////////// - $responserecord->attempt = $attempt->id; - $responserecord->question = $question->id; - $responserecord->answer = $QUIZ_QTYPES[$question->qtype] - ->convert_to_response_answer_field - ($questions[$question->id]->response); - - insert_record("quiz_responses", $responserecord) - or error("Unable to create initial response for question $question->id"); - - /// Catch any additional wrapped questions: - if ($wrapped = $QUIZ_QTYPES[$question->qtype] - ->wrapped_questions($questions[$question->id], - quiz_qtype_nameprefix($question))) { - $questionsinuse .= ",$wrapped"; - } - } - } - - } else { - /// In the case of review, the recentlyadded flag is set true - /// when the question has been added after the attempt and new - /// responses are never created - - foreach ($questions as $qid => $question) { - if ($questions[$qid]->recentlyadded = empty($rawresponses[$qid])) { - /* No action */ - } else { - $questions[$qid]->response = $QUIZ_QTYPES[$question->qtype] - ->extract_response($rawresponses[$qid], - quiz_qtype_nameprefix($question)); - } - } } - - return $questions; + return (QUIZ_EVENTDUPLICATEGRADE == $state->event); } - -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; +/** +* Applies the penalty for the previous attempts to the raw grade for the current +* attempt +* +* The grade for the question in the current state is computed by applying the +* penalty accumulated over the previous marked attempts at the question to the +* raw grade using the penalty scheme in use in the quiz. The ->grade field of +* the state object is modified to reflect the new grade. +* @param object $question The question for which the penalty is to be applied. +* @param object $state The state for which the grade is to be set from the +* raw grade and the cumulative penalty from the last +* graded state. The ->grade field is updated by applying +* the penalty scheme for the quiz to the ->raw_grade and +* ->last_graded->penalty fields. +* @param object $quiz The quiz to which the question belongs. The penalty +* scheme to apply is given by the ->penaltyscheme field. +*/ +function quiz_apply_penalty(&$question, &$state, $quiz) { + switch ($quiz->penaltyscheme) { + case QUIZ_PENALTYMULTIPLY: + $state->grade = (1 - $state->sumpenalty) * $state->raw_grade; + $state->sumpenalty += $state->penalty * (1-$state->sumpenalty); + break; + case QUIZ_PENALTYSUBTRACT: + $state->grade = $state->raw_grade - ($question->maxgrade * $state->sumpenalty); + $state->sumpenalty += (float) $state->penalty; + break; + case QUIZ_PENALTYNONE: + default: + $state->grade = $state->raw_grade; + break; } + // Ensure that the grade does not go down + $state->grade = max($state->grade, $state->last_graded->grade); } + function quiz_print_comment($text) { echo "".format_text($text, true, false).""; } @@ -715,6 +1809,13 @@ function quiz_print_correctanswer($text) { echo "

$text

"; } +/** +* Print the icon for the question type +* +* @param object $question The question object for which the icon is required +* @param boolean $editlink If true then the icon is a link to the question +* edit page. +*/ function quiz_print_question_icon($question, $editlink=true) { // Prints a question icon @@ -733,8 +1834,13 @@ function quiz_print_question_icon($question, $editlink=true) { } } +/** +* Print the question image if there is one +* +* @param integer $quizid The id of the quiz +* @param object $question The question object +*/ function quiz_print_possible_question_image($quizid, $question) { -// Includes the question image if there is one global $CFG; @@ -759,245 +1865,144 @@ function quiz_print_possible_question_image($quizid, $question) { } } -function quiz_navigation_javascript($link) { - return "javascript:navigate($link);"; +/** +* Returns a comma separated list of question ids for the current page +* +* @return string Comma separated list of question ids +* @param string $layout The string representing the quiz layout. Each page is represented as a +* comma separated list of question ids and 0 indicating page breaks. +* So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2 +* @param integer $page The number of the current page. +*/ +function quiz_questions_on_page($layout, $page) { + $pages = explode(',0', $layout); + return trim($pages[$page], ','); } -function quiz_print_navigation_panel($questions, $questionsperpage, $navigation) { - global $QUIZ_QTYPES; +/** +* Returns a comma separated list of question ids for the quiz +* +* @return string Comma separated list of question ids +* @param string $layout The string representing the quiz layout. Each page is represented as a +* comma separated list of question ids and 0 indicating page breaks. +* So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2 +*/ +function quiz_questions_in_quiz($layout) { + return str_replace(',0', '', $layout); +} - $numberinglayout = array(); - $nextqnumber = 1; - foreach ($questions as $question) { - if ($qnumberinc = $QUIZ_QTYPES[$question->qtype] - ->actual_number_of_questions($question)) { - $numberinglayout[] = $nextqnumber; - $nextqnumber += $qnumberinc; - } - } - - if ($nextqnumber - $qnumberinc <= $questionsperpage) { - /// The total number of questions does not exceed the maximum - /// number of allowed questions per page so... - return 0; - } - /// else - Navigation menu will be printed! - - /////////////////////////////////////////////// - /// Determine the layout of the navigation menu - /////////////////////////////////////////////// - if (1 == $questionsperpage) { - /// The simple case: - $pagelinkagelayout = $pagenavigationlayout = $numberinglayout; +/** +* Returns the number of pages in the quiz layout +* +* @return integer Comma separated list of question ids +* @param string $layout The string representing the quiz layout. +*/ +function quiz_number_of_pages($layout) { + return substr_count($layout, ',0'); +} +/** +* Returns the first question number for the current quiz page +* +* @return integer The number of the first question +* @param string $quizlayout The string representing the layout for the whole quiz +* @param string $pagelayout The string representing the layout for the current page +*/ +function quiz_first_questionnumber($quizlayout, $pagelayout) { + // this works by finding all the questions from the quizlayout that + // come before the current page and then adding up their lengths. + global $CFG; + $start = strpos($quizlayout, $pagelayout)-3; + if ($start > 0) { + $prevlist = substr($quizlayout, 0, $start); + return get_field_sql("SELECT sum(length)+1 FROM {$CFG->prefix}quiz_questions + WHERE id IN ($prevlist)"); } else { - /// More complicated: - $pagenavigationlayout = array(); - $pagelinkagelayout = array($currentpagestart = 1); - foreach ($numberinglayout as $questionnumber) { - if ($questionnumber - $currentpagestart >= $questionsperpage) { - $pagenavigationlayout[] = $currentpagestart - .'-'. ($questionnumber - 1); - if ($currentpagestart < $navigation - && $navigation < $questionnumber) { - // $navigation is out of sync so adjust for robustness - $navigation = $currentpagestart; - } - $pagelinkagelayout[] = $currentpagestart = $questionnumber; - } - } - $pagenavigationlayout[] = $currentpagestart .'-'. ($nextqnumber - 1); - if ($currentpagestart < $navigation) { - // $firstquestion is out of sync so adjust it for robustness... - $navigation = $currentpagestart; - } + return 1; } +} - foreach ($pagelinkagelayout as $key => $link) { - if ($link < $navigation) { - $previouspagelink = $link; - } else if ($link == $navigation) { - $currentnavigationtitle = $pagenavigationlayout[$key]; - } else { - $endpagelink = $link; - if (false == isset($nextpagelink)) { - $nextpagelink = $link; - } - } +/** +* Re-paginates the quiz layout +* +* @return string The new layout string +* @param string $layout The string representing the quiz layout. +* @param integer $perpage The number of questions per page +* @param boolean $shuffle Should the questions be reordered randomly? +*/ +function quiz_repaginate($layout, $perpage, $shuffle=false) { + $layout = str_replace(',0', '', $layout); // remove existing page breaks + $questions = explode(',', $layout); + if ($shuffle) { + srand((float)microtime() * 1000000); // for php < 4.2 + shuffle($questions); } + $i = 1; + $layout = ''; + foreach ($questions as $question) { + if ($perpage and $i > $perpage) { + $layout .= '0,'; + $i = 1; + } + $layout .= $question.','; + $i++; + } + return $layout.'0'; +} - /////////////////////////////////////////////// - /// Print the navigation menu - /////////////////////////////////////////////// +/** +* Print navigation panel for quiz attempt and review pages +* +* @param integer $page The number of the current page (counting from 0). +* @param integer $pages The total number of pages. +*/ +function quiz_print_navigation_panel($page, $pages) { + //$page++; echo '
'; - echo ''.get_string('questions', 'quiz').':'; - if (isset($previouspagelink)) { + echo '' . get_string('page') . ':'; + if ($page > 0) { + // Print previous link $strprev = get_string('previous'); - echo '('.$strprev.')'; + echo '(' . $strprev . ')'; } - foreach ($pagelinkagelayout as $key => $link) { - if ($link != $navigation) { - echo ''; + for ($i = 0; $i < $pages; $i++) { + if ($i == $page) { + echo ''.($i+1).''; } else { - echo ''; + echo ''.($i+1).''; } - echo $pagenavigationlayout[$key]; - echo ($link != $navigation) ? '' : ''; } - if (isset($nextpagelink)) { + + if ($page < $pages - 1) { + // Print next link $strnext = get_string('next'); - echo '('.$strnext.')'; + echo '(' . $strnext . ')'; } echo '
'; - - //////////////////////////////////////////////// - /// Return the potentially adjusted $navigation - //////////////////////////////////////////////// - return $navigation; } -function quiz_print_quiz_questions($quiz, $questions, $results=NULL, - $shuffleorder=NULL, $navigation=0) { -// Prints a whole quiz on one page. - - if ($navigation < 0) { - $navigation = 0; // For robustness - } - +/** +* Prints a question for a quiz page +* +* Simply calls the question type specific print_question() method. +*/ +function quiz_print_quiz_question(&$question, &$state, $number, $quiz, $options=null) { 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"; - - if ($navigation && $quiz->questionsperpage) { - echo ''; - $navigation = quiz_print_navigation_panel($questions, - $quiz->questionsperpage, $navigation); - } else { - $navigation = 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) - ? ' disabled="disabled" ' : ''; - - foreach ($questions as $question) { - - if (empty($question->qtype)) { // Just for robustness - continue; - } - - $questionorder[] = $question->id; - - if (0 == $navigation - || $navigation <= $nextquestionnumber - && $nextquestionnumber - $navigation < $quiz->questionsperpage) { - if ($results && isset($results->details[$question->id])) { - $details = $results->details[$question->id]; - } else { - $details = false; - } - - echo "
"; - print_simple_box_start("center", "90%"); - $nextquestionnumber = $QUIZ_QTYPES[$question->qtype]->print_question - ($nextquestionnumber, $quiz, $question, $readonly, $details); - print_simple_box_end(); - } else { - $nextquestionnumber += $QUIZ_QTYPES[$question->qtype] - ->actual_number_of_questions($question); - } - } - - if ($navigation) { - quiz_print_navigation_panel($questions, $quiz->questionsperpage, - $navigation); - } - 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 ""; - - if ($navigation && $quiz->questionsperpage) { - echo '"; - } - - return true; + $QUIZ_QTYPES[$question->qtype]->print_question($question, $state, $number, + $quiz, $options); } - - +/** +* Gets the default category in a course +* +* It returns the first category with no parent category. If no categories +* exist yet then one is created. +* @return object The default category +* @param integer $courseid The id of the course whose default category is wanted +*/ function quiz_get_default_category($courseid) { /// Returns the current category @@ -1051,64 +2056,6 @@ function quiz_get_category_menu($courseid, $published=false) { return $catmenu; } -function quiz_print_category_form($course, $current, $recurse=1, $showhidden=false) { -/// Prints a form to choose categories - -/// Make sure the default category exists for this course - if (!$categories = get_records("quiz_categories", "course", $course->id, "id ASC")) { - if (!$category = quiz_get_default_category($course->id)) { - notify("Error creating a default category!"); - } - } - -/// Get all the existing categories now - if (!$categories = get_records_select("quiz_categories", "course = '{$course->id}' OR publish = '1'", "parent, sortorder, name ASC")) { - notify("Could not find any question categories!"); - return false; // Something is really wrong - } - $categories = add_indented_names($categories); - foreach ($categories as $key => $category) { - if ($catcourse = get_record("course", "id", $category->course)) { - if ($category->publish && $category->course != $course->id) { - $category->indentedname .= " ($catcourse->shortname)"; - } - $catmenu[$category->id] = $category->indentedname; - } - } - $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, "", "", "", false, "self"); - echo ""; - echo "
"; - echo "id\" />"; - echo ""; - echo "
"; - echo '
'; - echo '
'; - echo ''; - echo ''; - print_string('recurse', 'quiz'); - // hide-feature - echo '
'; - echo ''; - print_string('showhidden', 'quiz'); - echo '
'; -} - - function add_indented_names(&$categories, $id = 0, $indent = 0) { // returns the categories with their names indented to show parent-child relationships $fillstr = '   '; @@ -1127,11 +2074,14 @@ function add_indented_names(&$categories, $id = 0, $indent = 0) { return $children; } - +/** +* Displays a select menu of categories with appended course names +* +* Optionaly non editable categories may be excluded. +* @author Howard Miller June '04 +*/ 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) { @@ -1173,22 +2123,35 @@ function quiz_get_category_coursename($category, $courseid = 0) { 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 +/** +* Creates an array of maximum grades for a quiz +* +* The grades are extracted from the quiz_question_instances table. +* @return array Array of grades indexed by question id +* These are the maximum possible grades that +* students can achieve for each of the questions +* @param integer $quiz The quiz object +*/ +function quiz_get_all_question_grades($quiz) { + global $CFG; + $questionlist = quiz_questions_in_quiz($quiz->questions); if (empty($questionlist)) { return array(); } - $questions = quiz_get_question_grades($quizid, $questionlist); + $instances = get_records_sql("SELECT question,grade,id + FROM {$CFG->prefix}quiz_question_instances + WHERE quiz = '$quiz->id'" . + (is_null($questionlist) ? '' : + "AND question IN ($questionlist)")); $list = explode(",", $questionlist); $grades = array(); foreach ($list as $qid) { - if (isset($questions[$qid])) { - $grades[$qid] = $questions[$qid]->grade; + if (isset($instances[$qid])) { + $grades[$qid] = $instances[$qid]->grade; } else { $grades[$qid] = 1; } @@ -1196,323 +2159,6 @@ function quiz_get_all_question_grades($questionlist, $quizid) { 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, $allowdelete=true, $quizid=0) { -// Prints a list of quiz questions in a small layout form with knobs -// returns sum of maximum grades -// $questionlist is comma-separated list -// $grades is an array of corresponding grades - - global $USER; - - if (!$questionlist) { - echo "

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

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

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

"; - return 0; - - } - - $strorder = get_string("order"); - $strquestionname = get_string("questionname", "quiz"); - $strgrade = get_string("grade"); - $strremove = get_string('remove', 'quiz'); - $stredit = get_string("edit"); - $straction = get_string("action"); - $strmoveup = get_string("moveup"); - $strmovedown = get_string("movedown"); - $strsavegrades = get_string("savegrades", "quiz"); - $strtype = get_string("type", "quiz"); - $strpreview = get_string("preview", "quiz"); - - $count = 0; - $sumgrade = 0; - $total = count($order); - echo "
"; - echo "sesskey\">"; - echo "\n"; - echo "\n"; - foreach ($order as $qnum) { - if (empty($questions[$qnum])) { - continue; - } - $question = $questions[$qnum]; - $canedit = isteacheredit($question->course); - $count++; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - - $sumgrade += $grades[$qnum]; - } - echo "\n"; - echo "
$strorder$strquestionname$strtype$strgrade$straction
$count"; - if ($count != 1) { - echo "sesskey\">\"$strmoveup\""; - } - echo ""; - if ($count != $total) { - echo "sesskey\">\"$strmovedown\""; - } - 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 ''; - - if ($canedit) { - $context = $quizid ? '&contextquiz='.$quizid : ''; - echo " - \"$strpreview\" "; - echo " - \"$stredit\" "; - if ($allowdelete) { - echo "sesskey\"> - \"$strremove\""; - } - } - echo "
\n"; - echo "\n"; - echo "\n"; - echo "\n"; - echo "$sumgrade"; - echo "\n
\n"; - echo "
\n"; - - return $sumgrade; -} - - -function quiz_print_cat_question_list($course, $categoryid, $quizselected=true, $recurse=1, $page, $perpage, $showhidden=false) { -// Prints the table of questions in a category with interactions - - global $QUIZ_QUESTION_TYPE, $USER; - - $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"); - $strselectnone = get_string("selectnone", "quiz"); - $strcreatenewquestion = get_string("createnewquestion", "quiz"); - $strquestionname = get_string("questionname", "quiz"); - $strdelete = get_string("delete"); - $strdeleteselected = get_string("deleteselected", 'quiz'); - $stredit = get_string("edit"); - $straction = get_string("action"); - $strrestore = get_string('restore'); - - $straddselectedtoquiz = get_string("addselectedtoquiz", "quiz"); - $straddtoquiz = get_string("addtoquiz", "quiz"); - $strtype = get_string("type", "quiz"); - $strcreatemultiple = get_string("createmultiple", "quiz"); - $strpreview = get_string("preview","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 ''; - } - else { - echo ''; - } - - echo ''; - - echo '
$strcreatenewquestion:'; - popup_form ("question.php?category=$category->id&qtype=", $QUIZ_QUESTION_TYPE, "addquestion", - "", "choose", "", "", false, "self"); - echo ''; - helpbutton("questiontypes", $strcreatenewquestion, "quiz"); - echo '
'; - print_string("publishedit","quiz"); - echo '
'; - if (isteacheredit($category->course)) { - echo ''.$strimportquestions.''; - helpbutton("import", $strimportquestions, "quiz"); - echo ' | '; - } - echo ''.$strexportquestions.''; - helpbutton("export", $strexportquestions, "quiz"); - echo '
'; - - echo '
'; - - $categorylist = ($recurse) ? quiz_categorylist($category->id) : $category->id; - - // hide-feature - $showhidden = $showhidden ? '' : " AND hidden = '0'"; - if (!$questions = get_records_select('quiz_questions', "category IN ($categorylist) AND qtype != '".RANDOM."'$showhidden", 'qtype, name ASC', '*', $page*$perpage, $perpage)) { - echo "

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

"; - return; - } - - $canedit = isteacheredit($category->course); - - echo '
'; - echo ''; - echo ''; - if ($canedit) { - echo ""; - } - echo ""; - echo "\n"; - foreach ($questions as $question) { - if ($question->qtype == RANDOM) { - //continue; - } - echo "\n"; - if ($canedit) { - echo "\n"; - } - if ($question->hidden) { - echo '\n"; - } else { - echo "\n"; - } - echo "\n"; - echo "\n"; - } - echo "
$straction$strquestionname$strtype
\n"; - if ($quizselected) { - echo "id&sesskey=$USER->sesskey\">\"$straddtoquiz\" "; - } - echo "id','$strpreview','scrollbars=yes,resizable=yes,width=700,height=480', false)\">\"$strpreview\" "; - echo "id\">\"$stredit\" "; - // hide-feature - if($question->hidden) { - echo "id&hide=0&sesskey=$USER->sesskey\">\"$strrestore\""; - } else { - echo "id&delete=$question->id\">\"$strdelete\""; - } - echo " id\" value=\"1\" />"; - echo "'.$question->name."".$question->name."\n"; - quiz_print_question_icon($question, $canedit); - echo "
\n"; - - echo ''.$strselectall.' /'. - ' '.$strselectnone.''; - echo " \n"; - echo "
"; - - echo '
'; - if ($quizselected) { - echo "\n"; - echo ''; - } - echo '\n"; - quiz_category_select_menu($course->id, false, true, $category->id); - echo "
"; - - $numquestions = count_records_select('quiz_questions', "category IN ($categorylist) AND qtype != '".RANDOM."'"); - echo ''; - print_paging_bar($numquestions, $page, $perpage, - "edit.php?perpage=$perpage&"); - - echo "
\n"; - if ($quizselected) { - for ($i=1;$i<=10; $i++) { - $randomcount[$i] = $i; - } - echo '
'; - echo "sesskey\">"; - print_string('addrandom1', 'quiz'); - choose_from_menu($randomcount, 'randomcreate', '1', ''); - print_string('addrandom2', 'quiz'); - // Don't offer the option to change the grade - //choose_from_menu($randomcount, 'randomgrade', '1', ''); - echo ''; - echo ''; - echo "id\" />"; - echo ' '; - helpbutton('random', get_string('random', 'quiz'), 'quiz'); - echo '
'; - } -} - - -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); @@ -1525,22 +2171,6 @@ function quiz_get_user_attempts($quizid, $userid) { } -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,$quiz->decimalpoints); - foreach ($attempts as $attempt) { - $attemptgrade = format_float(($attempt->sumgrades / $quiz->sumgrades) * $quiz->grade, $quiz->decimalpoints); - if ($attemptgrade == $bestgrade) { - $userattempts[] = "id&attempt=$attempt->id\">$attemptgrade"; - } else { - $userattempts[] = "id&attempt=$attempt->id\">$attemptgrade"; - } - } - return implode(",", $userattempts); -} - function quiz_get_best_grade($quiz, $userid) { /// Get the best current grade for a particular user in a quiz if (!$grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) { @@ -1550,19 +2180,36 @@ if (!$grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) { return (format_float($grade->grade,$quiz->decimalpoints)); } -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. +/** +* TODO: document this +* +* @return boolean Indicates success or failure. +* @param object $quiz The quiz for which the best grade is to be calculated +* and then saved. +* @param integer $userid The id of the user to save the best grade for. Can be +* null in which case the current user is assumed. +*/ +function quiz_save_best_grade($quiz, $userid=null) { + global $USER; + // Assume the current user if $userid is null + if (is_null($userid)) { + $userid = $USER->id; + } + + // Get all the attempts made by the user if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) { notify('Could not find any user attempts'); return false; } + // Calculate the best grade $bestgrade = quiz_calculate_best_grade($quiz, $attempts); $bestgrade = (($bestgrade / $quiz->sumgrades) * $quiz->grade); - if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', $userid)) { + // Save the best grade in the database + if ($grade = get_record('quiz_grades', 'quiz', $quiz->id, 'userid', + $userid)) { $grade->grade = round($bestgrade, $quiz->decimalpoints); $grade->timemodified = time(); if (!update_record('quiz_grades', $grade)) { @@ -1588,19 +2235,19 @@ function quiz_calculate_best_grade($quiz, $attempts) { switch ($quiz->grademethod) { - case ATTEMPTFIRST: + case QUIZ_ATTEMPTFIRST: foreach ($attempts as $attempt) { return $attempt->sumgrades; } break; - case ATTEMPTLAST: + case QUIZ_ATTEMPTLAST: foreach ($attempts as $attempt) { $final = $attempt->sumgrades; } return $final; - case GRADEAVERAGE: + case QUIZ_GRADEAVERAGE: $sum = 0; $count = 0; foreach ($attempts as $attempt) { @@ -1610,7 +2257,7 @@ function quiz_calculate_best_grade($quiz, $attempts) { return (float)$sum/$count; default: - case GRADEHIGHEST: + case QUIZ_GRADEHIGHEST: $max = 0; foreach ($attempts as $attempt) { if ($attempt->sumgrades > $max) { @@ -1627,21 +2274,21 @@ function quiz_calculate_best_attempt($quiz, $attempts) { switch ($quiz->grademethod) { - case ATTEMPTFIRST: + case QUIZ_ATTEMPTFIRST: foreach ($attempts as $attempt) { return $attempt; } break; - case GRADEAVERAGE: // need to do something with it :-) - case ATTEMPTLAST: + case QUIZ_GRADEAVERAGE: // need to do something with it :-) + case QUIZ_ATTEMPTLAST: foreach ($attempts as $attempt) { $final = $attempt; } return $final; default: - case GRADEHIGHEST: + case QUIZ_GRADEHIGHEST: $max = -1; foreach ($attempts as $attempt) { if ($attempt->sumgrades > $max) { @@ -1654,70 +2301,8 @@ function quiz_calculate_best_attempt($quiz, $attempts) { } -function quiz_save_attempt($quiz, $questions, $result, - $attemptnum, $finished = true) { -/// 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; - } - - // Not usually necessary, but there's some sort of very rare glitch - // I've seen where the number wasn't already the same. In these cases - // We upgrade the database to match the attemptnum we calculated - $attempt->attempt = $attemptnum; - - // Now let's complete this record and save it - - $attempt->sumgrades = $result->sumgrades; - if ($finished) { - $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) { - - // Fetch the response record for this question... - $response = get_record('quiz_responses', - 'attempt', $attempt->id, 'question', $question->id); - - $response->grade = $result->details[$question->id]->grade; - - if (!empty($question->response)) { - $responseanswerfield = $QUIZ_QTYPES[$question->qtype] - ->convert_to_response_answer_field($question->response); - - $response->answer = $responseanswerfield; - - } else if (!isset($response->answer)) { - $response->answer = ''; - } - - if (!update_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 +/// Convenience function that is used by some single-response /// question-types for determining correct answers. $bestanswerfraction = 0.0; @@ -1733,90 +2318,6 @@ function quiz_extract_correctanswers($answers, $nameprefix) { return $correctanswers; } -function quiz_grade_responses($quiz, $questions, $attemptid=0) { -/// 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 -/// 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 specific question) -/// []->answers[] (result answer records for the question response(s)) -/// []->correctanswers[] (answer records if question response(s) had been correct) -/// - HOWEVER, ->answers[] and ->correctanswers[] are supplied only -/// if there is a response on the question... -/// The array ->answers[] is indexed like ->response[] 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 concatenated with the ->id -/// of its answer record. - - global $QUIZ_QTYPES; - - if (!$questions) { - error("No questions!"); - } - - $result->sumgrades = 0.0; - foreach ($questions as $qid => $question) { - - if (!isset($question->response) && $attemptid) { - /// No response on the question - /// This case is common if the quiz shows a limited - /// number of questions per page. - $response = get_record('quiz_responses', 'attempt', - $attemptid, 'question', $qid); - $resultdetails->grade = $response->grade; - - } else if (empty($question->qtype)) { - continue; - - } else { - - $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; -} - - // this function creates default export filename function default_export_filename($course,$category) { //Take off some characters in the filename !! @@ -1858,8 +2359,11 @@ function default_export_filename($course,$category) { return $export_name; } -// function to read all questions for category into big array -// added by Howard Miller June 2004 +/** +* Function to read all questions for category into big array +* +* @author added by Howard Miller June 2004 +*/ function get_questions_category( $category ) { // questions will be added to an array @@ -1878,35 +2382,41 @@ function get_questions_category( $category ) { 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 +/** +* Get question data for export +* +* @todo This really needs to be handled by the question types rather +* than by the switch statement below. +* @author presumably Howard Miller +* 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) { @@ -1959,12 +2469,9 @@ function get_question_data( $question ) { return $question; } -// function to return single answer -// ->id answer id -// ->question question number -// ->answer -// ->fraction -// ->feedback +/** +* function to return single record from quiz_answers table +*/ function get_exp_answer( $id ) { if (!$answer = get_record("quiz_answers","id",$id )) { error( "quiz_answers record $id not found" ); @@ -1972,7 +2479,9 @@ function get_exp_answer( $id ) { return $answer; } -// function to return array of answers for export +/** +* 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" ); @@ -1980,6 +2489,9 @@ function get_exp_answers( $question_num ) { return $answers; } +/** +* Returns a comma separated list of ids of the category and all subcategories +*/ function quiz_categorylist($categoryid) { // returns a comma separated list of ids of the category and all subcategories $categorylist = $categoryid; @@ -1991,20 +2503,28 @@ function quiz_categorylist($categoryid) { return $categorylist; } + +/** +* Array of names of quizzes a question appears in +* +* @return array Array of quiz names +* @param integer Question id +*/ function quizzes_question_used($id) { -// $id = question id -// returns array of names of quizzes a question appears in - + $quizlist = array(); - if ($grades = get_records('quiz_question_grades', 'question', $id)) { - foreach($grades as $grade) { - $quizlist[$grade->quiz] = get_field('quiz', 'name', 'id', $grade->quiz); + if ($instances = get_records('quiz_question_instances', 'question', $id)) { + foreach($instances as $instance) { + $quizlist[$instance->quiz] = get_field('quiz', 'name', 'id', $instance->quiz); } } - + return $quizlist; } +/** +* Parse field names used for the replace options on question edit forms +*/ function quiz_parse_fieldname($name, $nameprefix='question') { $reg = array(); if (preg_match("/$nameprefix(\\d+)(\w+)/", $name, $reg)) { @@ -2014,6 +2534,88 @@ function quiz_parse_fieldname($name, $nameprefix='question') { } } + +/** +* Determine render options +*/ +function quiz_get_renderoptions($quiz, $state) { + // Show the question in readonly (review) mode if the quiz is in + // the closed state + $options->readonly = QUIZ_EVENTCLOSE === $state->event; + + // Show feedback once the question has been graded (if allowed by the quiz) + $options->feedback = ('' !== $state->grade) && ($quiz->review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY); + + // Show validation only after a validation event + $options->validation = QUIZ_EVENTVALIDATE === $state->event; + + // Show correct responses in readonly mode if the quiz allows it + $options->correct_responses = $options->readonly && ($quiz->review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY); + + // Always show responses and scores + $options->responses = true; + $options->scores = true; + + return $options; +} + + +/** +* Determine review options +*/ +function quiz_get_reviewoptions($quiz, $attempt, $isteacher=false) { + $options->readonly = true; + if ($isteacher and !$attempt->preview) { + // The teacher should be shown everything except during preview when the teachers + // wants to see just what the students see + $options->responses = true; + $options->scores = true; + $options->feedback = true; + $options->correct_responses = true; + $options->solutions = false; + return $options; + } + if ((time() - $attempt->timefinish) < 120) { + $options->responses = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_RESPONSES) ? 1 : 0; + $options->scores = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_SCORES) ? 1 : 0; + $options->feedback = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_FEEDBACK) ? 1 : 0; + $options->correct_responses = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_ANSWERS) ? 1 : 0; + $options->solutions = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0; + } else if (time() < $quiz->timeclose) { + $options->responses = ($quiz->review & QUIZ_REVIEW_OPEN & QUIZ_REVIEW_RESPONSES) ? 1 : 0; + $options->scores = ($quiz->review & QUIZ_REVIEW_OPEN & QUIZ_REVIEW_SCORES) ? 1 : 0; + $options->feedback = ($quiz->review & QUIZ_REVIEW_OPEN & QUIZ_REVIEW_FEEDBACK) ? 1 : 0; + $options->correct_responses = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_ANSWERS) ? 1 : 0; + $options->solutions = ($quiz->review & QUIZ_REVIEW_OPEN & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0; + } else { + $options->responses = ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_RESPONSES) ? 1 : 0; + $options->scores = ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_SCORES) ? 1 : 0; + $options->feedback = ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_FEEDBACK) ? 1 : 0; + $options->correct_responses = ($quiz->review & QUIZ_REVIEW_IMMEDIATELY & QUIZ_REVIEW_ANSWERS) ? 1 : 0; + $options->solutions = ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0; + } + return $options; +} + +/** +* Upgrade states for an attempt to Moodle 1.5 model +* +* @param object $attempt The attempt whose states need upgrading +*/ +function quiz_upgrade_states($attempt) { + global $CFG; + execute_sql("UPDATE {$CFG->prefix}quiz_states SET timestamp = '$attempt->timemodified' WHERE attempt = '$attempt->id'", false); + $newest->attemptid = $attempt->id; + if ($states = get_records('quiz_states', 'attempt', $attempt->id)) { + foreach ($states as $state) { + $newest->newgraded = $state->id; + $newest->new = $state->id; + $newest->questionid = $state->question; + insert_record('quiz_newest_states', $newest, false); + } + } +} + /** * Get list of available import or export formats **/ diff --git a/mod/quiz/mod.html b/mod/quiz/mod.html index 3a4afd13ac7..79eaff979ba 100644 --- a/mod/quiz/mod.html +++ b/mod/quiz/mod.html @@ -1,9 +1,38 @@ + + + dirroot/mod/quiz/locallib.php"); -// Ensure that all form variables are initialized +/// Ensure that all form variables are initialized if (!isset($form->name)) { $form->name = ""; } @@ -28,17 +57,11 @@ if (!isset($form->decimalpoints)) { $form->decimalpoints = $CFG->quiz_decimalpoints; } - if (!isset($form->feedback)) { - $form->feedback = $CFG->quiz_showfeedback; - } - if (!isset($form->correctanswers)) { - $form->correctanswers = $CFG->quiz_showanswer; - } if (!isset($form->review)) { - $form->review = $CFG->quiz_allowreview; + $form->review = $CFG->quiz_review; } if (!isset($form->questionsperpage)) { - $form->questionsperpage = 0; + $form->questionsperpage = $CFG->quiz_questionsperpage; } if (!isset($form->shufflequestions)) { $form->shufflequestions = $CFG->quiz_shufflequestions; @@ -64,15 +87,16 @@ if (!isset($form->popup)) { $form->popup = $CFG->quiz_popup; } + if (!isset($form->optionflags)) { + $form->optionflags = $CFG->quiz_optionflags; + } + if (!isset($form->penaltyscheme)) { + $form->penaltyscheme = $CFG->quiz_penaltyscheme; + } $fix = 0; // This will later be set to 1 if some of the variables have been fixed by the admin. // the following are used for drop-down menus - $reviewoptions = array(); - $reviewoptions[0] = get_string('reviewnever', 'quiz'); - $reviewoptions[1] = get_string('reviewafter', 'quiz'); - $reviewoptions[2] = get_string('reviewbefore', 'quiz'); - $reviewoptions[3] = get_string('reviewalways', 'quiz'); $yesnooptions = array(); $yesnooptions[0] = get_string("no"); @@ -85,11 +109,11 @@ $attemptoptions[$i] = "$i ".strtolower(get_string("attempts", "quiz")); } - $grades = array(); - for ($i=100; $i>=1; $i--) { - $grades[$i] = $i; - } - $grades[0] = get_string("nograde"); + $penaltyoptions = array(); + $penaltyoptions[QUIZ_PENALTYNONE] = get_string('penaltynone', 'quiz'); + $penaltyoptions[QUIZ_PENALTYMULTIPLY] = get_string('penaltymultiply', 'quiz'); + $penaltyoptions[QUIZ_PENALTYSUBTRACT] = get_string('penaltysubtract', 'quiz'); + ?> @@ -220,7 +244,7 @@ - + quiz_fix_grademethod) { ?> : @@ -248,6 +272,30 @@ +quiz_fix_adaptive) { ?> + + : + + optionflags & QUIZ_ADAPTIVE, ""); + helpbutton("adaptive", get_string("adaptive","quiz"), "quiz"); + ?> + + + + +quiz_fix_penaltyscheme) { ?> + + : + + penaltyscheme", ""); + helpbutton("penaltyscheme", get_string("penaltyscheme","quiz"), "quiz"); + ?> + + + + quiz_fix_decimalpoints) { ?> : @@ -265,53 +313,11 @@ -quiz_fix_showfeedback) { ?> - - : - - feedback", ""); - helpbutton("feedback", get_string("showfeedback","quiz"), "quiz"); - ?> - - - - -quiz_fix_showanswer) { ?> - - : - - correctanswers", ""); - helpbutton("correctanswers", get_string("showcorrectanswer","quiz"), "quiz"); - ?> - - - - -quiz_fix_allowreview) { ?> - - : - - review", ""); - helpbutton("review2", get_string("allowreview","quiz"), "quiz"); - ?> - - - - -quiz_fix_maximumgrade) { ?> - - : - - grade", ""); - helpbutton("maxgrade", get_string("maximumgrade"), "quiz"); - ?> - - - +quiz_fix_review) { + echo ''; + include('reviewoptions.html'); + echo ''; +} else $fix=1 ?> quiz_fix_popup) { ?> @@ -352,35 +358,6 @@ - - - @@ -466,7 +443,7 @@ - + quiz_fix_grademethod) { ?> : @@ -494,6 +471,30 @@ +quiz_fix_adaptive) { $fix = 1; ?> + + : + + optionflags & QUIZ_ADAPTIVE, ""); + helpbutton("adaptive", get_string("adaptive","quiz"), "quiz"); + ?> + + + + +quiz_fix_penaltyscheme) { $fix = 1; ?> + + : + + penaltyscheme", ""); + helpbutton("penaltyscheme", get_string("penaltyscheme","quiz"), "quiz"); + ?> + + + + quiz_fix_decimalpoints) { ?> : @@ -511,53 +512,11 @@ -quiz_fix_showfeedback) { ?> - - : - - feedback", ""); - helpbutton("feedback", get_string("showfeedback","quiz"), "quiz"); - ?> - - - - -quiz_fix_showanswer) { ?> - - : - - correctanswers", ""); - helpbutton("correctanswers", get_string("showcorrectanswer","quiz"), "quiz"); - ?> - - - - -quiz_fix_allowreview) { ?> - - : - - review", ""); - helpbutton("review", get_string("allowreview","quiz"), "quiz"); - ?> - - - - -quiz_fix_maximumgrade) { ?> - - : - - grade", ""); - helpbutton("maxgrade", get_string("maximumgrade"), "quiz"); - ?> - - - +quiz_fix_review) { + echo ''; + include('reviewoptions.html'); + echo ''; +} ?> quiz_fix_popup) { ?> @@ -591,6 +550,8 @@ + +
@@ -602,6 +563,7 @@
+ diff --git a/mod/quiz/multiple.php b/mod/quiz/multiple.php deleted file mode 100644 index 11f4676927b..00000000000 --- a/mod/quiz/multiple.php +++ /dev/null @@ -1,173 +0,0 @@ -modform)) { - error('You have used this page incorrectly!'); - } else { - $modform = $SESSION->modform; - } - - if (! $category = get_record('quiz_categories', 'id', $category)) { - error('Course ID is incorrect'); - } - - if (! $course = get_record('course', 'id', $category->course)) { - error('Course ID is incorrect'); - } - - require_login($course->id, false); - - if (!isteacher($course->id)) { - error('Only teachers can use this page!'); - } - - -/// If data submitted, then process and store. - - if ($form = data_submitted() and confirm_sesskey()) { - if ($form->randomcreate > 0) { - $newquestionids = array(); // this will hold the ids of the random questions - - // find existing random questions in this category - $random = RANDOM; - if ($existingquestions = get_records_select('quiz_questions', "qtype = '$random' AND category = '$category->id'")) { - // now remove the ones that are already used in this quiz - if ($questionids = explode(',', $modform->questions)) { - foreach ($questionids as $questionid) { - unset($existingquestions[$questionid]); - } - } - // now take as many of these as needed - $i = 0; - while (($existingquestion = array_pop($existingquestions)) and ($i < $form->randomcreate)) { - if ($existingquestion->questiontext == "$form->recurse") { - // this question has the right recurse property, so use it - $newquestionids[] = $existingquestion->id; - $i++; - } - } - $randomcreate = $form->randomcreate - $i; // the number of additional random questions needed. - } else { - $randomcreate = $form->randomcreate; - } - - if ($randomcreate > 0) { - - $question->qtype = RANDOM; - $question->category = $category->id; - $question->name = get_string('random', 'quiz') .' ('. $category->name .')'; - $question->questiontext = "$form->recurse"; // we use the questiontext field to store the info - // on whether to include questions in subcategories - $question->image = ''; - $question->defaultgrade = $form->randomgrade; - $question->hidden = 1; - for ($i=0; $i<$randomcreate; $i++) { - $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) - if (!$newquestionids[] = insert_record('quiz_questions', $question)) { - error('Could not insert new random question!'); - } - } - } - - // Add them to the quiz - if (!empty($modform->questions)) { - $questionids = explode(',', $modform->questions); - } else { - $questionids = array(); - } - - foreach ($newquestionids as $newquestionid) { - $modform->grades[$newquestionid] = $form->randomgrade; - $modform->sumgrades += $form->randomgrade; - } - - $newquestionids = array_merge($questionids, $newquestionids); - $modform->questions = implode(',', $newquestionids); - $SESSION->modform = $modform; - if (!set_field('quiz', 'questions', $modform->questions, 'id', $modform->instance)) { - error('Could not save question list'); - } - quiz_questiongrades_update($modform->grades, $modform->instance); - } - redirect('edit.php'); - } - - -/// Otherwise print the form - -/// Print headings - - $strquestions = get_string('questions', 'quiz'); - $strpublish = get_string('publish', 'quiz'); - $strdelete = get_string('delete'); - $straction = get_string('action'); - $stradd = get_string('add'); - $strcancel = get_string('cancel'); - $strsavechanges = get_string('savechanges'); - $strbacktoquiz = get_string('backtoquiz', 'quiz'); - $strquizzes = get_string('modulenameplural', 'quiz'); - $streditingquiz = get_string(isset($SESSION->modform->instance) ? 'editingquiz' : 'editquestions', 'quiz'); - $strcreatemultiple = get_string('createmultiple', 'quiz'); - - print_header_simple($strcreatemultiple, $strcreatemultiple, - "wwwroot/mod/quiz/index.php?id=$course->id\">$strquizzes". - " -> $streditingquiz -> $strcreatemultiple"); - - - print_heading_with_help($strcreatemultiple, 'createmultiple', 'quiz'); - - if (!$categories = quiz_get_category_menu($course->id, false)) { - error('No categories!'); - } - - for ($i=1;$i<=100; $i++) { - $randomcount[$i] = $i; - } - for ($i=1;$i<=10; $i++) { - $gradecount[$i] = $i; - } - $options = array(); - $options[0] = get_string('no'); - $options[1] = get_string('yes'); - - print_simple_box_start('center'); - echo '
'; - echo "sesskey\">"; - echo ''; - echo ''; - - echo ''; - - echo ''; - - echo ''; - echo '
'; - print_string('category', 'quiz'); - echo ':'; - // choose_from_menu($categories, "category", "$category->id", ""); - quiz_category_select_menu($course->id, false, true, $category->id ); - echo '
'; - print_string('randomcreate', 'quiz'); - echo ':'; - choose_from_menu($randomcount, 'randomcreate', '10', ''); - echo '
'; - print_string('defaultgrade', 'quiz'); - echo ':'; - choose_from_menu($gradecount, 'randomgrade', '1', ''); - echo '
 '; - echo ' '; - echo ' '; - echo '
'; - echo '
'; - print_simple_box_end(); - - print_footer($course); - -?> diff --git a/mod/quiz/pagelib.php b/mod/quiz/pagelib.php index a953f891439..ed5f6c91a20 100644 --- a/mod/quiz/pagelib.php +++ b/mod/quiz/pagelib.php @@ -63,9 +63,7 @@ class page_quiz extends page_generic_activity { } if(empty($morebreadcrumbs) && $this->user_allowed_editing()) { - $buttons = '
'. - ''. - '
'. + $buttons = ''; if(!empty($CFG->showblocksonmodpages)) { $buttons .= '
'. update_module_button($this->modulerecord->id, $this->courserecord->id, get_string('modulename', 'quiz')).'
'. @@ -87,4 +85,4 @@ class page_quiz extends page_generic_activity { } } -?> +?> \ No newline at end of file diff --git a/mod/quiz/preview.php b/mod/quiz/preview.php index 136c6b2353a..ccafbb1d83e 100644 --- a/mod/quiz/preview.php +++ b/mod/quiz/preview.php @@ -1,104 +1,225 @@ category)) { - error("This question doesn't belong to a valid category!"); -} -if ($quizid) { - if (! $quiz = get_record('quiz', 'id', $quizid)) { - error('Incorrect quiz id'); + $id = required_param('id', PARAM_INT); // question id + // if no quiz id is specified then a dummy quiz with default options is used + $quizid = optional_param('quizid', 0, PARAM_INT); + // Test if we are continuing an attempt at a question + $continue = optional_param('continue', 0, PARAM_BOOL); + // Check for any of the submit buttons + $fillcorrect = optional_param('fillcorrect', 0, PARAM_BOOL); + $markall = optional_param('markall', 0, PARAM_BOOL); + $finishattempt = optional_param('finishattempt', 0, PARAM_BOOL); + $back = optional_param('back', 0, PARAM_BOOL); + $startagain = optional_param('startagain', 0, PARAM_BOOL); + // We are always continuing an attempt if a submit button was pressed with the + // exception of the start again button + if ($fillcorrect || $markall || $finishattempt || $back) { + $continue = true; + } else if ($startagain) { + $continue = false; + } + + require_login(); + + if (!isteacherinanycourse()) { + error('This page is for teachers only'); + } + + if (!$continue) { + // Start a new attempt; delete the old session + unset($SESSION->quizpreview); + // Redirect to ourselves but with continue=1; prevents refreshing the page + // from restarting an attempt (needed so that random questions don't change) + $quizid = $quizid ? '&quizid=' . $quizid : ''; + redirect($CFG->wwwroot . '/mod/quiz/preview.php?id=' . $id . $quizid . + '&continue=1'); + } + + if (empty($quizid)) { + // get a sample quiz to be used as a skeleton + // this should really be done properly by instantiating a quiz object + if (!$quiz = get_records('quiz')) { + error('You have to create at least one quiz before using this preview'); + } + $quiz = array_values($quiz); + $quiz = $quiz[0]; + + // set everything to the default values + foreach($quiz as $field => $value) { + $quizfield = "quiz_".$field; + if(isset($CFG->$quizfield)) { + $quiz->$field = $CFG->$quizfield; + } + else { + $quiz->$field = 0; + } + } + + } else if (!$quiz = get_record('quiz', 'id', $quizid)) { + error("Quiz id $quizid does not exist"); + } + + $quiz->id = 0; // just for safety + $quiz->questions = $id; + + // Load the question information + if (!$questions = get_records('quiz_questions', 'id', $id)) { + error('Could not load question'); + } + $questions[$id]->quiz = 0; + $questions[$id]->maxgrade = 1; + + if (!$category = get_record("quiz_categories", "id", $questions[$id]->category)) { + error("This question doesn't belong to a valid category!"); + } + + if (!isteacher($category->course) and !$category->publish) { + error("You can't preview these questions!"); } -} else { - // make fake quiz - $quiz->id = 0; $quiz->course = $category->course; - $quiz->name = ""; - $quiz->intro = ""; - $quiz->timeopen = 0; - $quiz->timeclose = 0; - $quiz->attempts = $CFG->quiz_attempts; - $quiz->attemptonlast = $CFG->quiz_attemptonlast; - $quiz->feedback = $CFG->quiz_showfeedback; - $quiz->correctanswers = $CFG->quiz_showanswer; - $quiz->grademethod = $CFG->quiz_grademethod; - $quiz->review = $CFG->quiz_allowreview; - $quiz->shufflequestions = $CFG->quiz_shufflequestions; - $quiz->shuffleanswers = $CFG->quiz_shuffleanswers; - $quiz->questions = "$question->id"; - $quiz->sumgrades = $question->defaultgrade; - $quiz->grade = 0; - $quiz->timecreated = 0; - $quiz->timemodified = 0; - $quiz->timelimit = $CFG->quiz_timelimit; - $quiz->password = $CFG->quiz_password; - $quiz->subnet = $CFG->quiz_subnet; - $quiz->popup = $CFG->quiz_popup; -} -$qtype = $question->qtype; - -require_login(); - -if (!isteacherinanycourse()) { - error('This page is for teachers only'); -} - -if (!isteacher($category->course) and !$category->publish) { - error("You can't preview these questions!"); -} - -print_header(); -print_heading(get_string("previewquestion","quiz")); - -if ($rawanswers = data_submitted()) { - $rawanswers = (array)$rawanswers; - - foreach ($rawanswers as $key => $value) { // Parse input for question->response - $postedId = quiz_extract_posted_id($key); - if ($id == $postedId) $question->response[$key] = trim($value); + // Load the question type specific information + if (!quiz_get_question_options($questions)) { + error(get_string('newattemptfail', 'quiz')); } - print_simple_box_start("center", "90%"); - - $resultdetails = $QUIZ_QTYPES[$question->qtype]->grade_response($question, quiz_qtype_nameprefix($question)); - - $question->maxgrade = 1; - $quiz->correctanswers=1; - $quiz->feedback=1; - $quiz->grade=1; - $QUIZ_QTYPES[$question->qtype]->print_question(1, $quiz, $question, true, $resultdetails); + // Create a dummy quiz attempt + $attempt = quiz_create_attempt($quiz, 0); + $attempt->id = 0; - print_simple_box_end(); - echo "

\n"; - echo "

\n"; + // Restore the history of question sessions from the moodle session or create + // new sessions. Make $states a reference to the states array in the moodle + // session. + if (isset($SESSION->quizpreview->states) and $SESSION->quizpreview->questionid + == $id) { + // Reload the question session history from the moodle session + $states =& $SESSION->quizpreview->states; + $historylength = count($states) - 1; + if ($back && $historylength > 0) { + // Go back one step in the history + unset($states[$historylength]); + $historylength--; + } + } else { + // Record the question id in the moodle session + $SESSION->quizpreview->questionid = $id; + // Create an empty session for the question + if (!$newstates = + quiz_restore_question_sessions($questions, $quiz, $attempt)) { + error(get_string('newattemptfail', 'quiz')); + } + $SESSION->quizpreview->states = array($newstates); + $states =& $SESSION->quizpreview->states; + $historylength = 0; + } -} else { //show question list + if (!$fillcorrect && !$back && ($form = data_submitted())) { + $form = (array)$form; + $submitted = true; + + // Create a new item in the history of question states (don't simplify!) + $states[$historylength + 1] = array(); + $states[$historylength + 1][$id] = clone($states[$historylength][$id]); + $historylength++; + $curstate =& $states[$historylength][$id]; + + // Process the responses + unset($form['id']); + unset($form['quizid']); + unset($form['continue']); + unset($form['markall']); + unset($form['finishattempt']); + unset($form['back']); + unset($form['startagain']); + + $event = $finishattempt ? QUIZ_EVENTCLOSE : ($markall ? QUIZ_EVENTGRADE : QUIZ_EVENTSAVE); + if ($actions = quiz_extract_responses($questions, $form, $event)) { + quiz_process_responses($questions[$id], $states[$historylength][$id], $actions[$id], $quiz, $attempt); + if (QUIZ_EVENTGRADE != $curstate->event && QUIZ_EVENTCLOSE != $curstate->event) { + // Update the current state rather than creating a new one + $historylength--; + unset($states[$historylength]); + $states = array_values($states); + $curstate =& $states[$historylength][$id]; + } + } + } else { + $submitted = false; + $curstate =& $states[$historylength][$id]; + } + + $options = quiz_get_renderoptions($quiz, $curstate); + + // Fill in the correct responses (unless the question is in readonly mode) + if ($fillcorrect && !$options->readonly) { + $curstate->responses = $QUIZ_QTYPES[$questions[$id]->qtype] + ->get_correct_responses($questions[$id], $curstate); + } + + $strpreview = get_string('previewquestion', 'quiz'); + print_header($strpreview); + print_heading($strpreview); + + echo '

' . get_string('modulename', 'quiz') . ': '; + if (empty($quizid)) { + echo '[' . get_string('default', 'quiz') . ']'; + } else { + p($quiz->name); + } + echo '
'.get_string('question', 'quiz').': '; + p($questions[$id]->name); + echo "

\n"; + $number = 1; echo "\n"; echo "\n"; + echo "\n"; + echo "\n"; - print_simple_box_start("center", "90%"); - $readonly = false; - $resultdetails = NULL; - $QUIZ_QTYPES[$question->qtype]->print_question(1, $quiz, $question, $readonly, $resultdetails); - print_simple_box_end(); + quiz_print_quiz_question($questions[$id], $curstate, $number, $quiz, $options); - echo "
"; - echo "
"; - echo "\n"; - echo ""; - echo "
"; - echo "
"; -} - -print_footer(); + echo '
'; + echo '
'; + // Print the mark and finish attempt buttons + echo '\n"; + echo '\n"; + echo '
'; + echo '
'; + // Print the fill correct button (unless the question is in readonly mode) + if (!$options->readonly) { + echo '\n"; + } + // Print the navigation buttons + if ($historylength > 0) { + echo '\n"; + } + // Print the start again button + echo '\n"; + // Print the close window button + echo '"; + echo '
'; + echo ''; + print_footer(); ?> diff --git a/mod/quiz/question.php b/mod/quiz/question.php index b9409c0e491..4ced5e952f3 100644 --- a/mod/quiz/question.php +++ b/mod/quiz/question.php @@ -1,14 +1,22 @@ id) or - record_exists('quiz_responses', 'originalquestion', $question->id) or - record_exists('quiz_question_grades', 'question', $question->id)) { + if (record_exists('quiz_question_instances', 'question', $question->id) or + record_exists('quiz_states', 'originalquestion', $question->id)) { if (!set_field('quiz_questions', 'hidden', 1, 'id', $delete)) { error('Was not able to hide question'); } @@ -103,20 +110,23 @@ if (!delete_records("quiz_questions", "id", $question->id)) { error("An error occurred trying to delete question (id $question->id)"); } + if (!delete_records("quiz_questions", "parent", $question->id)) { + error("An error occurred trying to delete question (id $question->id)"); + } } redirect("edit.php"); } else { error("Confirmation string was incorrect"); } - + } else { if ($quiznames = quizzes_question_used($id)) { $a->questionname = $question->name; $a->quiznames = implode(', ', $quiznames); notify(get_string('questioninuse', 'quiz', $a)); } - - notice_yesno(get_string("deletequestioncheck", "quiz", $question->name), + + notice_yesno(get_string("deletequestioncheck", "quiz", $question->name), "question.php?sesskey=$USER->sesskey&id=$question->id&delete=$delete&confirm=".md5($delete), "edit.php"); } print_footer($course); @@ -124,12 +134,13 @@ } if ($form = data_submitted() and confirm_sesskey()) { - - if (isset($form->versioning)) { // use new code that - // handles whether to overwrite or copy a question and keeps - // track of the versions in the quiz_question_versions table - - // $replaceinquiz is an array with the ids of all quizzes in which the teacher has chosen to replace the old version + + if (isset($form->versioning) && isset($question->id)) { + // use new code that handles whether to overwrite or copy a question + // and keeps track of the versions in the quiz_question_version table + + // $replaceinquiz is an array with the ids of all quizzes in which + // the teacher has chosen to replace the old version $replaceinquiz = array(); foreach($form as $key => $val) { if ($tmp = quiz_parse_fieldname($key, 'q')) { @@ -142,23 +153,28 @@ // $quizlist is an array with the ids of quizzes which use this question $quizlist = array(); - if ($grades = get_records('quiz_question_grades', 'question', $form->id)) { - foreach($grades as $grade) { - $quizlist[$grade->quiz] = $grade->quiz; + if ($instances = get_records('quiz_question_instances', 'question', $question->id)) { + foreach($instances as $instance) { + $quizlist[$instance->quiz] = $instance->quiz; } } - + if (isset($form->makecopy)) { // explicitly requested copies should be unhidden $question->hidden = 0; } // Logic to determine whether old version should be overwritten - $makecopy = isset($form->makecopy) || (!$form->id); unset($form->makecopy); - $noresponses = $makecopy || !(record_exists('quiz_responses', 'question', $form->id) or - record_exists('quiz_responses', 'originalquestion', $form->id)); // Should be improved once the preview flag exists so that teacher preview responses are not taken into account - $notused = $makecopy || !record_exists('quiz_question_grades', 'question', $form->id); - $replaceinall = ($quizlist == $replaceinquiz); // question is being replaced in all quizzes - $replaceold = (!$makecopy && $noresponses && ($notused || $replaceinall)); + $makecopy = isset($form->makecopy) || (!$form->id); unset($form->makecopy); + if ($makecopy) { + $replaceold = false; + } else { + // this should be improved to exclude teacher preview responses and empty responses + // the current code leaves many unneeded questions in the database + $hasresponses = record_exists('quiz_states', 'question', $form->id) or + record_exists('quiz_states', 'originalquestion', $form->id); + $replaceinall = ($quizlist == $replaceinquiz); // question is being replaced in all quizzes + $replaceold = !$hasresponses && $replaceinall; + } if (!$replaceold) { // create a new question $oldquestionid = $question->id; @@ -169,10 +185,8 @@ } unset($question->id); } - unset($makecopy, $noresponses, $notused, $replaceinall, $replaceold); - + unset($makecopy, $hasresponses, $replaceinall, $replaceold); $question = $QUIZ_QTYPES[$qtype]->save_question($question, $form, $course); - if(!isset($question->id)) { error("Failed to save the question!"); } @@ -182,7 +196,7 @@ $version = new object(); $version->oldquestion = $oldquestionid; $version->newquestion = $question->id; - $version->userid = $USER->id; // field still needs to be added to table + $version->userid = $USER->id; $version->timestamp = time(); foreach($replaceinquiz as $qid) { @@ -193,11 +207,10 @@ } /// now update the question references in the quizzes - - $quizzes = implode(',', $replaceinquiz); - if (empty($quizzes) || !$quizzes = get_records_list("quiz", "id", $quizzes)) { + if (empty($replaceinquiz) || !$quizzes = get_records_list("quiz", "id", implode(',', $replaceinquiz))) { $quizzes = array(); } + foreach($quizzes as $quiz) { $questionlist = ",$quiz->questions,"; // a little hack with the commas here. not nice but effective $questionlist = str_replace(",$oldquestionid,", ",$question->id,", $questionlist); @@ -205,9 +218,10 @@ if (!set_field("quiz", 'questions', $questionlist, 'id', $quiz->id)) { error("Could not update questionlist in quiz $quiz->id!"); } - // the quiz_question_grades table needs to be updated too (aah, the joys of duplication :) - if (!set_field('quiz_question_grades', 'question', $question->id, 'quiz', $quiz->id, 'question', $oldquestionid)) { - error("Could not update question grade!"); + + // the quiz_question_instances table needs to be updated too (aah, the joys of duplication :) + if (!set_field('quiz_question_instances', 'question', $question->id, 'quiz', $quiz->id, 'question', $oldquestionid)) { + error("Could not update question instance!"); } if (isset($SESSION->modform) && (int)$SESSION->modform->instance === (int)$quiz->id) { $SESSION->modform->questions = $questionlist; @@ -215,24 +229,29 @@ unset($SESSION->modform->grades[$oldquestionid]); } } - - // fix responses + + // set originalquestion in states if ($attempts = get_records_list('quiz_attempts', 'quiz', implode(',', $replaceinquiz))) { foreach ($attempts as $attempt) { - if (!set_field('quiz_responses', 'originalquestion', $oldquestionid, 'attempt', $attempt->id, 'question', $oldquestionid) - or !set_field('quiz_responses', 'question', $question->id, 'attempt', $attempt->id, 'question', $oldquestionid)) { - error("Could not point responses to new question"); - } + set_field('quiz_states', 'originalquestion', $oldquestionid, 'attempt', $attempt->id, 'question', $question->id, 'originalquestion', '0'); } } } - - redirect("edit.php"); - } else { // use the old code which simply overwrites old versions + } else { + // use the old code which simply overwrites old versions + // it is also used for creating new questions $question = $QUIZ_QTYPES[$qtype]->save_question($question, $form, $course); + $quizlist = array(); + } + + if (empty($question->errors) && + $QUIZ_QTYPES[$qtype]->finished_edit_wizard($form)) { + $QUIZ_QTYPES[$question->qtype]->get_question_options($question); + // Automagically regrade all attempts (and states) in the affected quizzes + quiz_regrade_question_in_quizzes($question, $quizlist); redirect("edit.php"); } - } + } $grades = array(1,0.9,0.8,0.75,0.70,0.66666,0.60,0.50,0.40,0.33333,0.30,0.25,0.20,0.16666,0.142857,0.10,0.05,0); foreach ($grades as $grade) { @@ -274,6 +293,9 @@ if (empty($question->image)) { $question->image = ""; } + if (!isset($question->penalty)) { + $question->penalty = 0.1; + } // Set up some Richtext editing if necessary if ($usehtmleditor = can_use_richtext_editor()) { @@ -281,13 +303,17 @@ } else { $defaultformat = FORMAT_MOODLE; } - + + if (isset($question->errors)) { + $err = $question->errors; + } + echo '
'; print_simple_box_start('center'); require('questiontypes/'.$QUIZ_QTYPES[$qtype]->name().'/editquestion.php'); print_simple_box_end(); - if ($usehtmleditor) { + if ($usehtmleditor) { use_html_editor('questiontext'); } diff --git a/mod/quiz/questiontypes/calculated/editquestion.php b/mod/quiz/questiontypes/calculated/editquestion.php index 4a5ffdbaa6f..e0fb5a3826b 100644 --- a/mod/quiz/questiontypes/calculated/editquestion.php +++ b/mod/quiz/questiontypes/calculated/editquestion.php @@ -25,14 +25,14 @@ if ($form) { or $answers[0]->answer and $calculatedmessages[] = $formulaerrors or $calculatedmesages[] = get_string('missingformula', 'quiz'); - + $answers[0]->tolerance = array_shift($form->tolerance) or $answers[0]->tolerance = 0.0; is_numeric($answers[0]->tolerance) or $calculatedmessages[] = get_string('tolerancemustbenumeric', 'quiz'); $answers[0]->feedback = array_shift($form->feedback); - + // Let's trust the drop down menus. $answers[0]->tolerancetype = array_shift($form->tolerancetype); @@ -80,7 +80,7 @@ if ($form) { $units[0]->unit = array_shift($form->unit); array_shift($form->multiplier); // In case it is not 1.0 $units[0]->multiplier = 1.0; // Must! - + // Accept other units if they have legal multipliers $i = 1; foreach ($form->multiplier as $key => $multiplier) { @@ -108,7 +108,7 @@ if ($form) { require("$CFG->dirroot/mod/quiz/questiontypes/datasetdependent/datasetitems.php"); exit(); } - + /***** Now continue by preparing for the second page ****** ***** in the question wizard: "questiondatasets.html" ******/ $datasetmessage = ''; @@ -138,7 +138,7 @@ if ($form) { $hiddeninputnames[] = 'multiplier[]'; $hiddeninputvalues[] = $unit->multiplier; } - + /*** Determine possible and mandatory datasets... ***/ $possibledatasets = $qtypeobj->find_dataset_names($question->questiontext); $mandatorydatasets = array(); @@ -154,7 +154,7 @@ if ($form) { require("$CFG->dirroot/mod/quiz/questiontypes/datasetdependent/questiondatasets.html"); exit(); } - + } else { /*********************************************************/ /***** First page in question wizard - calculated.html! **/ @@ -163,7 +163,7 @@ if ($form) { // The layout of the editing page will only support // one formula alternative for calculated questions. // However, the code behind supports up to six formulas - // and the database store and attempt/review framework + // and the database store and attempt/review framework // does not have any limit. $answers= array(); for ($i=0; $i<6; $i++) { @@ -177,7 +177,8 @@ if ($form) { $answers[$i]->correctanswerformat = "1"; // ... decimals } if (!empty($question->id)) { - $answersraw = $qtypeobj->get_answers($question); + $QUIZ_QTYPES[$question->qtype]->get_question_options($question); + $answersraw = $question->options->answers; if (!empty($answersraw)) { /*** Overwrite the default valued answer slots *** *** with correct values from database ***/ @@ -233,7 +234,6 @@ if ($form) { } } } - print_heading_with_help(get_string("editingcalculated", "quiz"), "calculated", "quiz"); require("$CFG->dirroot/mod/quiz/questiontypes/calculated/calculated.html"); diff --git a/mod/quiz/questiontypes/calculated/modifiednumericalqtype.php b/mod/quiz/questiontypes/calculated/modifiednumericalqtype.php deleted file mode 100644 index ceee561b635..00000000000 --- a/mod/quiz/questiontypes/calculated/modifiednumericalqtype.php +++ /dev/null @@ -1,38 +0,0 @@ -answers; - } - - function set_answers($calculatedanswers) { - $this->answers = $calculatedanswers; - } -} -//// END OF CLASS //// - -?> diff --git a/mod/quiz/questiontypes/calculated/questiontype.php b/mod/quiz/questiontypes/calculated/questiontype.php index 6224b6e6aaf..3926aac06c6 100644 --- a/mod/quiz/questiontypes/calculated/questiontype.php +++ b/mod/quiz/questiontypes/calculated/questiontype.php @@ -11,23 +11,153 @@ class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype { // Used by the function custom_generator_tools: var $calcgenerateidhasbeenadded = false; - - function get_answers($question) { - global $CFG; - return get_records_sql( - "SELECT a.*, c.tolerance, c.tolerancetype, c.correctanswerlength, c.correctanswerformat, c.id as calcid - FROM {$CFG->prefix}quiz_answers a, - {$CFG->prefix}quiz_calculated c - WHERE c.question = $question->id AND a.id = c.answer"); - } - + function name() { return 'calculated'; } - + + function get_question_options(&$question) { + // First get the datasets and default options + if(false === parent::get_question_options($question)) { + return false; + } + + if (!$options = get_record('quiz_calculated', 'question', $question->id)) { + notify("No options were found for calculated question + #{$question->id}! Proceeding with defaults."); + $options = new stdClass; + $options->tolerance = 0.01; + $options->tolerancetype = 1; // relative + $options->correctanswerlength = 2; + $options->correctanswerformat = 1; // decimals + } + $question->options->tolerance = $options->tolerance; + $question->options->tolerancetype = $options->tolerancetype; + $question->options->correctanswerlength = $options->correctanswerlength; + $question->options->correctanswerformat = $options->correctanswerformat; + + // For historic reasons we also need these fields in the answer objects. + // This should eventually be removed and related code changed to use + // the values in $question->options instead. + foreach ($question->options->answers as $key => $answer) { + $answer = &$question->options->answers[$key]; // for PHP 4.x + $answer->calcid = $options->id; + $answer->tolerance = $options->tolerance; + $answer->tolerancetype = $options->tolerancetype; + $answer->correctanswerlength = $options->correctanswerlength; + $answer->correctanswerformat = $options->correctanswerformat; + } + + $this->get_numerical_units($question); + + return true; + } + + function get_numerical_units(&$question) { + $virtualqtype = $this->get_virtual_qtype(); + return $virtualqtype->get_numerical_units($question); + } + + function save_question_options($question, $options) { + // Get old answers: + $oldanswers = array(); + if ($this->get_question_options($question)) { + $oldanswers = $question->options->answers; + } + + // Update with new answers + $answerrec->question = $calcrec->question = $question->id; + foreach ($options->answers as $newanswer) { + $answerrec->answer = $newanswer->answer; + $answerrec->fraction = $newanswer->fraction; + $answerrec->feedback = $newanswer->feedback; + $calcrec->tolerance = $newanswer->tolerance; + $calcrec->tolerancetype = $newanswer->tolerancetype; + $calcrec->correctanswerlength = $newanswer->correctanswerlength; + $calcrec->correctanswerformat = $newanswer->correctanswerformat; + if ($oldanswer = array_shift($oldanswers)) { + // Reuse old record: + $calcrec->answer = $answerrec->id = $oldanswer->id; + $calcrec->id = $oldanswer->calcid; + if (!update_record('quiz_answers', $answerrec)) { + error("Unable to update answer for calculated question #{$question->id}!"); + } else { + // notify("Answer updated successfully for calculated question $question->name"); + } + if (!update_record('quiz_calculated', $calcrec)) { + error("Unable to update options for calculated question #{$question->id}!"); + } else { + // notify("Options updated successfully for calculated question $question->name"); + } + } else { + unset($answerrec->id); + unset($calcrec->id); + if (!($calcrec->answer = insert_record('quiz_answers', + $answerrec))) { + error("Unable to insert answer for calculated question $question->name"); + } else { + // notify("Answer inserted successfully for calculated question $question->name"); + } + if (!insert_record('quiz_calculated', $calcrec)) { + error("Unable to insert options calculared question $question->name"); + } else { + // notify("Options inserted successfully for calculated question $question->name"); + } + } + } + + // Delete excessive records: + foreach ($oldanswers as $oldanswer) { + if (!delete_records('quiz_answers', 'id', $oldanswer->id)) { + error("Unable to delete old answers for calculated question $question->name"); + } else { + // notify("Old answers deleted successfully for calculated question $question->name"); + } + if (!delete_records('quiz_calculated', 'id', $oldanswer->calcid)) { + error("Unable to delete old options for calculated question $question->name"); + } else { + // notify("Old options deleted successfully for calculated question $question->name"); + } + } + + $virtualqtype = $this->get_virtual_qtype(); + $virtualqtype->save_numerical_units($question); + + return true; + } + + function print_question_formulation_and_controls(&$question, &$state, $quiz, + $options) { + // Substitute variables in questiontext before giving the data to the + // virtual type for printing + $virtualqtype = $this->get_virtual_qtype(); + $unit = $virtualqtype->get_default_numerical_unit($question); + foreach ($question->options->answers as $key => $answer) { + $answer = &$question->options->answers[$key]; // for PHP 4.x + $answer->answer = $this->substitute_variables($answer->answer, + $state->options->dataset); + // apply_unit + } + $question->questiontext = parent::substitute_variables( + $question->questiontext, $state->options->dataset); + $virtualqtype->print_question_formulation_and_controls($question, + $state, $quiz, $options); + } + + function grade_responses(&$question, &$state, $quiz) { + // Forward the grading to the virtual qtype + foreach ($question->options->answers as $key => $answer) { + $answer = &$question->options->answers[$key]; // for PHP 4.x + $answer->answer = $this->substitute_variables($answer->answer, + $state->options->dataset); + } + return parent::grade_responses($question, $state, $quiz); + } + function create_virtual_qtype() { - require('modifiednumericalqtype.php'); - return new quiz_calculated_qtype_numerical_helper(); + global $CFG; + require_once("$CFG->dirroot/mod/quiz/questiontypes/numerical/questiontype.php"); + return new quiz_numerical_qtype(); } function supports_dataset_item_generation() { @@ -116,7 +246,7 @@ class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype { // Reformat according to the precision $regs[4]: - // Determine the format 0.[1-9][0-9]* for the nbr... + // Determine the format 0.[1-9][0-9]* for the nbr... $p10 = 0; while ($nbr < 1) { --$p10; @@ -157,7 +287,7 @@ class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype { } } - // The larger of the values decide the sign in case the + // The larger of the values decide the sign in case the // have equal different signs (which they really must not have) if ($regs[2] + $regs[3] > 0) { return $nbr; @@ -172,7 +302,8 @@ class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype { } function comment_header($question) { - $answers = $this->get_answers($question); + $this->get_question_options($question); + $answers = $question->options->answers; $strheader = ''; $delimiter = ''; foreach ($answers as $answer) { @@ -193,17 +324,25 @@ class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype { } // Get answers - $answers = $this->get_answers($question); + $answers = $question->options->answers; $stranswers = get_string('answer', 'quiz'); $strmin = get_string('min', 'quiz'); $strmax = get_string('max', 'quiz'); $errors = ''; $delimiter = ': '; + $state = new stdClass; + $state->responses = array(); + $state->options = new stdClass; + $virtualqtype = $this->get_virtual_qtype(); foreach ($answers as $answer) { $calculated = quiz_qtype_calculated_calculate_answer( $answer->answer, $data, $answer->tolerance, $answer->tolerancetype, $answer->correctanswerlength, $answer->correctanswerformat, $unit); + $state->responses[''] = $calculated->answer; + $virtualqtype->get_tolerance_interval($question, $state); + $calculated->min = $state->options->min; + $calculated->max = $state->options->max; if ($calculated->min === '') { // This should mean that something is wrong $errors .= " -$calculated->answer"; @@ -213,6 +352,7 @@ class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype { } $strmin .= $delimiter.$calculated->min; $strmax .= $delimiter.$calculated->max; + $delimiter = ', '; } return "$stranswers
$strmin
$strmax
$errors"; @@ -224,104 +364,6 @@ class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype { '3' => get_string('geometric', 'quiz')); } - function save_question_options($question, $options) { - // Get old answers: - $oldanswers = $this->get_answers($question) - or $oldanswers = array(); // if there are none - - // Update with new answers - $answerrec->question = $calcrec->question = $question->id; - foreach ($options->answers as $newanswer) { - $answerrec->answer = $newanswer->answer; - $answerrec->fraction = $newanswer->fraction; - $answerrec->feedback = $newanswer->feedback; - $calcrec->tolerance = $newanswer->tolerance; - $calcrec->tolerancetype = $newanswer->tolerancetype; - $calcrec->correctanswerlength = $newanswer->correctanswerlength; - $calcrec->correctanswerformat = $newanswer->correctanswerformat; - if ($oldanswer = array_shift($oldanswers)) { - // Reuse old record: - $calcrec->answer = $answerrec->id = $oldanswer->id; - $calcrec->id = $oldanswer->calcid; - if (!update_record('quiz_answers', $answerrec)) { - error("Unable to update answer for calculated question $question->name"); - } else { - // notify("Answer updated successfully for calculated question $question->name"); - } - if (!update_record('quiz_calculated', $calcrec)) { - error("Unable to update options calculared question $question->name"); - } else { - // notify("Options updated successfully for calculated question $question->name"); - } - } else { - unset($answerrec->id); - unset($calcrec->id); - if (!($calcrec->answer = insert_record('quiz_answers', - $answerrec))) { - error("Unable to insert answer for calculated question $question->name"); - } else { - // notify("Answer inserted successfully for calculated question $question->name"); - } - if (!insert_record('quiz_calculated', $calcrec)) { - error("Unable to insert options calculared question $question->name"); - } else { - // notify("Options inserted successfully for calculated question $question->name"); - } - } - } - - // Delete excessive records: - foreach ($oldanswers as $oldanswer) { - if (!delete_records('quiz_answers', 'id', $oldanswer->id)) { - error("Unable to delete old answers for calculated question $question->name"); - } else { - // notify("Old answers deleted successfully for calculated question $question->name"); - } - if (!delete_records('quiz_calculated', 'id', $oldanswer->calcid)) { - error("Unable to delete old options for calculated question $question->name"); - } else { - // notify("Old options deleted successfully for calculated question $question->name"); - } - } - - // Get old units (just like for numerical questions) - $oldunits = get_records('quiz_numerical_units', - 'question', $question->id) - or $oldunits = array(); // if there are none - if (1 == count($options->units) && !$options->units[0]->unit) { - /// Only default unit and it is empty, so drop it: - $options->units = array(); - } - foreach ($options->units as $newunit) { - $newunit->question = $question->id; - if ($oldunit = array_shift($oldunits)) { - $newunit->id = $oldunit->id; - if (!update_record('quiz_numerical_units', $newunit)) { - error("Unable to update unit $newunit->unit for $question->name"); - } else { - // notify("Unit $newunit->unit was updated successfully for $question->name"); - } - } else { - if (!insert_record('quiz_numerical_units', $newunit)) { - error("Unable to insert unit $newunit->unit for $question->name"); - } else { - // notify("Unit $newunit->unit was inserted successfully for question $question->name"); - } - } - } - - // Delete excessive unit records - foreach ($oldunits as $oldunit) { - if (!delete_records('quiz_numerical_units', 'id', $oldunit->id)) { - error("Unable to delete old unit $oldunit->unit for question $question->name"); - } else { - notify("Deleted old unit $oldunit->unit successfully for question $question->name"); - } - } - - return true; - } - function dataset_options($question, $name, $renameabledatasets=false) { // Takes datasets from the parent implementation but // filters options that are currently not accepted by calculated @@ -361,48 +403,34 @@ class quiz_calculated_qtype extends quiz_dataset_dependent_questiontype { return $datasetmenus; } - function grade_response($question, $nameprefix) { - /// Determines the answers and then lets the - /// NUMERICAL question type take care of the - /// grading... - - list($datasetnumber, $individualdata) = - $this->parse_datasetinput($question->response[$nameprefix]); - - // find the raw answer material - global $CFG; - if (!($answers = $this->get_answers($question))) { - notify("Error no answers found for question $question->id"); - } - - /// Find a default unit: - if ($unit = get_record('quiz_numerical_units', - 'question', $question->id, 'multiplier', 1.0)) { - $unit = $unit->unit; - } else { - $unit = ''; - } - - // Construct answers for the numerical question type - foreach ($answers as $aid => $answer) { - $answernumerical = quiz_qtype_calculated_calculate_answer( - $answer->answer, $individualdata, + function get_correct_responses(&$question, &$state) { + foreach ($question->options->answers as $key => $answer) { + if (((float) $answer->fraction) === 1.0) { + $virtualqtype = $this->get_virtual_qtype(); + $unit = $virtualqtype->get_default_numerical_unit($question); + $answernumerical = quiz_qtype_calculated_calculate_answer( + $answer->answer, $state->options->dataset, $answer->tolerance, $answer->tolerancetype, $answer->correctanswerlength, $answer->correctanswerformat, $unit); - $answers[$aid]->answer = $answernumerical->answer; - $answers[$aid]->min = $answernumerical->min; - $answers[$aid]->max = $answernumerical->max; + return array('' => $answernumerical->answer); + } } + return null; + } - // Forward the grading to the virtual qtype - $virtualnameprefix = $this->create_virtual_nameprefix( - $nameprefix, $question->response[$nameprefix]); - unset($question->response[$nameprefix]); - $virtualqtype = $this->get_virtual_qtype(); - $virtualqtype->set_answers($answers); - return $virtualqtype->grade_response($question, - $virtualnameprefix); + function substitute_variables($str, $dataset) { + $formula = parent::substitute_variables($str, $dataset); + if ($error = quiz_qtype_calculated_find_formula_errors($formula)) { + return $error; + } + /// Calculate the correct answer + eval('$str = '.$formula.';'); + return $str; + } + + function print_question_grading_details(&$question, &$state, $quiz, $options) { + // do nothing } } //// END OF CLASS //// @@ -418,57 +446,10 @@ function quiz_qtype_calculated_calculate_answer($formula, $individualdata, /// ->answer the correct answer /// ->min the lower bound for an acceptable response /// ->max the upper bound for an accetpable response - + /// Exchange formula variables with the correct values... - foreach ($individualdata as $name => $value) { - $formula = str_replace('{'.$name.'}', "($value)", $formula); - } - if (ereg('\\{([^}]*)\\}', $formula, $regs)) { - // This is the normal case for a recently added question. - // Return a notification about it - $calculated->answer = $calculated->min = $calculated->max = ''; - return $calculated; - - } else if ($error = quiz_qtype_calculated_find_formula_errors($formula)) { - $calculated->answer = $error; - $calculated->min = $calculated->max = ''; - return $calculated; - } - - /// Calculate the correct answer - eval('$answer = '.$formula.';'); - - /// Calculate min and max - switch ($tolerancetype) { - case '1': case 'relative': - /// Recalculate the tolerance and fall through - /// to the nominal case: - $tolerance = $answer * $tolerance; - - // Falls through to the nominal case - - case '2': case 'nominal': - $tolerance = abs($tolerance); // important - $max = $answer + $tolerance; - $min = $answer - $tolerance; - break; - - case '3': case 'geometric': - $quotient = 1 + abs($tolerance); - if ($answer >= 0) { - $max = $answer * $quotient; - $min = $answer / $quotient; - } else { - $min = $answer * $quotient; - $max = $answer / $quotient; - } - break; - - default: - error("Unknown tolerance type $tolerancetype"); - } - $calculated->min = $min; - $calculated->max = $max; - + global $QUIZ_QTYPES; + $answer = $QUIZ_QTYPES[CALCULATED]->substitute_variables($formula, $individualdata); if ('1' == $answerformat) { /* Answer is to have $answerlength decimals */ /*** Adjust to the correct number of decimals ***/ @@ -476,7 +457,7 @@ function quiz_qtype_calculated_calculate_answer($formula, $individualdata, if ($answerlength) { /* Try to include missing zeros at the end */ - + if (ereg('^(.*\\.)(.*)$', $calculated->answer, $regs)) { $calculated->answer = $regs[1] . substr( $regs[2] . '00000000000000000000000000000000000000000x', @@ -502,7 +483,7 @@ function quiz_qtype_calculated_calculate_answer($formula, $individualdata, $sign = ''; } - // Determine the format 0.[1-9][0-9]* for the answer... + // Determine the format 0.[1-9][0-9]* for the answer... $p10 = 0; while ($answer < 1) { --$p10; @@ -547,7 +528,7 @@ function quiz_qtype_calculated_calculate_answer($formula, $individualdata, } else { $calculated->answer = 0.0; } - + /// Return the result return $calculated; } @@ -566,7 +547,7 @@ function quiz_qtype_calculated_find_formula_errors($formula) { // Strip away empty space and lowercase it $formula = strtolower(str_replace(' ', '', $formula)); - $safeoperatorchar = '-+/*%>:^~:^~prefix}quiz_dataset_definitions a, + {$CFG->prefix}quiz_question_datasets b + WHERE b.question = $question->id + AND a.id = b.datasetdefinition")) { + error("Couldn't get the specified dataset for a calculated " . + "question! (question: {$question->id}, " . + "datasetnumber: {$datasetnumber})"); + } + + // Choose a random dataset + $state->options->datasetnumber = rand(1, $maxnumber); + $state->options->dataset = + $this->pick_question_dataset($question,$state->options->datasetnumber); + $state->responses = array('' => ''); + return true; + } + + function restore_session_and_responses(&$question, &$state) { + if (!ereg('^dataset([0-9]+)[^-]*-(.*)$', + $state->responses[''], $regs)) { + notify ("Wrongly formatted raw response answer " . + "{$state->responses['']}! Could not restore session for " . + " question #{$question->id}."); + $state->options->datasetnumber = 1; + $state->options->dataset = array(); + $state->responses = array('' => ''); + return false; + } + + // Restore the chosen dataset + $state->options->datasetnumber = $regs[1]; + $state->options->dataset = + $this->pick_question_dataset($question,$state->options->datasetnumber); + $state->responses = array('' => $regs[2]); + return true; + } + + function save_session_and_responses(&$question, &$state) { + $responses = 'dataset'.$state->options->datasetnumber.'-'. + $state->responses['']; + + // Set the legacy answer field + if (!set_field('quiz_states', 'answer', $responses, 'id', + $state->id)) { + return false; + } + return true; + } + + function print_question_formulation_and_controls(&$question, &$state, $quiz, + $options) { + // Substitute variables in questiontext before giving the data to the + // virtual type for printing + $virtualqtype = $this->get_virtual_qtype(); + $unit = $virtualqtype->get_default_numerical_unit($question); + foreach ($question->options->answers as $answer) { + $answer->answer = $this->substitute_variables($answer->answer, + $state->options->dataset); + } + $question->questiontext = $this->substitute_variables( + $question->questiontext, $state->options->dataset); + $virtualqtype->print_question_formulation_and_controls($question, + $state, $quiz, $options); + } + + function get_correct_responses(&$question, &$state) { + foreach ($question->options->answers as $answer) { + $answer->answer = $this->substitute_variables( + $answer->answer, $state->options->dataset); + if (((float) $answer->fraction) === 1.0) { + return array('' => $answer->answer); + } + } + return null; + } + + function grade_responses(&$question, &$state, $quiz) { + // Forward the grading to the virtual qtype + foreach ($question->options->answers as $answer) { + $answer->answer = $this->substitute_variables($answer->answer, + $state->options->dataset); + } + $virtualqtype = $this->get_virtual_qtype(); + return $virtualqtype->grade_responses($question, $state, $quiz) ; + } + + function substitute_variables($str, $dataset) { + foreach ($dataset as $name => $value) { + $str = str_replace('{'.$name.'}', $value, $str); + } + return $str; + } + function uses_quizfile($question, $relativefilepath) { // Check whether the specified file is available by any // dataset item on this question... @@ -41,7 +140,165 @@ class quiz_dataset_dependent_questiontype extends quiz_default_questiontype { return parent::uses_quizfile($question, $relativefilepath); } } - + + function finished_edit_wizard(&$form) { + return isset($form->backtoquiz); + } + + function save_question($question, $form, $course, $subtypeoptions=false) { + // For dataset dependent questions a wizard is used for editing + // questions. Therefore calls from question.php are ignored. + // Instead questions are saved when this method is called by + // editquestion.php + + if ($subtypeoptions) { + // Let's save the question + // We need to save the question first (in order to get the id) + // We then save the dataset definitions and finally we + // save the subtype options... + + // Save question + 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 + // Set the unique code (not to be changed) + $question->stamp = make_unique_id_code(); + $question->version = 1; + if (!$question->id=insert_record("quiz_questions", $question)) { + error("Could not insert new question!"); + } + } + + // Save datasets + global $CFG; + $datasetdefinitions = get_records_sql( // Indexed by name... + "SELECT a.name, a.id, a.type, a.category + FROM {$CFG->prefix}quiz_dataset_definitions a, + {$CFG->prefix}quiz_question_datasets b + WHERE a.id = b.datasetdefinition + AND b.question = $question->id"); + + foreach ($form->dataset as $dataset) { + if (!$dataset) { + continue; // The no dataset case... + } + + list($type, $category, $name) = explode('-', $dataset, 3); + + if (isset($datasetdefinitions[$name]) + and $datasetdefinitions[$name]->type == $type + and $datasetdefinitions[$name]->category == $category) { + // Keep this dataset as it already fulfills our dreams + // by preventing it from being deleted + unset($datasetdefinitions[$name]); + continue; + } + + // We need to create a new datasetdefinition + unset ($datasetdef); + $datasetdef->type = $type; + $datasetdef->name = $name; + $datasetdef->category = $category; + + if (!$datasetdef->id = insert_record( + 'quiz_dataset_definitions', $datasetdef)) { + error("Unable to create dataset $name"); + } + + if ($category) { + // We need to look for already existing + // datasets in the category. + // By first creating the datasetdefinition above we + // can manage to automatically take care of + // some possible realtime concurrence + while ($olderdatasetdef = get_record_select( + 'quiz_dataset_definitions', + " type = '$type' AND name = '$name' + AND category = '$category' + AND id < $datasetdef->id ")) { + // Use older dataset instead: + delete_records('quiz_dataset_definitions', + 'id', $datasetdef->id); + $datasetdef = $olderdatasetdef; + } + } + + // Create relation to this dataset: + unset($questiondataset); + $questiondataset->question = $question->id; + $questiondataset->datasetdefinition = $datasetdef->id; + if (!insert_record('quiz_question_datasets', + $questiondataset)) { + error("Unable to create relation to dataset $name"); + } + } + + // Remove local obsolete datasets as well as relations + // to datasets in other categories: + if (!empty($datasetdefinitions)) { + foreach ($datasetdefinitions as $def) { + delete_records('quiz_question_datasets', + 'question', $question->id, + 'datasetdefinition', $def->id); + + if ($def->category == 0) { // Question local dataset + delete_records('quiz_dataset_definitions', 'id', $def->id); + delete_records('quiz_dataset_items', + 'definition', $def->id); + } + } + } + + // Save subtype options + $this->save_question_options($question, $subtypeoptions); + return $question; + + } else if (empty($form->editdatasets)) { + // Parse for common question entries and + // continue with editquestion.php by returning the question + $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; + } + return $question; + } else { + return $question; + } + } + +/// Dataset functionality + function pick_question_dataset($question, $datasetnumber) { + // Select a dataset in the following format: + // An array indexed by the variable names (d.name) pointing to the value + // to be substituted + global $CFG; + if (!$dataset = get_records_sql( + "SELECT d.name, i.value + FROM {$CFG->prefix}quiz_dataset_definitions d, + {$CFG->prefix}quiz_dataset_items i, + {$CFG->prefix}quiz_question_datasets q + WHERE q.question = $question->id + AND q.datasetdefinition = d.id + AND d.id = i.definition + AND i.number = $datasetnumber")) { + error("Couldn't get the specified dataset for a dataset dependent " . + "question! (question: {$question->id}, " . + "datasetnumber: {$datasetnumber})"); + } + array_walk($dataset, create_function('&$val', '$val = $val->value;')); + return $dataset; + } + function create_virtual_qtype() { error("No vitrual question type for question type ".$this->name()); } @@ -92,10 +349,10 @@ class quiz_dataset_dependent_questiontype extends quiz_default_questiontype { } function dataset_options($question, $name) { - + // First options - it is not a dataset... $options['0'] = get_string('nodataset', 'quiz'); - + // Construct question local options global $CFG; $currentdatasetdef = get_record_sql( @@ -105,7 +362,7 @@ class quiz_dataset_dependent_questiontype extends quiz_default_questiontype { WHERE a.id = b.datasetdefinition AND b.question = '$question->id' AND a.name = '$name'") - or $currentdatasetdef->type = '0'; + or $currentdatasetdef->type = '0'; foreach (array( LITERAL, FILE, LINK) as $type) { $key = "$type-0-$name"; if ($currentdatasetdef->type == $type @@ -115,7 +372,7 @@ class quiz_dataset_dependent_questiontype extends quiz_default_questiontype { $options[$key] = get_string("newlocal$type", 'quiz'); } } - + // Construct question category options $categorydatasetdefs = get_records_sql( "SELECT a.type, a.id @@ -145,142 +402,6 @@ class quiz_dataset_dependent_questiontype extends quiz_default_questiontype { : ''); } - function save_question_options($question, $options) { - // Default does nothing... - return true; - } - - function save_question($question, $form, $course, $subtypeoptions=false) { - // For dataset dependent questions a wizard is used for editing - // questions. Therefore calls from question.php are ignored. - // Instead questions are saved when this method is called by - // editquestion.php - - if ($subtypeoptions) { - // Let's save the question - // We need to save the question first (in order to get the id) - // We then save the dataset definitions and finally we - // save the subtype options... - - // Save question - 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 - // Set the unique code (not to be changed) - $question->stamp = make_unique_id_code(); - $question->version = 1; - if (!$question->id=insert_record("quiz_questions", $question)) { - error("Could not insert new question!"); - } - } - - // Save datasets - global $CFG; - $datasetdefinitions = get_records_sql( // Indexed by name... - "SELECT a.name, a.id, a.type, a.category - FROM {$CFG->prefix}quiz_dataset_definitions a, - {$CFG->prefix}quiz_question_datasets b - WHERE a.id = b.datasetdefinition - AND b.question = $question->id"); - - foreach ($form->dataset as $dataset) { - if (!$dataset) { - continue; // The no dataset case... - } - - list($type, $category, $name) = explode('-', $dataset, 3); - - if (isset($datasetdefinitions[$name]) - and $datasetdefinitions[$name]->type == $type - and $datasetdefinitions[$name]->category == $category) { - // Keep this dataset as it already fulfills our dreams - // by preventing it from being deleted - unset($datasetdefinitions[$name]); - continue; - } - - // We need to create a new datasetdefinition - unset ($datasetdef); - $datasetdef->type = $type; - $datasetdef->name = $name; - $datasetdef->category = $category; - - if (!$datasetdef->id = insert_record( - 'quiz_dataset_definitions', $datasetdef)) { - error("Unable to create dataset $name"); - } - - if ($category) { - // We need to look for already existing - // datasets in the category. - // By first creating the datasetdefinition above we - // can manage to automatically take care of - // some possible realtime concurrence - while ($olderdatasetdef = get_record_select( - 'quiz_dataset_definitions', - " type = '$type' AND name = '$name' - AND category = '$category' - AND id < $datasetdef->id ")) { - // Use older dataset instead: - delete_records('quiz_dataset_definitions', - 'id', $datasetdef->id); - $datasetdef = $olderdatasetdef; - } - } - - // Create relation to this dataset: - unset($questiondataset); - $questiondataset->question = $question->id; - $questiondataset->datasetdefinition = $datasetdef->id; - if (!insert_record('quiz_question_datasets', - $questiondataset)) { - error("Unable to create relation to dataset $name"); - } - } - - // Remove local obsolete datasets as well as relations - // to datasets in other categories: - if (!empty($datasetdefinitions)) { - foreach ($datasetdefinitions as $def) { - delete_records('quiz_question_datasets', - 'question', $question->id, - 'datasetdefinition', $def->id); - - if ($def->category == 0) { // Question local dataset - delete_records('quiz_dataset_definitions', 'id', $def->id); - delete_records('quiz_dataset_items', - 'definition', $def->id); - } - } - } - - // Save subtype options - $this->save_question_options($question, $subtypeoptions); - return $question; - - } else if (empty($form->editdatasets)) { - // Parse for common question entries and - // continue with editquestion.php by returning the question - $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; - } - return $question; - } else { - return $question; - } - } - function find_dataset_names($text) { /// Returns the possible dataset names found in the text as an array /// The array has the dataset name for both key and value @@ -292,114 +413,8 @@ class quiz_dataset_dependent_questiontype extends quiz_default_questiontype { return $datasetnames; } - function convert_to_response_answer_field($questionresponse) { - // It does not look like all platforms support the ksort strategi - // so gotta try something else... - foreach ($questionresponse as $key => $response) { - if (!isset($shortestkey) - || strlen($shortestkey) > strlen($key)) { - $shortestkey = $key; - } - } - $dataset = $questionresponse[$shortestkey]; - unset($questionresponse[$shortestkey]); - $virtualqtype = $this->get_virtual_qtype(); - return "dataset$dataset-" . $virtualqtype - ->convert_to_response_answer_field($questionresponse); - } - - function create_response($question, $nameprefix, $questionsinuse) { - /// This method must pick a dataset and have its number and - /// data injected in the response keys - - - // First we retrieve the dataset definitions for this questions - // and check how many datasets we have available ($maxnumber) - global $CFG; - $datasetdefinitions = get_records_sql( - "SELECT a.* FROM {$CFG->prefix}quiz_dataset_definitions a, - {$CFG->prefix}quiz_question_datasets b - WHERE a.id = b.datasetdefinition - AND b.question = $question->id"); - $definitionids = $delimiter = ''; - foreach ($datasetdefinitions as $datasetdef) { - $definitionids .= $delimiter.$datasetdef->id; - $delimiter = ','; - if (!isset($maxnumber) || $datasetdef->itemcount < $maxnumber) { - $maxnumber = $datasetdef->itemcount; - } - } - - // We then pick dataset number and retrieve the datasetitems - if (!isset($maxnumber) || 0 == $maxnumber) { - notify("Error: Question $question->id does not - have items for its datasets"); - $datasetinput = 0; - $datasetitems = array(); - - } else { - isset($this->datasetnumbers[$question->category]) - and $this->datasetnumbers[$question->category] <= $maxnumber - or $this->datasetnumbers[$question->category] = - quiz_qtype_dataset_pick_new($question->category, - $maxnumber); - $datasetinput = $this->datasetnumbers[$question->category]; - $datasetitems = get_records_select('quiz_dataset_items', - "definition in ($definitionids) - AND number = $datasetinput"); - } - - // Build the rest of $datasetinput - foreach ($datasetitems as $item) { - $datasetdef = $datasetdefinitions[$item->definition]; - - // We here need to pay attention to whether the - // data item is a link or an ordinary literal - if ($datasetdef->type == LITERAL) { - // The ordinary simple case - $value = $item->value; - - } else { - $icon = "wwwroot/pix/f/" - . mimeinfo('icon', $item->value) - . '" height="16" width="16" border="0" alt="File" />'; - if (substr(strtolower($item->value), 0, 7)=='http://') { - $link = $item->value; - - } else { - global $quiz; // Try to reach this info globally - if ($CFG->slasharguments) { - // Use this method if possible for better caching - $link = "quizfile.php/$quiz->id/$question->id/$item->value"; - - } else { - $link = "quizfile.php?file=/$quiz->id/$question->id/$item->value"; - } - } - - if ($datasetdef->type == FILE - and ereg('/([^/]+)$', $item->value, $regs)) { - $linktext = $regs[1]; - } else { - $linktext = $item->value; - } - $value = 'name\">$icon$linktext"; - } - - $datasetinput .= ';' . base64_encode($datasetdef->name) - . ':' . base64_encode($value); - } - - // Use the virtual question type and have it ->create_response: - $virtualqtype = $this->get_virtual_qtype(); - $response = $virtualqtype->create_response($question, - $this->create_virtual_nameprefix($nameprefix, $datasetinput), - $questionsinuse); - $response[$nameprefix] = $datasetinput; - return $response; - } - +/// The following functions are needed for database upgrade script only + // This is used by extract_responses, so it needs to stay function create_virtual_nameprefix($nameprefix, $datasetinput) { // This default implementation is sometimes overridden if (!ereg('([0-9]+)' . $this->name() . '$', $nameprefix, $regs)) { @@ -414,147 +429,16 @@ class quiz_dataset_dependent_questiontype extends quiz_default_questiontype { $rawresponse->answer, $regs)) { error ("Wrongly formatted raw response answer $rawresponse->answer"); } - + // Truncate raw response to fit the virtual qtype $rawresponse->answer = $regs[2]; - + $virtualqtype = $this->get_virtual_qtype(); $response = $virtualqtype->extract_response($rawresponse, $this->create_virtual_nameprefix($nameprefix, $regs[1])); $response[$nameprefix] = $regs[1]; return $response; } - - function print_question_formulation_and_controls($question, - $quiz, $readonly, $answers, $correctanswers, $nameprefix) { - - // Replace wild-cards with dataset items - $datasetinput = $question->response[$nameprefix]; - list($datasetnumber, $data) = - $this->parse_datasetinput($datasetinput); - foreach ($data as $name => $value) { - $question->questiontext = str_replace - ('{'.$name.'}', $value, $question->questiontext); - } - - // Print hidden field with dataset info - echo ''; - - // Forward to the virtual qtype - unset($question->response[$nameprefix]); - $virtualqtype = $this->get_virtual_qtype(); - $virtualqtype->print_question_formulation_and_controls( - $question, $quiz, $readonly, $answers, $correctanswers, - $this->create_virtual_nameprefix($nameprefix, $datasetinput)); - } - - function parse_datasetinput($datasetinput) { - /// Returns an array consisting of three pieces of information - /// In [0] there is the dataset number - /// In [1] there is an array where the data items are mapped by name - - /// The dataset related part of the response key - /// follows right after the response key and end with : - /// In order to avoid any conflict that can occur whenever anyone - /// wish to use : in the data, the data dependent part - /// has been converted to base64 in two steps - - $rawdata = split('[:;]', $datasetinput); - $rawlength = count($rawdata); - $i = 0; - $data = array(); - while (++$i < $rawlength) { - $data[base64_decode($rawdata[$i])] = - base64_decode($rawdata[++$i]); - } - return array($rawdata[0], $data); - } } //// END OF CLASS //// - -function quiz_qtype_dataset_pick_new($category, $maxnumber) { -//// Function used by ->create_response -//// It takes care of picking a new datasetnumber for -//// the user in the specified question category - - // We need to know whether the attempt builds on the last or - // not. It can not be determined by the function args to - // ->create_response. - // Instead of adding that argument to all implementations of - // create_response we try to reach $quiz globally. That - // should work because this function is only used by - // attempt.php when starting an attempt. - global $quiz; //// PATTERN VIOLATION //// - if ($quiz->attemptonlast) { - - // Dataset numbers for attemptonlast quizes are stored - // in quiz_attemptonlast_datasets - global $USER; - if (!($attemptonlastdataset = get_record( - 'quiz_attemptonlast_datasets', - 'userid', $USER->id, 'category', $category)) - or $attemptonlastdataset->datasetnumber > $maxnumber) { - - // No suitable $attemptonlastdataset - - if ($attemptonlastdataset) { - // Remove the unsuitable: - delete_records('quiz_attemptonlast_datasets', - 'id', $attemptonlastdataset->id); - unset($attemptonlastdataset->id); - unset($attemptonlastdataset->datasetnumber); - - } else { - $attemptonlastdataset->userid = $USER->id; - $attemptonlastdataset->category = $category; - } - - // Create without setting datasetnumber - // so that this user gets its id - $attemptonlastdataset->id = insert_record( - 'quiz_attemptonlast_datasets', $attemptonlastdataset); - - // Pick the datasetnumber in a thread safe way - // so that this can be done without having - // concurrent users get the same datasetnumber! - // The chosen pattern relies on - // synchronization for autoincrement on the id - // when the previous insert_record statement - // was executed. - if (!($latestdatasetpick = get_record_select( - 'quiz_attemptonlast_datasets', - "category = $category AND datasetnumber > 0 - AND id < $attemptonlastdataset->id", - ' max(id) id '))) { - // Smells like the current user is first: - $latestdatasetpick->id = 0; - } - $latestattemptonlasts = get_records_select( - 'quiz_attemptonlast_datasets', - "$latestdatasetpick->id <= id - AND id < $attemptonlastdataset->id - AND category = $category"); - $attemptonlastdataset->datasetnumber = - (count($latestattemptonlasts) - + (isset($latestattemptonlasts[$latestdatasetpick->id]) - ? $latestattemptonlasts[$latestdatasetpick->id] - ->datasetnumber - 1 - : 0)) - % $maxnumber + 1; - if (!update_record('quiz_attemptonlast_datasets', - $attemptonlastdataset)) { - notify("Error unable to save the picked datasetnumber in - quiz_attemptonlast_datasets for user $USER-id"); - } - } - return $attemptonlastdataset->datasetnumber; - - } else { - // When it is not an attemptonlast - // we pick the dataset number randomly - return rand ( 1 , $maxnumber ); - } -} - ?> diff --git a/mod/quiz/questiontypes/description/questiontype.php b/mod/quiz/questiontypes/description/questiontype.php index f067300d562..6e9311077ce 100644 --- a/mod/quiz/questiontypes/description/questiontype.php +++ b/mod/quiz/questiontypes/description/questiontype.php @@ -17,27 +17,23 @@ class quiz_description_qtype extends quiz_default_questiontype { return 'description'; } + function get_question_options(&$question) { + // No options to be restored for this question type + return true; + } + function save_question_options($question) { /// No options to be saved for this question type: return true; } - function create_response($question, $nameprefix, $questionsinuse) { - /// This question type does never have any responses, - /// so do not return any... - - return array(); - } - - function print_question($currentnumber, $quiz, $question, - $readonly, $resultdetails) { - echo '

'; + function print_question(&$question, &$state, &$number, $quiz, $options) { + print_simple_box_start('center', '90%'); echo format_text($question->questiontext, $question->questiontextformat, NULL, $quiz->course); quiz_print_possible_question_image($quiz->id, $question); - echo '

'; - return $currentnumber; + print_simple_box_end('center', '90%'); } function actual_number_of_questions($question) { @@ -49,12 +45,11 @@ class quiz_description_qtype extends quiz_default_questiontype { return 0; } - function grade_response($question, $nameprefix) { - $result->grade = 0.0; - $result->answers = array(); - $result->correctanswers = array(); - return $result; + function grade_responses(&$question, &$state, $quiz) { + $state->raw_grade = 0; + $state->penalty = 0; } + } //// END OF CLASS //// diff --git a/mod/quiz/questiontypes/match/questiontype.php b/mod/quiz/questiontypes/match/questiontype.php index 0abc2029c1e..3a998bc9cd2 100644 --- a/mod/quiz/questiontypes/match/questiontype.php +++ b/mod/quiz/questiontypes/match/questiontype.php @@ -11,6 +11,10 @@ class quiz_match_qtype extends quiz_default_questiontype { return 'match'; } + function get_question_options(&$question) { + return true; + } + function save_question_options($question) { if (!$oldsubquestions = get_records("quiz_match_sub", "question", $question->id, "id ASC")) { @@ -81,21 +85,104 @@ class quiz_match_qtype extends quiz_default_questiontype { return true; } - function convert_to_response_answer_field($questionresponse) { - /// This method, together with extract_response, should be - /// obsolete as soon as we get a better response storage + function create_session_and_responses(&$question, &$state, $quiz, $attempt) { + if (!$state->options->subquestions = get_records('quiz_match_sub', + 'question', $question->id)) { + notify('Error: Missing subquestions!'); + return false; + } - $delimiter = ''; - $responseanswerfield = ''; - foreach ($questionresponse as $key => $value) { - if ($matchid = $this->extract_response_id($key)) { - $responseanswerfield .= "$delimiter$matchid-$value"; - $delimiter = ','; - } else { - notify("Error: Illegal match key $key detected"); + foreach ($state->options->subquestions as $key => $subquestion) { + // This seems rather over complicated, but it is useful for the + // randomsamatch questiontype, which can then inherit the print + // and grading functions. This way it is possible to define multiple + // answers per question, each with different marks and feedback. + $answer = new stdClass(); + $answer->id = $subquestion->id; + $answer->answer = $subquestion->answertext; + $answer->fraction = 1.0; + $state->options->subquestions[$key]->options + ->answers[$subquestion->id] = clone($answer); + } + + // Shuffle the answers if required + $subquestionids = array_values(array_map(create_function('$val', + 'return $val->id;'), $state->options->subquestions)); + if ($quiz->shuffleanswers) { + $subquestionids = swapshuffle($subquestionids); + } + $state->options->order = $subquestionids; + // Create empty responses + foreach ($subquestionids as $val) { + $state->responses[$val] = ''; + } + return true; + } + + function restore_session_and_responses(&$question, &$state) { + // The serialized format for matching questions is a comma separated + // list of question answer pairs (e.g. 1-1,2-3,3-2), where the ids of + // both refer to the id in the table quiz_match_sub. + $responses = explode(',', $state->responses['']); + $responses = array_map(create_function('$val', + 'return explode("-", $val);'), $responses); + + // Restore the previous responses + $state->responses = array(); + foreach ($responses as $response) { + $state->responses[$response[0]] = $response[1]; + } + + if (!$state->options->subquestions = get_records('quiz_match_sub', + 'question', $question->id)) { + notify('Error: Missing subquestions!'); + return false; + } + + foreach ($state->options->subquestions as $key => $subquestion) { + // This seems rather over complicated, but it is useful for the + // randomsamatch questiontype, which can then inherit the print + // and grading functions. This way it is possible to define multiple + // answers per question, each with different marks and feedback. + $answer = new stdClass(); + $answer->id = $subquestion->id; + $answer->answer = $subquestion->answertext; + $answer->fraction = 1.0; + $state->options->subquestions[$key]->options + ->answers[$subquestion->id] = clone($answer); + } + + return true; + } + + function save_session_and_responses(&$question, &$state) { + // Serialize responses + $responses = array(); + foreach ($state->responses as $key => $val) { + if ($key != '') { + $responses[] = "$key-$val"; } } - return $responseanswerfield; + $responses = implode(',', $responses); + + // Set the legacy answer field + if (!set_field('quiz_states', 'answer', $responses, 'id', + $state->id)) { + return false; + } + return true; + } + + function get_correct_responses(&$question, &$state) { + $responses = array(); + foreach ($state->options->subquestions as $sub) { + foreach ($sub->options->answers as $answer) { + if (1 == $answer->fraction) { + $responses[$sub->id] = $answer->id; + } + } + } + return empty($responses) ? null : $responses; } function extract_response($rawresponse, $nameprefix) { @@ -114,8 +201,19 @@ class quiz_match_qtype extends quiz_default_questiontype { return $response; } - function print_question_formulation_and_controls($question, - $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + function print_question_formulation_and_controls(&$question, &$state, $quiz, $options) { + $subquestions = $state->options->subquestions; + $correctanswers = $this->get_correct_responses($question, $state); + $nameprefix = $question->name_prefix; + $answers = array(); + foreach ($subquestions as $subquestion) { + foreach ($subquestion->options->answers as $sub) { + $answers[$sub->id] = $sub->answer; + } + } + + // Shuffle the answers + $answers = draw_rand_array($answers, count($answers)); // Print question text and possible image if (!empty($question->questiontext)) { @@ -125,37 +223,7 @@ class quiz_match_qtype extends quiz_default_questiontype { } quiz_print_possible_question_image($quiz->id, $question); - // It so happens to be that $correctanswers for this question type also - // contains the subqustions, which we need to make sure we have: - if (empty($correctanswers)) { - $options = get_record('quiz_match', 'question', $question->id) - and $subquestions = get_records_list('quiz_match_sub', 'id', - $options->subquestions); - } else { - $subquestions = $correctanswers; - } - - /// Check whether everything turned out alright: - if (empty($subquestions)) { - notify("Error: Missing subquestions for this question!"); - - } else { - /// Everything is fine - - /// Set up $subquestions and $answers and do the shuffling: - - if ($quiz->shuffleanswers) { - $subquestions = draw_rand_array($subquestions, - count($subquestions)); - } - foreach ($subquestions as $key => $subquestion) { - unset($answers[$key]); - $answers[$subquestion->id] = $subquestion->answertext; - } - $answers = draw_rand_array($answers, count($answers)); - } - ///// Print the input controls ////// - echo ''; foreach ($subquestions as $subquestion) { @@ -167,76 +235,60 @@ class quiz_match_qtype extends quiz_default_questiontype { /// Drop-down list: $menuname = $nameprefix.$subquestion->id; - $response = isset($question->response[$menuname]) - ? $question->response[$menuname] : '0'; - if ($readonly - and $quiz->correctanswers - and isset($correctanswers[$menuname]) - and ($correctanswers[$menuname]->id == $response)) { + $response = isset($state->responses[$subquestion->id]) + ? $state->responses[$subquestion->id] : '0'; + if ($options->readonly + and $options->correct_responses + and isset($correctanswers[$subquestion->id]) + and ($correctanswers[$subquestion->id] == $response)) { $class = ' class="highlight" '; } else { $class = ''; } echo "'; } echo '
"; - choose_from_menu($answers, $menuname, $response); - if ($quiz->feedback && isset($answers[$menuname]) - && $answers[$menuname]->feedback) { - quiz_print_comment($answers[$menuname]->feedback); + + choose_from_menu($answers, $menuname, $response, 'choose', '', 0, + false, $options->readonly); + /* There is currently no feedback for this questiontype. + if ($quiz->feedback && isset($subquestion->feedback)) { + quiz_print_comment($subquestion->feedback); } + */ echo '
'; } - function grade_response($question, $nameprefix) { - /// This question type does not use the table quiz_answers - /// but we will take some measures to emulate that record anyway. + function grade_responses(&$question, &$state, $quiz) { + $subquestions = $state->options->subquestions; + $responses = &$state->responses; - $result->grade = 0.0; - $result->answers = array(); - $result->correctanswers = array(); - - if (!($options = get_record('quiz_match', 'question', $question->id) - and $subquestions = get_records_list('quiz_match_sub', - 'id', $options->subquestions))) { - notify("Error: Cannot find match options and subquestions - for question $question->id"); - return $result; - } - - $fraction = 1.0 / count($subquestions); - - /// Populate correctanswers arrays: + $answers = array(); foreach ($subquestions as $subquestion) { - $subquestion->fraction = $fraction; - $subquestion->answer = $subquestion->answertext; - $subquestion->feedback = ''; - $result->correctanswers[$nameprefix.$subquestion->id] = - $subquestion; - } - - foreach ($question->response as $responsekey => $answerid) { - - if ($answerid and $answer = - $result->correctanswers[$nameprefix.$answerid]) { - - if ($result->correctanswers[$responsekey]->answer - == $answer->answer) { - - /// The response was correct! - $result->answers[$responsekey] = - $result->correctanswers[$responsekey]; - $result->grade += $fraction; - - } else { - /// The response was incorrect: - $answer->fraction = 0.0; - $result->answers[$responsekey] = $answer; - } - + foreach ($subquestion->options->answers as $sub) { + $answers[$sub->id] = $sub->answer; } } - return $result; + + $sumgrade = 0; + foreach ($subquestions as $key => $sub) { + if (isset($sub->options->answers[$responses[$key]])) { + $sumgrade += $sub->options->answers[$responses[$key]]->fraction; + } + } + + $state->raw_grade = $sumgrade/count($subquestions); + if (empty($state->raw_grade)) { + $state->raw_grade = 0; + } + + // Make sure we don't assign negative or too high marks + $state->raw_grade = min(max((float) $state->raw_grade, + 0.0), 1.0) * $question->maxgrade; + $state->penalty = 0; + // Only allow one attempt at the question + $state->event = QUIZ_EVENTCLOSE; + return true; } } //// END OF CLASS //// diff --git a/mod/quiz/questiontypes/multianswer/editquestion.php b/mod/quiz/questiontypes/multianswer/editquestion.php index b9feb484021..beb3a65d3fa 100644 --- a/mod/quiz/questiontypes/multianswer/editquestion.php +++ b/mod/quiz/questiontypes/multianswer/editquestion.php @@ -1,41 +1,54 @@ questiontext and $question->id) { - $answers = quiz_get_answers($question); + if ($question->questiontext and $question->id and $question->qtype) { + global $QUIZ_QTYPES; + $QUIZ_QTYPES[$question->qtype]->get_question_options($question); - foreach ($answers as $multianswer) { - $parsableanswerdef = '{' . $multianswer->norm . ':'; - switch ($multianswer->answertype) { - case MULTICHOICE: - $parsableanswerdef .= 'MULTICHOICE:'; - break; - case SHORTANSWER: - $parsableanswerdef .= 'SHORTANSWER:'; - break; - case NUMERICAL: - $parsableanswerdef .= 'NUMERICAL:'; - break; - default: - error("answertype $multianswer->answertype not recognized"); - } - $separator= ''; - foreach ($multianswer->subanswers as $subanswer) { - $parsableanswerdef .= $separator - . '%' . round(100*$subanswer->fraction) . '%'; - $parsableanswerdef .= $subanswer->answer; - if (isset($subanswer->min) && isset($subanswer->max) - and $subanswer->min || $subanswer->max) { - // Special for numerical answers: - $errormargin = $subanswer->answer - $subanswer->min; - $parsableanswerdef .= ":$errormargin"; + foreach ($question->options->questions as $key => $wrapped) { + // The old way of restoring the definitions is kept to gradually + // update all multianswer questions + if (empty($wrapped->questiontext)) { + $parsableanswerdef = '{' . $wrapped->defaultgrade . ':'; + switch ($wrapped->qtype) { + case MULTICHOICE: + $parsableanswerdef .= 'MULTICHOICE:'; + break; + case SHORTANSWER: + $parsableanswerdef .= 'SHORTANSWER:'; + break; + case NUMERICAL: + $parsableanswerdef .= 'NUMERICAL:'; + break; + default: + error("questiontype $wrapped->qtype not recognized"); } - if ($subanswer->feedback) { - $parsableanswerdef .= "#$subanswer->feedback"; + $separator= ''; + foreach ($wrapped->options->answers as $subanswer) { + $parsableanswerdef .= $separator + . '%' . round(100*$subanswer->fraction) . '%'; + $parsableanswerdef .= $subanswer->answer; + if (!empty($wrapped->options->tolerance)) { + // Special for numerical answers: + $parsableanswerdef .= ":{$wrapped->options->tolerance}"; + // We only want tolerance for the first alternative, it will + // be applied to all of the alternatives. + unset($wrapped->options->tolerance); + } + if ($subanswer->feedback) { + $parsableanswerdef .= "#$subanswer->feedback"; + } + $separator = '~'; } - $separator = '~'; + $parsableanswerdef .= '}'; + // Fix the questiontext fields of old questions + set_field('quiz_questions', 'questiontext', $parsableanswerdef, + 'id', $wrapped->id); + } else { + $parsableanswerdef = $wrapped->questiontext; } - $parsableanswerdef .= '}'; + //echo "{#$key}
"; + //echo $parsableanswerdef.'
'; $question->questiontext = str_replace - ("{#$multianswer->positionkey}", $parsableanswerdef, + ("{#$key}", $parsableanswerdef, $question->questiontext); } } diff --git a/mod/quiz/questiontypes/multianswer/questiontype.php b/mod/quiz/questiontypes/multianswer/questiontype.php index 04d0f802d55..d7ce8e7d558 100644 --- a/mod/quiz/questiontypes/multianswer/questiontype.php +++ b/mod/quiz/questiontypes/multianswer/questiontype.php @@ -15,99 +15,81 @@ /// QUESTION TYPE CLASS ////////////////// class quiz_embedded_cloze_qtype extends quiz_default_questiontype { - function get_answers($question) { - // This function is not used by any other function within this class. - // It is possible that it is used by some report that uses the - // function quiz_get_answers in lib.php - - /// The returned answers includes subanswers... - // As this question type embedds some other question types, - // it is necessary to have access to those: - global $QUIZ_QTYPES; - - $answers = array(); - - $virtualquestion->id = $question->id; - - if ($multianswers = get_records('quiz_multianswers', 'question', $question->id)) { - foreach ($multianswers as $multianswer) { - $virtualquestion->qtype = $multianswer->answertype; - // Call to other question type for subanswers - $addedcondition = " AND a.id IN ($multianswer->answers) "; - $multianswer->subanswers = - $QUIZ_QTYPES[$multianswer->answertype] - ->get_answers($virtualquestion, $addedcondition); - $answers[] = $multianswer; - } - } - return $answers; - } - - function get_position_multianswer($questionid, $positionkey) { - // As a separate function in order to make it overridable - - return get_record('quiz_multianswers', 'question', $questionid, - 'positionkey', $positionkey); - } - - function get_multianswers($questionid) { - // As a separate function in order to make it overridable - return get_records('quiz_multianswers', 'question', $questionid); - } - function name() { return 'multianswer'; } + function get_question_options(&$question) { + // Get relevant data indexed by positionkey from the multianswers table + if (!$sequence = get_field('quiz_multianswers', 'sequence', 'question', + $question->id)) { + notify('Error: Missing question options!'); + return false; + } + + global $QUIZ_QTYPES; + // Temporary solution to be replaced with commented out version after + // redundant wrapped questions are deleted when necessary. + // $wrappedquestions = get_records('quiz_questions', 'parent', $question->id); + $wrappedquestions = get_records_list('quiz_questions', 'id', $sequence); + + // We want an array with question ids as index and the positions as values + $sequence = array_flip(explode(',', $sequence)); + array_walk($sequence, create_function('&$val', '$val++;')); + + foreach ($wrappedquestions as $wrapped) { + if (!$QUIZ_QTYPES[$wrapped->qtype] + ->get_question_options($wrapped)) { + error("Unable to recognized questiontype " . + $wrapped->qtype); + } + $wrapped->maxgrade = $wrapped->defaultgrade; + $question->options->questions[$sequence[$wrapped->id]] = clone($wrapped); + } + + return true; + } + function save_question_options($question) { - if (!$oldmultianswers = get_records("quiz_multianswers", "question", $question->id, "id ASC")) { - $oldmultianswers = array(); + global $QUIZ_QTYPES; + if (!$oldwrappedids = + get_records('quiz_questions', 'parent', $question->id, '', 'id, id')) { + // We need to select 'id, id' because the first one is consumed by + // get_records. + $oldwrappedids = array(); } - - if (empty($question->name)) { - $result->notice = get_string("missingname", "quiz"); - return $result; + $oldwrappedids = array_keys($oldwrappedids); + $sequence = array(); + foreach($question->options->questions as $wrapped) { + if ($oldwrappedid = array_shift($oldwrappedids)) { + $wrapped->id = $oldwrappedid; + } + $wrapped->name = $question->name; + $wrapped->parent = $question->id; + $wrapped->category = $question->category; + $wrapped->version = $question->version; + $wrapped = $QUIZ_QTYPES[$wrapped->qtype]->save_question($wrapped, + $wrapped, $question->course); + $sequence[] = $wrapped->id; } - if (!$question->answers) { - $result->notice = get_string('noquestionintext', 'quiz'); - return $result; - } - - // Insert all the new multi answers - foreach ($question->answers as $dataanswer) { - if ($oldmultianswer = array_shift($oldmultianswers)) { // Existing answer, so reuse it - $multianswer = $oldmultianswer; - $multianswer->positionkey = $dataanswer->positionkey; - $multianswer->norm = $dataanswer->norm; - $multianswer->answertype = $dataanswer->answertype; - if (! $multianswer->answers = - quiz_qtype_multianswer_save_alternatives - ($question->id, $dataanswer->answertype, - $dataanswer->alternatives, $oldmultianswer->answers)) - { - $result->error = "Could not update multianswer alternatives! (id=$multianswer->id)"; - return $result; - } + // Delete redundant wrapped questions + $oldwrappedids = implode(',', $oldwrappedids); + delete_records_select('quiz_questions', "id IN ($oldwrappedids)"); + + if (!empty($sequence)) { + $multianswer = new stdClass; + $multianswer->question = $question->id; + $multianswer->sequence = implode(',', $sequence); + if ($oldid = + get_field('quiz_multianswers', 'id', 'question', $question->id)) { + $multianswer->id = $oldid; if (!update_record("quiz_multianswers", $multianswer)) { - $result->error = "Could not update quiz multianswer! (id=$multianswer->id)"; - return $result; - } - } else { // This is a completely new answer - unset($multianswer); - $multianswer->question = $question->id; - $multianswer->positionkey = $dataanswer->positionkey; - $multianswer->norm = $dataanswer->norm; - $multianswer->answertype = $dataanswer->answertype; - - if (! $multianswer->answers = - quiz_qtype_multianswer_save_alternatives - ($question->id, $dataanswer->answertype, - $dataanswer->alternatives)) - { - $result->error = "Could not insert multianswer alternatives! (questionid=$question->id)"; + $result->error = "Could not update quiz multianswer! " . + "(id=$multianswer->id)"; return $result; } + } else { if (!insert_record("quiz_multianswers", $multianswer)) { $result->error = "Could not insert quiz multianswer!"; return $result; @@ -115,75 +97,66 @@ class quiz_embedded_cloze_qtype extends quiz_default_questiontype { } } } - - function save_question($authorizedquestion, $form, $course) { - $question = quiz_qtype_multianswer_extract_question - ($form->questiontext); + function save_question($authorizedquestion, $form, $course) { + $question = + quiz_qtype_multianswer_extract_question ($form->questiontext); if (isset($authorizedquestion->id)) { $question->id = $authorizedquestion->id; - } - $question->qtype = $authorizedquestion->qtype; - $question->category = $authorizedquestion->category; - - $question->name = $form->name; - if (empty($form->image)) { - $question->image = ""; + $question->version = $form->version = $authorizedquestion->version; } else { - $question->image = $form->image; + $question->version = $form->version = 1; } - if (!empty($question->id)) { // Question already exists - if (!update_record("quiz_questions", $question)) { - error("Could not update question!"); - } - } else { // Question is a new one - $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) - if (!$question->id = insert_record("quiz_questions", $question)) { - error("Could not insert new question!"); - } - } - // Now to save all the answers and type-specific options - - $form->id = $question->id; - $form->qtype = $question->qtype; - $form->category = $question->category; - - $result = $this->save_question_options($question); - - 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; - } - - return $question; + $question->category = $authorizedquestion->category; + $form->course = $course; // To pass the course object to + // save_question_options, where it is + // needed to call type specific + // save_question methods. + $form->defaultgrade = $question->defaultgrade; + $form->questiontext = $question->questiontext; + $form->questiontextformat = 0; + $form->options = clone($question->options); + unset($question->options); + return parent::save_question($question, $form, $course); } - - function convert_to_response_answer_field($questionresponse) { - /// This method, together with extract_response, should be - /// obsolete as soon as we get a better response storage - $delimiter = ''; - $responseanswerfield = ''; - foreach ($questionresponse as $key => $value) { - if ($multianswerid = $this->extract_response_id($key)) { - $responseanswerfield .= "$delimiter$multianswerid-$value"; - $delimiter = ','; - } else { - notify("Error: Illegal match key $key detected"); - } + function create_session_and_responses(&$question, &$state, $quiz, $attempt) { + $state->responses = array(); + foreach ($question->options->questions as $key => $wrapped) { + $state->responses[$key] = ''; } - return $responseanswerfield; + return true; + } + + function restore_session_and_responses(&$question, &$state) { + $responses = explode(',', $state->responses['']); + $state->responses = array(); + foreach ($responses as $response) { + $tmp = explode("-", $response); + // restore encoded characters + $state->responses[$tmp[0]] = + str_replace(array(",", "-"), array(",", "-"), $tmp[1]); + } + return true; + } + + function save_session_and_responses(&$question, &$state) { + $responses = $state->responses; + array_walk($responses, create_function('&$val, $key', + // encode - (hyphen) and , (comma) to - because they are used as + // delimiters + '$val = str_replace(array(",", "-"), array(",", "-"), $val); + $val = "$key-$val";')); + $responses = implode(',', $responses); + + // Set the legacy answer field + if (!set_field('quiz_states', 'answer', $responses, 'id', + $state->id)) { + return false; + } + return true; } function extract_response($rawresponse, $nameprefix) { @@ -204,8 +177,22 @@ class quiz_embedded_cloze_qtype extends quiz_default_questiontype { return $response; } - function print_question_formulation_and_controls($question, - $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + function get_correct_responses(&$question, &$state) { + global $QUIZ_QTYPES; + $responses = array(); + foreach($question->options->questions as $key => $wrapped) { + $correct = $QUIZ_QTYPES[$wrapped->qtype] + ->get_correct_responses($wrapped, $state); + $responses[$key] = $correct['']; + } + return $responses; + } + + function print_question_formulation_and_controls(&$question, &$state, $quiz, + $options) { + global $QUIZ_QTYPES; + $readonly = empty($options->readonly) ? '' : 'disabled="disabled"'; + $nameprefix = $question->name_prefix; // For this question type, we better print the image on top: quiz_print_possible_question_image($quiz->id, $question); @@ -224,45 +211,77 @@ class quiz_embedded_cloze_qtype extends quiz_default_questiontype { echo $qtextsplits[0]; $qtextremaining = $qtextsplits[1]; - $multianswer = $this->get_position_multianswer($question->id, $regs[1]); + $positionkey = $regs[1]; + $wrapped = &$question->options->questions[$positionkey]; + $answers = &$wrapped->options->answers; + $correctanswers = $QUIZ_QTYPES[$wrapped->qtype] + ->get_correct_responses($wrapped, $state); - $inputname = $nameprefix.$multianswer->id; - $response = isset($question->response[$inputname]) - ? $question->response[$inputname] : ''; + $inputname = $nameprefix.$positionkey; + $response = isset($state->responses[$positionkey]) + ? $state->responses[$positionkey] : null; - /// Determine style - if (!empty($correctanswers) && '' !== $response) { - - if (!isset($answers[$inputname]) - || $answers[$inputname]->fraction <= 0.0) { - // The response must have been totally wrong: - $style = ' style="background-color:red" '; - - } else if ($answers[$inputname]->fraction >= 1.0) { - // The response must was correct!! - $style = 'style="background-color:lime"'; - - } else { - // This response did at least give some credit: - $style = 'style="background-color:yellow"'; - } - } else { - // No colorish feedback is to be used - $style = ''; - } // Determine feedback popup if any - if ($quiz->feedback && isset($answers[$inputname]) - && '' !== $answers[$inputname]->feedback) { - $title = str_replace("'", "\\'", $answers[$inputname]->feedback); - $popup = " onmouseover=\"return overlib('$title', CAPTION, '$strfeedback', FGCOLOR);\" ". - " onmouseout=\"return nd();\" "; - } else { - $popup = ''; + $popup = ''; + $style = ''; + if ($options->feedback) { + $chosenanswer = null; + switch ($wrapped->qtype) { + case NUMERICAL: + case SHORTANSWER: + $testedstate = clone($state); + $testedstate->responses[''] = $response; + $teststate = clone($state); + $raw_grade = 0; + foreach ($answers as $answer) { + $teststate->responses[''] = trim($answer->answer); + if($QUIZ_QTYPES[$wrapped->qtype] + ->compare_responses($wrapped, $testedstate, $teststate)) { + if (empty($raw_grade) || $raw_grade < $answer->fraction) { + //var_dump($teststate->responses['']); + //var_dump($testedstate->responses['']); + $chosenanswer = clone($answer); + } + } + } + break; + case MULTICHOICE: + if (isset($answers[$response])) { + $chosenanswer = clone($answers[$response]); + } + break; + default: + break; + } + + if (!empty($chosenanswer->feedback)) { + $feedback = str_replace("'", "\\'", $chosenanswer->feedback); + $popup = " onmouseover=\"return overlib('$feedback', CAPTION, '$strfeedback', FGCOLOR);\" ". + " onmouseout=\"return nd();\" "; + } + + /// Determine style + if (!empty($chosenanswer)) { + if (!isset($chosenanswer->fraction) + || $chosenanswer->fraction <= 0.0) { + // The response must have been totally wrong: + $style = ' style="background-color:red" '; + + } else if ($chosenanswer->fraction >= 1.0) { + // The response was correct!! + $style = 'style="background-color:lime"'; + + } else { + // This response did at least give some credit: + $style = 'style="background-color:yellow"'; + } + } + } // Print the input control - switch ($multianswer->answertype) { + switch ($wrapped->qtype) { case SHORTANSWER: case NUMERICAL: echo " answers); - foreach ($mcanswers as $mcanswer) { + foreach ($answers as $mcanswer) { $selected = $response == $mcanswer->id ? ' selected="selected" ' : ''; $outputoptions .= ""; } - echo ""; echo $outputoptions; echo ''; break; @@ -290,56 +308,30 @@ class quiz_embedded_cloze_qtype extends quiz_default_questiontype { echo $qtextremaining; } - function grade_response($question, $nameprefix) { - + function grade_responses(&$question, &$state, $quiz) { global $QUIZ_QTYPES; - - $result->grade = 0.0; - $result->answers = array(); - $result->correctanswers = array(); - - $multianswers = $this->get_multianswers($question->id); - // Default settings: - $subquestion->id = $question->id; - $normsum = 0; - - // Grade each multianswer - foreach ($multianswers as $multianswer) { - $name = $nameprefix.$multianswer->id; - $subquestion->response[$nameprefix] = - isset($question->response[$name]) - ? $question->response[$name] : ''; - - $subresult = $QUIZ_QTYPES[$multianswer->answertype] - ->grade_response($subquestion, $nameprefix, - " AND a.id IN ($multianswer->answers) "); - - // Summarize subquestion results: - - if (isset($subresult->answers[$nameprefix])) { - - /// Answer was found: - $result->answers[$name] = $subresult->answers[$nameprefix]; - - if ($result->answers[$name]->fraction >= 1.0) { - // This is also the correct answer: - $result->correctanswers[$name] = $result->answers[$name]; - } + $teststate = clone($state); + $state->raw_grade = 0; + foreach($question->options->questions as $key => $wrapped) { + $teststate->responses = array('' => $state->responses[$key]); + $teststate->raw_grade = 0; + if (false === $QUIZ_QTYPES[$wrapped->qtype] + ->grade_responses($wrapped, $teststate, $quiz)) { + return false; } - - if (!isset($result->correctanswers[$name])) { - // Pick the first correctanswer: - foreach ($subresult->correctanswers as $correctanswer) { - $result->correctanswers[$name] = $correctanswer; - break; - } - } - $result->grade += $multianswer->norm * $subresult->grade; - $normsum += $multianswer->norm; + $state->raw_grade += $teststate->raw_grade; } - $result->grade /= $normsum; + $state->raw_grade /= $question->defaultgrade; + $state->raw_grade = min(max((float) $state->raw_grade, 0.0), 1.0) + * $question->maxgrade; - return $result; + if (empty($state->raw_grade)) { + $state->raw_grade = 0.0; + } + $state->penalty = 0; + // Only allow one attempt at the question + $state->event = QUIZ_EVENTCLOSE; + return true; } } //// END OF CLASS //// @@ -360,6 +352,7 @@ $QUIZ_QTYPES[MULTIANSWER]= new quiz_embedded_cloze_qtype(); //// likely to be subject for overriding. ///////////////////////////////////////////////////////////// + function quiz_qtype_multianswer_extract_question($text) { //////////////////////////////////////////////// @@ -428,216 +421,77 @@ function quiz_qtype_multianswer_extract_question($text) { //// Start of the actual function //////////////////////////////////////// - $question = NULL; + $question = new stdClass; $question->qtype= MULTIANSWER; $question->questiontext= $text; - $question->answers= array(); + $question->options->questions = array(); $question->defaultgrade = 0; // Will be increased for each answer norm for ($positionkey=1 ; ereg(ANSWER_REGEX, $question->questiontext, $answerregs) - ; ++$positionkey ) - { - unset($multianswer); - - $multianswer->positionkey = $positionkey; - $multianswer->norm = $answerregs[ANSWER_REGEX_NORM] - or $multianswer->norm = '1'; + ; ++$positionkey ) { + $wrapped = new stdClass; + $wrapped->defaultgrade = $answerregs[ANSWER_REGEX_NORM] + or $wrapped->defaultgrade = '1'; if ($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL]) { - $multianswer->answertype = NUMERICAL; + $wrapped->qtype = NUMERICAL; + $wrapped->multiplier = array(); + $wrapped->units = array(); } else if($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER]) { - $multianswer->answertype = SHORTANSWER; + $wrapped->qtype = SHORTANSWER; + $wrapped->usecase = 0; } else if($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE]){ - $multianswer->answertype = MULTICHOICE; + $wrapped->qtype = MULTICHOICE; + $wrapped->single = 1; } else { - error("Cannot identify answertype $answerregs[2]"); + error("Cannot identify qtype $answerregs[2]"); return false; } - $multianswer->alternatives= array(); + // Each $wrapped simulates a $form that can be processed by the + // respective save_question and save_question_options methods of the + // wrapped questiontypes + $wrapped->answer = array(); + $wrapped->fraction = array(); + $wrapped->feedback = array(); + $wrapped->questiontext = $answerregs[0]; + $wrapped->questiontextformat = 0; + $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES]; while (ereg(ANSWER_ALTERNATIVE_REGEX, $remainingalts, $altregs)) { - unset($alternative); - if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) { - $alternative->fraction = '1'; + $wrapped->fraction[] = '1'; + } else if ($percentile = + $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]){ + $wrapped->fraction[] = .01 * $percentile; } else { - $alternative->fraction = .01 * - $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION] - or $alternative->fraction = '0'; + $wrapped->fraction[] = '0'; } - $alternative->feedback = $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK]; + $wrapped->feedback[] = $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK]; if ($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL] && ereg(NUMERICAL_ALTERNATIVE_REGEX, $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs) ) { - $alternative->answer = $numregs[NUMERICAL_CORRECT_ANSWER]; + $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER]; if ($numregs[NUMERICAL_ABS_ERROR_MARGIN]) { - $alternative->min = $numregs[NUMERICAL_CORRECT_ANSWER] - - $numregs[NUMERICAL_ABS_ERROR_MARGIN]; - $alternative->max = $numregs[NUMERICAL_CORRECT_ANSWER] - + $numregs[NUMERICAL_ABS_ERROR_MARGIN]; + $wrapped->tolerance = $numregs[NUMERICAL_ABS_ERROR_MARGIN]; } else { - $alternative->min = $numregs[NUMERICAL_CORRECT_ANSWER]; - $alternative->max = $numregs[NUMERICAL_CORRECT_ANSWER]; + $wrapped->tolerance = 0; } - } else { // Min and max must stay undefined... - $alternative->answer = - $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER]; + } else { // Tolerance can stay undefined for non numerical questions + $wrapped->answer[] = $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER]; } - - $multianswer->alternatives[] = $alternative; $tmp = explode($altregs[0], $remainingalts, 2); $remainingalts = $tmp[1]; } - $question->defaultgrade += $multianswer->norm; - $question->answers[] = $multianswer; + $question->defaultgrade += $wrapped->defaultgrade; + $question->options->questions[$positionkey] = clone($wrapped); $question->questiontext = implode("{#$positionkey}", explode($answerregs[0], $question->questiontext, 2)); } return $question; } -function quiz_qtype_multianswer_save_alternatives($questionid, - $answertype, $alternatives, $oldalternativeids= NULL) { -// Returns false if something goes wrong, -// otherwise the ids of the answers. - - if (empty($oldalternativeids) - or !($oldalternatives = - get_records_list('quiz_answers', 'id', $oldalternativeids))) - { - $oldalternatives = array(); - } - - $alternativeids = array(); - - foreach ($alternatives as $altdata) { - - if ($altold = array_shift($oldalternatives)) { // Use existing one... - $alt = $altold; - $alt->answer = $altdata->answer; - $alt->fraction = $altdata->fraction; - $alt->feedback = $altdata->feedback; - if (!update_record("quiz_answers", $alt)) { - return false; - } - - } else { // Completely new one - unset($alt); - $alt->question= $questionid; - $alt->answer = $altdata->answer; - $alt->fraction = $altdata->fraction; - $alt->feedback = $altdata->feedback; - if (!($alt->id = insert_record("quiz_answers", $alt))) { - return false; - } - } - - // For the answer type numerical, each alternative has individual options: - if ($answertype == NUMERICAL) { - if ($numericaloptions = - get_record('quiz_numerical', 'answer', $alt->id)) - { - // Reuse existing numerical options - $numericaloptions->min = $altdata->min; - $numericaloptions->max = $altdata->max; - if (!update_record('quiz_numerical', $numericaloptions)) { - return false; - } - } else { - // New numerical options - $numericaloptions->answer = $alt->id; - $numericaloptions->question = $questionid; - $numericaloptions->min = $altdata->min; - $numericaloptions->max = $altdata->max; - if (!insert_record("quiz_numerical", $numericaloptions)) { - return false; - } - } - } else { // Delete obsolete numerical options - delete_records('quiz_numerical', 'answer', $alt->id); - } // end if NUMERICAL - - $alternativeids[] = $alt->id; - } // end foreach $alternatives - $answers = implode(',', $alternativeids); - - // Removal of obsolete alternatives from answers and quiz_numerical: - while ($altobsolete = array_shift($oldalternatives)) { - delete_records("quiz_answers", "id", $altobsolete->id); - - // Possibly obsolute numerical options are also to be deleted: - delete_records("quiz_numerical", 'answer', $altobsolete->id); - } - - // Common alternative options and removal of obsolete options - switch ($answertype) { - case NUMERICAL: - if (!empty($oldalternativeids)) { - delete_records('quiz_shortanswer', 'answers', -$oldalternativeids); - delete_records('quiz_multichoice', 'answers', -$oldalternativeids); - } - break; - case SHORTANSWER: - if (!empty($oldalternativeids)) { - delete_records('quiz_multichoice', 'answers', -$oldalternativeids); - $options = get_record('quiz_shortanswer', - 'answers', $oldalternativeids); - } else { - unset($options); - } - if (empty($options)) { - // Create new shortanswer options - $options->question = $questionid; - $options->usecase = 0; - $options->answers = $answers; - if (!insert_record('quiz_shortanswer', $options)) { - return false; - } - } else if ($answers != $oldalternativeids) { - // Shortanswer options needs update: - $options->answers = $answers; - if (!update_record('quiz_shortanswer', $options)) { - return false; - } - } - break; - case MULTICHOICE: - if (!empty($oldalternativeids)) { - delete_records('quiz_shortanswer', 'answers', -$oldalternativeids); - $options = get_record('quiz_multichoice', - 'answers', $oldalternativeids); - } else { - unset($options); - } - if (empty($options)) { - // Create new multichoice options - $options->question = $questionid; - $options->layout = 0; - $options->single = 1; - $options->answers = $answers; - if (!insert_record('quiz_multichoice', $options)) { - return false; - } - } else if ($answers != $oldalternativeids) { - // Multichoice options needs update: - $options->answers = $answers; - if (!update_record('quiz_multichoice', $options)) { - return false; - } - } - break; - default: - return false; - } - return $answers; -} - ?> diff --git a/mod/quiz/questiontypes/multichoice/questiontype.php b/mod/quiz/questiontypes/multichoice/questiontype.php index 8a3ea96ddbe..c6e96945c40 100644 --- a/mod/quiz/questiontypes/multichoice/questiontype.php +++ b/mod/quiz/questiontypes/multichoice/questiontype.php @@ -13,28 +13,30 @@ class quiz_multichoice_qtype extends quiz_default_questiontype { - function get_answers($question, $addedcondition= '') { - // The added condition is one addition that has been added - // to the behaviour of this question type in order to make - // it embeddable within a multianswer (embedded cloze) question - - global $CFG; - - // There should be multiple answers - return get_records_sql("SELECT a.*, mc.single - FROM {$CFG->prefix}quiz_multichoice mc, - {$CFG->prefix}quiz_answers a - WHERE mc.question = '$question->id' - AND mc.question = a.question " - . $addedcondition); - } - function name() { return 'multichoice'; } + function get_question_options(&$question) { + // Get additional information from database + // and attach it to the question object + if (!$question->options = get_record('quiz_multichoice', 'question', + $question->id)) { + notify('Error: Missing question options!'); + return false; + } + + if (!$question->options->answers = get_records('quiz_answers', 'question', + $question->id)) { + notify('Error: Missing question answers!'); + return false; + } + + return true; + } + function save_question_options($question) { - + if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { $oldanswers = array(); @@ -130,6 +132,85 @@ class quiz_multichoice_qtype extends quiz_default_questiontype { return true; } + function create_session_and_responses(&$question, &$state, $quiz, $attempt) { + // Shuffle the answers if required + $answerids = array_values(array_map(create_function('$val', + 'return $val->id;'), $question->options->answers)); + if ($quiz->shuffleanswers) { + $answerids = swapshuffle($answerids); + } + $state->options->order = $answerids; + // Create empty responses + if ($question->options->single) { + $state->responses = array('' => ''); + } else { + $state->responses = array(); + } + return true; + } + + + function restore_session_and_responses(&$question, &$state) { + // The serialized format for multiple choice quetsions + // is (optionally) a comma separated list of answer ids + // followed by a colon, followed by another comma separated + // list of answer ids, which are the radio/checkboxes that were + // ticked. + // E.g. 1,3,2,4:2,4 means that the answers were shown in the order + // 1, 3, 2 and then 4 and the answers 2 and 4 were checked. + + $pos = strpos($state->responses[''], ':'); + if (false === $pos) { // No order of answers is given, so use the default + $state->options->order = array_keys($question->options->answers); + } else { // Restore the order of the answers + $state->options->order = explode(',', substr($state->responses[''], 0, + $pos)); + $state->responses[''] = substr($state->responses[''], $pos + 1); + } + // Restore the responses + if (empty($state->responses[''])) { // No previous responses + if ($question->options->single) { + $state->responses = array('' => ''); + } else { + $state->responses = array(); + } + } else { + if ($question->options->single) { + $state->responses = array('' => $state->responses['']); + } else { + // Get array of answer ids + $state->responses = explode(',', $state->responses); + // Create an array indexed by these answer ids + $state->responses = array_flip($state->responses); + // Set the value of each element to be equal to the index + array_walk($state->responses, create_function('&$a, $b', + '$a = $b;')); + } + } + return true; + } + + function save_session_and_responses(&$question, &$state) { + // Bundle the answer order and the responses into the legacy answer + // field. + // The serialized format for multiple choice quetsions + // is (optionally) a comma separated list of answer ids + // followed by a colon, followed by another comma separated + // list of answer ids, which are the radio/checkboxes that were + // ticked. + // E.g. 1,3,2,4:2,4 means that the answers were shown in the order + // 1, 3, 2 and then 4 and the answers 2 and 4 were checked. + $responses = implode(',', $state->options->order) . ':'; + $responses .= implode(',', $state->responses); + + // Set the legacy answer field + if (!set_field('quiz_states', 'answer', $responses, 'id', + $state->id)) { + return false; + } + return true; + } + function extract_response($rawresponse, $nameprefix) { // Fetch additional details from the database... if (!$options = get_record("quiz_multichoice", @@ -153,16 +234,33 @@ class quiz_multichoice_qtype extends quiz_default_questiontype { } } - function print_question_formulation_and_controls($question, - $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + function get_correct_responses(&$question, &$state) { + if ($question->options->single) { + foreach ($question->options->answers as $answer) { + if (((int) $answer->fraction) === 1) { + return array('' => $answer->id); + } + } + return null; + } else { + $responses = array(); + foreach ($question->options->answers as $answer) { + if (((float) $answer->fraction) > 0.0) { + $responses[$answer->id] = (string) $answer->id; + } + } + return empty($responses) ? null : $responses; + } + } - // Fetch additional details from the database... - if (!$options = get_record("quiz_multichoice", "question", $question->id)) { - notify("Error: Missing question options!"); - } - if (!$answers = get_records_list("quiz_answers", "id", $options->answers)) { - notify("Error: Missing question answers!"); - } + function print_question_formulation_and_controls(&$question, &$state, $quiz, $options) { + + $answers = &$question->options->answers; + $correctanswers = $this->get_correct_responses($question, $state); + $readonly = empty($options->readonly) ? '' : 'disabled="disabled"'; + + $formatoptions = new stdClass; + $formatoptions->para = false; // Print formulation echo format_text($question->questiontext, @@ -171,97 +269,106 @@ class quiz_multichoice_qtype extends quiz_default_questiontype { quiz_print_possible_question_image($quiz->id, $question); // Print input controls and alternatives - echo ""; - $stranswer = get_string("answer", "quiz"); + echo '
'; + $stranswer = get_string('answer', 'quiz'); echo "
$stranswer:  "; - echo ""; - $answerids = explode(",", $options->answers); + echo '
'; - if ($quiz->shuffleanswers) { - $answerids = swapshuffle($answerids); - } - - // Handle the case of unanswered single-choice questions: - if ($options->single) { - $singleresponse = isset($question->response[$nameprefix]) - ? $question->response[$nameprefix] : '0'; - } - - foreach ($answerids as $key => $aid) { - $answer = $answers[$aid]; + // Print each answer in a separate row + foreach ($state->options->order as $key => $aid) { + $answer = &$answers[$aid]; $qnumchar = chr(ord('a') + $key); echo '"; - - /// remove para tags - not needed - $options->para = false; - - if ($readonly and $quiz->correctanswers and !empty($correctanswers[$nameprefix.$aid])) { - echo ''; + + + // Print the control + echo "answer) . '" />'; + + echo ''; + + // Print the text by the control highlighting if correct responses + // should be shown and the current answer is the correct answer in + // the single selection case or has a positive score in the multiple + // selection case + if ($options->readonly && $options->correct_responses && + in_array($aid, $correctanswers)) { + echo ''; } else { - echo ''; + echo ''; } - if ($quiz->feedback) { - echo ""; + + // Print feedback by selected options if feedback is on + if (($options->feedback || $options->correct_responses) && + $checked) { + echo ''; } - echo ""; + + echo ''; } - echo "
'; - if ($options->single) { - $type = ' type="radio" '; - $name = " name=\"$nameprefix\" "; - $checked = $singleresponse == $aid - ? ' checked="checked" ' : ''; + if ($question->options->single) { + $type = 'type="radio"'; + $name = "name=\"{$question->name_prefix}\""; + $checked = $aid == $state->responses[''] + ? 'checked="checked"' : ''; } else { $type = ' type="checkbox" '; - $name = " name=\"$nameprefix$aid\" "; - $checked = !empty($question->response[$nameprefix.$aid]) - ? ' checked="checked" ' : ''; + $name = "name=\"{$question->name_prefix}$aid\""; + $checked = isset($state->responses[$aid]) + ? 'checked="checked"' : ''; } - echo "id\" alt=\"".s($answer->answer)."\" />"; - - echo "'.format_text("$qnumchar. $answer->answer", FORMAT_MOODLE , $options).'' . + format_text("$qnumchar. $answer->answer", FORMAT_MOODLE , + $formatoptions) . ''.format_text("$qnumchar. $answer->answer", FORMAT_MOODLE, $options).'' . + format_text("$qnumchar. $answer->answer", FORMAT_MOODLE, + $formatoptions) . ' "; - if ($checked) { // Simpliest condition to use here - quiz_print_comment($answer->feedback); - } - echo " '; + quiz_print_comment($answer->feedback); + echo '
"; - echo "
"; + echo '
'; + echo '
'; } - function grade_response($question, $nameprefix, $addedanswercondition='') { + function grade_responses(&$question, &$state, $quiz) { + $answers = $question->options->answers; + $testedstate = clone($state); + $teststate = clone($state); - $result->correctanswers = array(); - $result->answers = array(); - $result->grade = 0.0; - - $answers = $this->get_answers($question, $addedanswercondition); - - /// Set ->answers[] and ->grade - if (!empty($question->response)) { - foreach ($question->response as $name => $response) { - if (isset($answers[$response])) { - $result->answers[$name] = $answers[$response]; - $result->grade += $answers[$response]->fraction; + foreach($answers as $answer) { + $teststate->responses[''] = $answer->id; + if($question->options->single) { + if($this->compare_responses($question, $testedstate, + $teststate)) { + $state->raw_grade = $answer->fraction; + break; } - } - } - - /// Set ->correctanswers[] - foreach ($answers as $answer) { - - if ($answer->single) { - $result->correctanswers = - quiz_extract_correctanswers($answers, $nameprefix); - break; - } else { - if ($answer->fraction > 0.0) { - $result->correctanswers[$nameprefix.$answer->id] = $answer; + foreach($state->responses as $response) { + $testedstate->responses = array('' => $response); + if($this->compare_responses($question, $testedstate, + $teststate)) { + $state->raw_grade += $answer->fraction; + break; + } } } } - return $result; + if (empty($state->raw_grade)) { + $state->raw_grade = 0; + } + + // Make sure we don't assign negative or too high marks + $state->raw_grade = min(max((float) $state->raw_grade, + 0.0), 1.0) * $question->maxgrade; + // Apply the penalty for this attempt + $state->penalty = 0; + // Only allow one attempt at the question + $state->event = QUIZ_EVENTCLOSE; + return true; + } + + function print_question_grading_details(&$question, &$state, $quiz, $options) { + // do nothing } } //// END OF CLASS //// diff --git a/mod/quiz/questiontypes/numerical/editquestion.php b/mod/quiz/questiontypes/numerical/editquestion.php index 39df1bbb481..a74b8dea036 100644 --- a/mod/quiz/questiontypes/numerical/editquestion.php +++ b/mod/quiz/questiontypes/numerical/editquestion.php @@ -1,83 +1,24 @@ id)) { - $answersraw= quiz_get_answers($question); - } - $answers= array(); - for ($i=0; $i<6; $i++) { - $answers[$i]->answer = ""; // Make answer slots, default as blank... - $answers[$i]->min = ""; - $answers[$i]->max = ""; - $answers[$i]->feedback = ""; - } - if (!empty($answersraw)) { - $i=0; - foreach ($answersraw as $answer) { - $answers[$i] = $answer; - $i++; - } - } - $units = array(); - for ($i=0 ; $i<6 ; $i++) { - $units[$i]->multiplier = ''; // Make unit slots, default as blank... - $units[$i]->unit = ''; - } - if (!empty($question->id) and $unitsraw = get_records( - 'quiz_numerical_units', 'question', $question->id)) { - /// Find default unit and have it put in the zero slot - /// This procedure might be overridden later when - /// the unit is stripped form an answer... - foreach ($unitsraw as $key => $unit) { - if (1.0 == $unit->multiplier) { - /// Default unit found: - $units[0] = $unit; - unset($unitsraw[$key]); - break; - } - } - /// Fill remaining answer slots with whatsever left - if (!empty($unitsraw)) { - $i = 1; // The zero slot got the default unit... - foreach ($unitsraw as $unit) { - $units[$i] = $unit; - $i++; - } - } + // Get all the extra information if we're editing + if (!empty($question->id) && isset($question->qtype) && + $QUIZ_QTYPES[$question->qtype]->get_question_options($question)) { + $answer = array_values($question->options->answers); + usort($answer, create_function('$a, $b', + 'if ($a->fraction == $a->fraction) { return 0; }' . + 'else { return $a->fraction < $b->fraction ? -1 : 1; }')); + $answer = $answer[0]; // Get the answer with the highest fraction (i.e. 1) + $units = array_values($question->options->units); + usort($units, create_function('$a, $b', // make sure the default unit is at index 0 + 'if (1.0 === (float)$a->multiplier) { return -1; } else '. + 'if (1.0 === (float)$b->multiplier) { return 1; } else { return 0; }')); + $tolerance = $question->options->tolerance; } else { - $units[0]->multiplier = 1.0; - } - - // Strip unit from answers, if they have any: - foreach ($units as $key => $unit) { - if (1.0 == $unit->multiplier && $unit->unit) { - $ukey = $key; - // Possible default unit found: - foreach ($answers as $i => $answer) { - if (($answer->min || $answer->max) && ereg( - "^(.*)$unit->unit$", $answer->answer, $numreg)) { - $answers[$i]->answer = $numreg[1]; - if (0!=$ukey) { - // Make unit default by putting it first: - $units[$ukey] = $units[0]; - $units[0] = $unit; - $ukey = 0; - } - } - } - } - } - - // Strip trailing zeros from multipliers - foreach ($units as $i => $unit) { - if (ereg('^(.*\\..(.*[^0])?)0+$', $unit->multiplier, $regs1)) { - if (ereg('^(.+)\\.0$', $regs1[1], $regs2)) { - $units[$i]->multiplier = $regs2[1]; - } else { - $units[$i]->multiplier = $regs1[1]; - } - } + $answer = new stdClass; + $answer->answer = ''; + $answer->feedback = ''; + $units = array(); + $tolerance = ''; } print_heading_with_help(get_string("editingnumerical", "quiz"), "numerical", "quiz"); diff --git a/mod/quiz/questiontypes/numerical/numerical.html b/mod/quiz/questiontypes/numerical/numerical.html index 57b3cae213c..a4a4fb522f4 100644 --- a/mod/quiz/questiontypes/numerical/numerical.html +++ b/mod/quiz/questiontypes/numerical/numerical.html @@ -31,7 +31,7 @@ "; } @@ -67,44 +67,36 @@ : - min) && is_numeric($answers[0]->answer)) { - $acceptederror = (float)($answers[0]->answer) - - (float)($answers[0]->min); - } else { - $acceptederror = ""; - } - ?> - " />   + " /> : - " />± - - - + " />± -
: - + : - - - " /> - () + multiplier) { + $unit = $units[0]->unit; + } else { + $unit = ''; + } + $multiplier = '1.0'; + ?> + + " /> + () @@ -113,66 +105,31 @@ multiplier; + $unit = $units[$i]->unit; + } else { + $multiplier = ''; + $unit = ''; + } ?> : - " size="10" - align="right" name="multiplier[]" - value="multiplier) ?>" - alt="" /> -    : - " - name="unit[]" - size="5" value="unit) ?>" /> + " /> +    : + qtype]->print_replacement_options($question, $course, $contextquiz); - $QUIZ_QTYPES[$question->qtype]->print_question_form_end($question, 'onclick="return determineMinAndMax();"'); + $QUIZ_QTYPES[$question->qtype]->print_question_form_end($question); ?> - - diff --git a/mod/quiz/questiontypes/numerical/questiontype.php b/mod/quiz/questiontypes/numerical/questiontype.php index b6e519da1c2..dc7ecf1fcc1 100644 --- a/mod/quiz/questiontypes/numerical/questiontype.php +++ b/mod/quiz/questiontypes/numerical/questiontype.php @@ -18,223 +18,278 @@ require_once("$CFG->dirroot/mod/quiz/questiontypes/shortanswer/questiontype.php" class quiz_numerical_qtype extends quiz_shortanswer_qtype { - function get_answers($question, $addedcondition='') { - // The added condition is one addition that has been added - // to the behaviour of this question type in order to make - // it embeddable within a multianswer (embedded cloze) question - - global $CFG; - - // There can be multiple answers - return get_records_sql("SELECT a.*, n.min, n.max - FROM {$CFG->prefix}quiz_numerical n, - {$CFG->prefix}quiz_answers a - WHERE a.question = '$question->id' - AND n.answer = a.id " - . $addedcondition); - } - function name() { return 'numerical'; } - function save_question_options($question) { + function get_question_options(&$question) { + if (!$question->options->answers = get_records('quiz_answers', + 'question', $question->id)) { + notify('Error: Missing question answer!'); + return false; + } - if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { + if (false === ($question->options->tolerance = get_field('quiz_numerical', + 'tolerance', 'question', $question->id))) { + $question->options->tolerance = 0; + } + $this->get_numerical_units($question); + return true; + } + + function get_numerical_units(&$question) { + if ($question->options->units = get_records('quiz_numerical_units', + 'question', $question->id, 'id ASC')) { + $question->options->units = array_values($question->options->units); + usort($question->options->units, create_function('$a, $b', // make sure the default unit is at index 0 + 'if (1.0 === (float)$a->multiplier) { return -1; } else '. + 'if (1.0 === (float)$b->multiplier) { return 1; } else { return 0; }')); + array_walk($question->options->units, create_function('$val', + '$val->multiplier = (float)$val->multiplier;')); + } else { + $question->options->units = array(); + } + return true; + } + + function get_default_numerical_unit(&$question) { + if (!isset($question->options->units[0])) { + return ''; + } else if (1.0 === (float)$question->options->units[0]->multiplier) { + return $question->options->units[0]; + } else { + foreach ($question->options->units as $unit) { + if (1.0 === (float)$unit->multiplier) { + return $unit; + } + } + } + } + + function save_question_options($question) { + // save_question_options supports the definition of multiple answers + // for numerical questions. This is not currently used by the editing + // interface, but the GIFT format supports it. The multianswer qtype, + // for example can make use of this feature. + + // Get old versions of the objects + if (!$oldanswers = get_records("quiz_answers", "question", $question->id)) { $oldanswers = array(); } - $answers = array(); - $maxfraction = -1; + if (!$oldoptions = get_record("quiz_numerical", "question", $question->id)) { + $oldoptions = new stdClass; + } + $result = $this->save_numerical_units($question); + if (isset($result->error)) { + return $result; + } else { + $units = &$result->units; + } + $answerids = array(); // Insert all the new answers foreach ($question->answer as $key => $dataanswer) { if ($dataanswer != "") { + $answer = new stdClass; + $answer->question = $question->id; + $answer->answer = trim($dataanswer); + $answer->fraction = $question->fraction[$key]; + $answer->feedback = trim($question->feedback[$key]); + if (!is_numeric($answer->answer)) { + $result->error = "The answer '$answer->answer' is not numeric!"; + return $result; + } + if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it - $answer = $oldanswer; - $answer->answer = $dataanswer; - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!update_record("quiz_answers", $answer)) { + $answer->id = $oldanswer->id; + if (! update_record("quiz_answers", $answer)) { $result->error = "Could not update quiz answer! (id=$answer->id)"; return $result; } } else { // This is a completely new answer - unset($answer); - $answer->answer = $dataanswer; - $answer->question = $question->id; - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!$answer->id = insert_record("quiz_answers", $answer)) { + if (! $answer->id = insert_record("quiz_answers", $answer)) { $result->error = "Could not insert quiz answer!"; return $result; } } - $answers[] = $answer->id; - if ($question->fraction[$key] > $maxfraction) { - $maxfraction = $question->fraction[$key]; - } - - if ($options = get_record("quiz_numerical", "answer", $answer->id)) { - $options->min= $question->min[$key]; - $options->max= $question->max[$key]; - if (!update_record("quiz_numerical", $options)) { - $result->error = "Could not update quiz numerical options! (id=$options->id)"; - return $result; - } - } else { // completely new answer - unset($options); - $options->question = $question->id; - $options->answer = $answer->id; - $options->min = $question->min[$key]; - $options->max = $question->max[$key]; - if (!insert_record("quiz_numerical", $options)) { - $result->error = "Could not insert quiz numerical options!"; - return $result; - } - } + $answerids[] = $answer->id; } } - /// Save units - /// Make unit records - $newunits = array(); - unset($tmpunit); - $tmpunit->question = $question->id; - foreach ($question->multiplier as $key => $multiplier) { - if ($multiplier && is_numeric($multiplier)) { - $tmpunit->multiplier = $multiplier; - $tmpunit->unit = trim($question->unit[$key]); - $newunits[] = $tmpunit; + // Set up the options object + $options = clone($oldoptions); + $options->question = $question->id; + $options->answers = implode(',', $answerids); + $options->tolerance = $this->apply_unit($question->tolerance, $units); + + // Save options + if (isset($oldoptions->id)) { // options have changed + $options->id = $oldoptions->id; + if (! update_record('quiz_numerical', $options)) { + $result->error = "Could not update quiz numerical options! (id=$options->id)"; + return $result; } - } - if (1 == count($newunits) && !$newunits[0]->unit) { - /// Only default unit and it is empty, so drop it: - $newunits = array(); - } - if ($oldunits = get_records('quiz_numerical_units', - 'question', $question->id)) { - foreach ($oldunits as $unit) { - if ($newunit = array_shift($newunits)) { - $unit->multiplier = $newunit->multiplier; - $unit->unit = $newunit->unit; - if (!update_record('quiz_numerical_units', $unit)) { - $result->error = "Could not update quiz_numerical_unit $unit->unit"; - return $result; - } - } else { - delete_records('quiz_numerical_units', 'id', $unit->id); - } - } - } - foreach ($newunits as $newunit) { - // Create new records for the remaining units: - if (!insert_record('quiz_numerical_units', $newunit)) { - $result->error = "Unable to insert new unit $newunit->unit"; + } else { // new options + if (! insert_record('quiz_numerical', $options)) { + $result->error = "Could not insert quiz numerical options!"; return $result; } } - - /// Perform sanity checks on fractional grades - if ($maxfraction != 1) { - $maxfraction = $maxfraction * 100; - $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction); - return $result; - } else { - return true; - } } - - function grade_response($question, $nameprefix, $addedanswercondition='') { - $result->answers = array(); - $units = get_records('quiz_numerical_units', - 'question', $question->id); - if (isset($question->response[$nameprefix])) { - $response = trim(stripslashes($question->response[$nameprefix])); - // Arrays with 'wild cards': - $search = array(' ',','); - $replace = array('','.'); - $responsenum = str_replace($search, $replace, $response); - if (empty($units)) { - if ('' !== $responsenum && is_numeric($responsenum)) { - $responsenum = (float)$responsenum; - } else { - unset($responsenum); // Answer is not numeric - } - } else if (ereg( - '^([+-]?([0-9]+(\\.[0-9]*)?|[.][0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$', - $responsenum, $responseparts)) { - $responsenum = (float)$responseparts[1]; - if ($responseparts[5]) { - $responseunit = $responseparts[5]; - } else { - $responseunit = ''; - } - $unitidentified = false; - foreach ($units as $unit) { - if (str_replace($search, $replace, $unit->unit) - == $responseunit) { - $unitidentified = true; - $responsenum /= $unit->multiplier; - break; + function save_numerical_units($question) { + if (!$oldunits = get_records("quiz_numerical_units", "question", $question->id)) { + $oldunits = array(); + } + + // Set the units + $units = array(); + $keys = array(); + $oldunits = array_values($oldunits); + usort($oldunits, create_function('$a, $b', // make sure the default unit is at index 0 + 'if (1.0 === (float)$a->multiplier) { return -1; } else '. + 'if (1.0 === (float)$b->multiplier) { return 1; } else { return 0; }')); + foreach ($oldunits as $unit) { + $units[] = clone($unit); + } + $n = isset($question->multiplier) ? count($question->multiplier) : 0; + for ($i = 0; $i < $n; $i++) { + // Discard any unit which doesn't specify the unit or the multiplier + if (!empty($question->multiplier[$i]) && !empty($question->unit[$i])) { + $units[$i]->question = $question->id; + $units[$i]->multiplier = + $this->apply_unit($question->multiplier[$i], array()); + $units[$i]->unit = $question->unit[$i]; + } else { + unset($units[$i]); + } + } + unset($question->multiplier, $question->unit); + + /// Save units + for ($i = 0; $i < $n; $i++) { + if (!isset($units[$i]) && isset($oldunits[$i])) { // Delete if it hasn't been resubmitted + delete_records('quiz_numerical_units', 'id', $oldunits[$i]->id); + } else if ($oldunits != $units) { // answer has changed or is new + if (isset($oldunits[$i]->id)) { // answer has changed + $units[$i]->id = $oldunits[$i]->id; + if (! update_record('quiz_numerical_units', $units[$i])) { + $result->error = "Could not update quiz_numerical_unit $units[$i]->unit"; + return $result; + } + } else if (isset($units[$i])) { // answer is new + if (! insert_record('quiz_numerical_units', $units[$i])) { + $result->error = "Unable to insert new unit $units[$i]->unit"; + return $result; } } - if (!$unitidentified) { - unset($responsenum); // No unit OK - } - } else { - unset($responsenum); // Answer is not numeric } - } else { + } + $result->units = &$units; + return $result; + } + + function compare_responses(&$question, &$state, &$teststate) { + if (isset($state->responses[''])) { + $response = $this->apply_unit(stripslashes($state->responses['']), + $question->options->units); + } else { $response = ''; } - $answers = $this->get_answers($question, $addedanswercondition); - foreach ($answers as $answer) { - - /// Check if response matches answer... - if ('' !== $response and empty($result->answers) - || $answer->fraction - > $result->answers[$nameprefix]->fraction - and strtolower($response) == strtolower($answer->answer) - || '' != trim($answer->min) && isset($responsenum) - && $responsenum >= (float)$answer->min - && $responsenum <= (float)$answer->max) { - $result->answers[$nameprefix] = $answer; - } + if (isset($teststate->responses[''])) { + $testresponse = + $this->apply_unit(stripslashes($teststate->responses['']), + $question->options->units); + } else { + $testresponse = ''; } - $result->grade = isset($result->answers[$nameprefix]) - ? $result->answers[$nameprefix]->fraction - : 0.0; - $result->correctanswers = quiz_extract_correctanswers($answers, - $nameprefix); + $this->get_tolerance_interval($question, $teststate); + // We need to add a tiny fraction (0.00000000000000001) to make the + // comparison work correctly. Otherwise seemingly equal values can yield + // false. + $tolerance = (float)$question->options->tolerance + 0.00000000000000001; - ///////////////////////////////////////////////// - // For numerical answer we have the policy to - // set feedback for any response, even if the - // response does not entitle the student to it. - ///////////////////////////////////////////////// - if ('' !== $response and empty($result->answers) - || empty($result->answers[$nameprefix]->feedback)) { - // Look for just any feedback: - foreach ($result->correctanswers as $correctanswer) { - if ($correctanswer->feedback) { - $result->answers[$nameprefix]->feedback = - $correctanswer->feedback; - if (empty($result->answers[$nameprefix]->id)) { - // Better fake an answer as well: - $result->answers[$nameprefix]->id = 0; - $result->answers[$nameprefix]->answer = $response; - $result->answers[$nameprefix]->fraction = 0.0; - $result->answers[$nameprefix]->question = $question->id; - } - break; - } + if (is_numeric($response) && is_numeric($testresponse)) { + if ($teststate->options->min <= $response + && $teststate->options->max >= $response) { + return true; } } + return false; + } - return $result; + function get_tolerance_interval(&$question, &$state) { + $answer = (float)$state->responses['']; + if (empty($question->options->tolerance)) { + $state->options->min = $state->options->max = $answer; + return true; + } + + if (!isset($question->options->tolerancetype)) { + $question->options->tolerancetype = 2; // nominal + } + $tolerance = (float)$question->options->tolerance; + switch ($question->options->tolerancetype) { + case '1': case 'relative': + /// Recalculate the tolerance and fall through + /// to the nominal case: + $tolerance = $answer * $tolerance; + // Falls through to the nominal case - + case '2': case 'nominal': + $tolerance = abs($tolerance); // important - otherwise min and max are swapped + $max = $answer + $tolerance; + $min = $answer - $tolerance; + break; + case '3': case 'geometric': + $quotient = 1 + abs($tolerance); + $max = $answer * $quotient; + $min = $answer / $quotient; + break; + default: + error("Unknown tolerance type $question->options->tolerancetype"); + } + + $state->options->min = $min; + $state->options->max = $max; + return true; + } + + /** + * Checks if the $rawresponse has a unit and applys it if appropriate. + * + * @param string $rawresponse The response string to be converted to a float. + * @param array $units An array with the defined units, where the + * unit is the key and the multiplier the value. + * @return float The rawresponse with the unit taken into + * account as a float. + */ + function apply_unit($rawresponse, $units) { + if (!is_numeric($rawresponse)) { + return $rawresponse; + } + + // Make units more useful + $tmpunits = array(); + foreach ($units as $unit) { + $tmpunits[$unit->unit] = $unit->multiplier; + } + + $search = array(' ', ','); + $replace = array('', '.'); + $rawresponse = str_replace($search, $replace, $rawresponse); // remove spaces + if (ereg( + '^([+-]?([0-9]+(\\.[0-9]*)?|[.][0-9]+)([eE][-+]?[0-9]+)?)([^0-9].*)?$', + $rawresponse, $responseparts)) { + $responsenum = (float)$responseparts[1]; + if (isset($tmpunits[$responseparts[5]])) { + return (float)$responseparts[1]*$tmpunits[$responseparts[5]]; + } else { + return (float)$responseparts[1]; + } + } + return (float)$rawresponse; } } //// END OF CLASS //// diff --git a/mod/quiz/questiontypes/random/questiontype.php b/mod/quiz/questiontypes/random/questiontype.php index a269a0d09b8..df68859248b 100644 --- a/mod/quiz/questiontypes/random/questiontype.php +++ b/mod/quiz/questiontypes/random/questiontype.php @@ -23,31 +23,215 @@ class quiz_random_qtype extends quiz_default_questiontype { return 'random'; } - function save_question_options($question) { - /// No options to be saved for this question type: + function get_question_options(&$question) { + // Don't do anything here, because the random question has no options. + // Everything is handled by the create- or restore_session_and_responses + // functions. return true; } - function wrapped_questions($question) { - global $QUIZ_QTYPES; - - foreach ($question->response as $key => $response) { - if (ereg('[^0-9][0-9]+random$', $key)) { - $randomquestion = get_record('quiz_questions', - 'id', $response); - $randomquestion->response = $question->response; - unset($randomquestion->response[$key]); - if ($subwrapped = $QUIZ_QTYPES[$randomquestion->qtype] - ->wrapped_questions($randomquestion)) { - return "$response,$subwrapped"; - } else { - return $response; - } + function save_question_options($question) { + // No options, but we use the parent field to hide random questions. + // To avoid problems we set the parent field to the question id. + return (set_field('quiz_questions', 'parent', $question->id, 'id', + $question->id) ? true : false); + } + + function create_session_and_responses(&$question, &$state, $quiz, $attempt) { + // Choose a random question from the category: + // We need to make sure that no question is used more than once in the + // quiz. Therfore the following need to be excluded: + // 1. All questions that are explicitly assigned to the quiz + // 2. All random questions + // 3. All questions that are already chosen by an other random question + if (!isset($quiz->questionsinuse)) { + $quiz->questionsinuse = $quiz->questions; + } + + if (!isset($this->catrandoms[$question->category])) { + // Need to fetch random questions from category $question->category" + // (Note: $this refers to the questiontype, not the question.) + global $CFG; + $possiblerandomqtypes = "'" + . implode("','", $this->possiblerandomqtypes) . "'"; + if ($question->questiontext == "1") { + // recurse into subcategories + $categorylist = quiz_categorylist($question->category); + } else { + $categorylist = $question->category; + } + $this->catrandoms[$question->category] = get_records_sql + ("SELECT * FROM {$CFG->prefix}quiz_questions + WHERE category IN ($categorylist) + AND parent = '0' + AND id NOT IN ($quiz->questionsinuse) + AND qtype IN ($possiblerandomqtypes)"); + $this->catrandoms[$question->category] = + draw_rand_array($this->catrandoms[$question->category], + count($this->catrandoms[$question->category])); // from bug 1889 + } + + while ($wrappedquestion = + array_pop($this->catrandoms[$question->category])) { + if (!ereg("(^|,)$wrappedquestion->id(,|$)", $quiz->questionsinuse)) { + /// $randomquestion is not in use and will therefore be used + /// as the randomquestion here... + + global $QUIZ_QTYPES; + $QUIZ_QTYPES[$wrappedquestion->qtype] + ->get_question_options($wrappedquestion); + $QUIZ_QTYPES[$wrappedquestion->qtype] + ->create_session_and_responses($wrappedquestion, $state, $quiz, + $attempt); + $wrappedquestion->name_prefix = $question->name_prefix; + $wrappedquestion->maxgrade = $question->maxgrade; + $quiz->questionsinuse .= ",$wrappedquestion->id"; + + $state->options->question = &$wrappedquestion; + return true; } } + notify(get_string('toomanyrandom', 'quiz', $question->category)); return false; } + function restore_session_and_responses(&$question, &$state) { + global $QUIZ_QTYPES; + if (!ereg('^random([0-9]+)-(.*)$', $state->responses[''], $answerregs)) { + notify("The answer value '{$state->responses['']}' for the state with " + ."id=$state->id to the random question " + ."$question->id is malformated." + ." - No response can be extracted!"); + return false; + } + $state->responses[''] = $answerregs[2]; + + if (!$wrappedquestion = get_record('quiz_questions', 'id', $answerregs[1])) { + return false; + } + + if (!$QUIZ_QTYPES[$wrappedquestion->qtype] + ->get_question_options($wrappedquestion)) { + return false; + } + + if (!$QUIZ_QTYPES[$wrappedquestion->qtype] + ->restore_session_and_responses($wrappedquestion, $state)) { + return false; + } + $wrappedquestion->name_prefix = $question->name_prefix; + $wrappedquestion->maxgrade = $question->maxgrade; + + $state->options->question = &$wrappedquestion; + return true; + } + + function save_session_and_responses(&$question, &$state) { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + + // Trick the wrapped question into pretending to be the random one. + $realqid = $wrappedquestion->id; + $wrappedquestion->id = $question->id; + $QUIZ_QTYPES[$wrappedquestion->qtype] + ->save_session_and_responses($wrappedquestion, $state); + + // Read what the wrapped question has just set the answer field to + // (if anything) + $response = get_field('quiz_states', 'answer', 'id', $state->id); + if(false === $response) { + return false; + } + + // Prefix the answer field... + $response = "random$realqid-$response"; + + // ... and save it again. + if (!set_field('quiz_states', 'answer', $response, 'id', $state->id)) { + return false; + } + + // Restore the real id + $wrappedquestion->id = $realqid; + return true; + } + + function get_correct_responses(&$question, &$state) { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + return $QUIZ_QTYPES[$wrappedquestion->qtype] + ->get_correct_responses($wrappedquestion, $state); + } + + function print_question(&$question, &$state, &$number, $quiz, $options) { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + $QUIZ_QTYPES[$wrappedquestion->qtype] + ->print_question($wrappedquestion, $state, $number, $quiz, $options); + } +/* + function print_question_grading_details(&$question, &$state, $quiz, + $options) { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + $QUIZ_QTYPES[$wrappedquestion->qtype] + ->print_question_grading_details($wrappedquestion, $state, $quiz, + $options); + } + + function print_question_formulation_and_controls(&$question, &$state, $quiz, + $options) { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + $QUIZ_QTYPES[$wrappedquestion->qtype] + ->print_question_formulation_and_controls($wrappedquestion, $state, + $quiz, $options); + } + + function print_question_submit_buttons(&$question, &$state, $quiz, + $options) { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + $QUIZ_QTYPES[$wrappedquestion->qtype] + ->print_question_submit_buttons($wrappedquestion, $state, $quiz, + $options); + } +*/ + function grade_responses(&$question, &$state, $quiz) { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + return $QUIZ_QTYPES[$wrappedquestion->qtype] + ->grade_responses($wrappedquestion, $state, $quiz); + } + + function get_texsource(&$question, &$state, $quiz, $type) { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + return $QUIZ_QTYPES[$wrappedquestion->qtype] + ->get_texsource($wrappedquestion, $state, $quiz, $type); + } + + function compare_responses(&$question, $state, $teststate) { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + return $QUIZ_QTYPES[$wrappedquestion->qtype] + ->compare_responses($wrappedquestion, $state, $teststate); + } + + function print_replacement_options($question, $course, $quizid='0') { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + return $QUIZ_QTYPES[$wrappedquestion->qtype] + ->print_replacement_options($wrappedquestion, $state, $quizid); + } + + function print_question_form_end($question, $submitscript='') { + global $QUIZ_QTYPES; + $wrappedquestion = &$state->options->question; + return $QUIZ_QTYPES[$wrappedquestion->qtype] + ->print_question_form_end($wrappedquestion, $state, $quizid); + } +/* function convert_to_response_answer_field($questionresponse) { global $QUIZ_QTYPES; @@ -64,52 +248,69 @@ class quiz_random_qtype extends quiz_default_questiontype { return ''; } + + function create_response($question, $nameprefix, $questionsinuse) { // It's for question types like RANDOMSAMATCH and RANDOM that // the true power of the pattern with this function comes to the surface. - global $CFG; - if (!isset($this->catrandoms[$question->category])) { - //Need to fetch random questions from category $question->category" - - $possiblerandomqtypes = "'" - . implode("','", $this->possiblerandomqtypes) . "'"; - if ($question->questiontext == "1") { - // recurse into subcategories - $categorylist = quiz_categorylist($question->category); - } else { - $categorylist = $question->category; - } - $this->catrandoms[$question->category] = get_records_sql - ("SELECT * FROM {$CFG->prefix}quiz_questions - WHERE category IN ($categorylist) - AND id NOT IN ($questionsinuse) - AND qtype IN ($possiblerandomqtypes)"); - $this->catrandoms[$question->category] = - draw_rand_array($this->catrandoms[$question->category], - count($this->catrandoms[$question->category])); // from bug 1889 - } - - while ($randomquestion = - array_pop($this->catrandoms[$question->category])) { - if (!ereg("(^|,)$randomquestion->id(,|$)", $questionsinuse)) { - /// $randomquestion is not in use and will therefore be used - /// as the randomquestion here... - - global $QUIZ_QTYPES; - $response = $QUIZ_QTYPES[$randomquestion->qtype] - ->create_response($randomquestion, - quiz_qtype_nameprefix($randomquestion, $nameprefix), - "$questionsinuse,$randomquestion->id"); - $response[$nameprefix] = $randomquestion->id; - return $response; - } - } - notify(get_string('toomanyrandom', 'quiz', $question->category)); - return array(); } + +*/ + /* + function print_question_formulation_and_controls($question, + $quiz, $readonly, $answers, $correctanswers, $nameprefix) { + global $QUIZ_QTYPES; + + // Get the wrapped question... + if ($actualquestion = $this->get_wrapped_question($question, + $nameprefix)) { + echo ''; + return $QUIZ_QTYPES[$actualquestion->qtype] + ->print_question_formulation_and_controls($actualquestion, + $quiz, $readonly, $answers, $correctanswers, + quiz_qtype_nameprefix($actualquestion, $nameprefix)); + } else { + echo '

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

'; + } + } + + + function get_wrapped_question($question, $nameprefix) { + if (!empty($question->response[$nameprefix]) + and $actualquestion = get_record('quiz_questions', + 'id', $question->response[$nameprefix])) { + $actualquestion->response = $question->response; + unset($actualquestion->response[$nameprefix]); + $actualquestion->maxgrade = $question->maxgrade; + return $actualquestion; + } else { + return false; + } + } + + function grade_response($question, $nameprefix) { + global $QUIZ_QTYPES; + + // Get the wrapped question... + if ($actualquestion = $this->get_wrapped_question($question, + $nameprefix)) { + return $QUIZ_QTYPES[$actualquestion->qtype]->grade_response( + $actualquestion, + quiz_qtype_nameprefix($actualquestion, $nameprefix)); + } else { + $result->grade = 0.0; + $result->answers = array(); + $result->correctanswers = array(); + return $result; + } + } + +*/ + function extract_response($rawresponse, $nameprefix) { global $QUIZ_QTYPES; @@ -137,8 +338,8 @@ class quiz_random_qtype extends quiz_default_questiontype { /// function is now able to handle both types of response record. - /*** Pick random question id from the answer field in a way that ***/ - /*** works for both formats: ***/ + // Pick random question id from the answer field in a way that + /// works for both formats: if (!ereg('^(random)?([0-9]+)(-(.*))?$', $rawresponse->answer, $answerregs)) { error("The answer value '$rawresponse->answer' for the response with " ."id=$rawresponse->id to the random question " @@ -151,8 +352,8 @@ class quiz_random_qtype extends quiz_default_questiontype { 'id', $randomquestionid)) { if ($answerregs[1] && $answerregs[3]) { - /**** The raw response is formatted according to - /**** Moodle version 1.5 or later ****/ + // The raw response is formatted according to + // Moodle version 1.5 or later $randomresponse = $rawresponse; $randomresponse->question = $randomquestionid; $randomresponse->answer = $answerregs[4]; @@ -160,22 +361,19 @@ class quiz_random_qtype extends quiz_default_questiontype { } else if ($randomresponse = get_record ('quiz_responses', 'question', $rawresponse->answer, 'attempt', $rawresponse->attempt)) { - /*** The response was stored by an older version of Moodle */ - /*** :-) ***/ - + // The response was stored by an older version of Moodle + // :-) + } else { notify("Error: Cannot find response to random question $randomquestionid"); unset($randomresponse); } - + if (isset($randomresponse)) { /// The prefered case: - /// There is a random question and a response field, from + /// There is a random question and a response field, from /// which the response array can be extracted: - $response = $QUIZ_QTYPES[$randomquestion->qtype] - ->extract_response($randomresponse, - quiz_qtype_nameprefix($randomquestion, $nameprefix)); } else { @@ -191,7 +389,8 @@ class quiz_random_qtype extends quiz_default_questiontype { // good chances to execute properly anyway.) } $response[$nameprefix] = $randomquestionid; - return $response; + //return $response; + return ''; } else { notify("Error: Unable to find random question $rawresponse->question"); /// No new random question is picked as this is probably @@ -199,54 +398,6 @@ class quiz_random_qtype extends quiz_default_questiontype { return array(); } } - - function print_question_formulation_and_controls($question, - $quiz, $readonly, $answers, $correctanswers, $nameprefix) { - global $QUIZ_QTYPES; - - // Get the wrapped question... - if ($actualquestion = $this->get_wrapped_question($question, - $nameprefix)) { - echo ''; - return $QUIZ_QTYPES[$actualquestion->qtype] - ->print_question_formulation_and_controls($actualquestion, - $quiz, $readonly, $answers, $correctanswers, - quiz_qtype_nameprefix($actualquestion, $nameprefix)); - } else { - echo '

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

'; - } - } - - function get_wrapped_question($question, $nameprefix) { - if (!empty($question->response[$nameprefix]) - and $actualquestion = get_record('quiz_questions', - 'id', $question->response[$nameprefix])) { - $actualquestion->response = $question->response; - unset($actualquestion->response[$nameprefix]); - $actualquestion->maxgrade = $question->maxgrade; - return $actualquestion; - } else { - return false; - } - } - - function grade_response($question, $nameprefix) { - global $QUIZ_QTYPES; - - // Get the wrapped question... - if ($actualquestion = $this->get_wrapped_question($question, - $nameprefix)) { - return $QUIZ_QTYPES[$actualquestion->qtype]->grade_response( - $actualquestion, - quiz_qtype_nameprefix($actualquestion, $nameprefix)); - } else { - $result->grade = 0.0; - $result->answers = array(); - $result->correctanswers = array(); - return $result; - } - } } //// END OF CLASS //// diff --git a/mod/quiz/questiontypes/randomsamatch/editquestion.php b/mod/quiz/questiontypes/randomsamatch/editquestion.php index 8589f8d5e88..10c71c2ba2e 100644 --- a/mod/quiz/questiontypes/randomsamatch/editquestion.php +++ b/mod/quiz/questiontypes/randomsamatch/editquestion.php @@ -4,7 +4,9 @@ } else { $options->choose = ""; } - $numberavailable = count_records("quiz_questions", "category", $category->id, "qtype", SHORTANSWER); + $saquestions = $QUIZ_QTYPES[RANDOMSAMATCH]->get_sa_candidates($category->id); + $numberavailable = count($saquestions); + unset($saquestions); print_heading_with_help(get_string("editingrandomsamatch", "quiz"), "randomsamatch", "quiz"); require("$CFG->dirroot/mod/quiz/questiontypes/randomsamatch/randomsamatch.html"); diff --git a/mod/quiz/questiontypes/randomsamatch/questiontype.php b/mod/quiz/questiontypes/randomsamatch/questiontype.php index 06dddeb45bb..af12c09f850 100644 --- a/mod/quiz/questiontypes/randomsamatch/questiontype.php +++ b/mod/quiz/questiontypes/randomsamatch/questiontype.php @@ -13,18 +13,34 @@ class quiz_randomsamatch_qtype extends quiz_match_qtype { /// Extends MATCH as there are quite a few simularities... - // $catrandoms carries question ids for shortanswer questions - // available as random questios. - // They are sorted by category. - var $catrandoms = array(); - function name() { return 'randomsamatch'; } + function get_question_options(&$question) { + if (!$question->options->choose = get_field('quiz_randomsamatch', + 'choose', 'question', $question->id)) { + notify('Error: Missing question options!'); + return false; + } + + // This could be included as a flag in the database. It's already + // supported by the code. + // Recurse subcategories: 0 = no recursion, 1 = recursion + $question->options->subcats = 1; + return true; + + } + function save_question_options($question) { $options->question = $question->id; $options->choose = $question->choose; + + if (2 > $question->choose) { + $result->error = "At least two shortanswer questions need to be chosen!"; + return $result; + } + if ($existing = get_record("quiz_randomsamatch", "question", $options->question)) { $options->id = $existing->id; @@ -40,66 +56,117 @@ class quiz_randomsamatch_qtype extends quiz_match_qtype { } return true; } - - function wrapped_questions($question) { - if (empty($question->response)) { - return false; - } else { - $wrapped = ''; - $delimiter = ''; - foreach ($question->response as $rkey => $response) { - $wrapped .= $delimiter.$this->extract_response_id($rkey); - $delimiter = ','; - } - return $wrapped; + + function create_session_and_responses(&$question, &$state, $quiz, $attempt) { + // Choose a random shortanswer question from the category: + // We need to make sure that no question is used more than once in the + // quiz. Therfore the following need to be excluded: + // 1. All questions that are explicitly assigned to the quiz + // 2. All random questions + // 3. All questions that are already chosen by an other random question + global $QUIZ_QTYPES; + if (!isset($quiz->questionsinuse)) { + $quiz->questionsinuse = $quiz->questions; } + + if ($question->options->subcats) { + // recurse into subcategories + $categorylist = quiz_categorylist($question->category); + } else { + $categorylist = $question->category; + } + + $saquestions = $this->get_sa_candidates($categorylist, $quiz->questionsinuse); + + $count = count($saquestions); + $wanted = $question->options->choose; + if ($count < $wanted && isteacherinanycourse()) { + notify("Error: could not get enough Short-Answer questions! + Got $count Short-Answer questions, but wanted $wanted. + Reducing number to choose from to $count!"); + $question->options->choose = $count; + } + + $saquestions = + draw_rand_array($saquestions, $question->options->choose); // from bug 1889 + + foreach ($saquestions as $key => $wrappedquestion) { + if (!$QUIZ_QTYPES[$wrappedquestion->qtype] + ->get_question_options($wrappedquestion)) { + return false; + } + + // Now we overwrite the $question->options->answers field to only + // *one* (the first) correct answer. This loop can be deleted to + // take all answers into account (i.e. put them all into the + // drop-down menu. + $foundcorrect = false; + foreach ($wrappedquestion->options->answers as $answer) { + if ($foundcorrect || $answer->fraction != 1.0) { + unset($wrappedquestion->options->answers[$answer->id]); + } else if (!$foundcorrect) { + $foundcorrect = true; + } + } + + if (!$QUIZ_QTYPES[$wrappedquestion->qtype] + ->create_session_and_responses($wrappedquestion, $state, $quiz, + $attempt)) { + return false; + } + $wrappedquestion->name_prefix = $question->name_prefix; + $wrappedquestion->maxgrade = $question->maxgrade; + $quiz->questionsinuse .= ",$wrappedquestion->id"; + $state->options->subquestions[$key] = clone($wrappedquestion); + } + + // Shuffle the answers if required + $subquestionids = array_values(array_map(create_function('$val', + 'return $val->id;'), $state->options->subquestions)); + if ($quiz->shuffleanswers) { + $subquestionids = swapshuffle($subquestionids); + } + $state->options->order = $subquestionids; + // Create empty responses + foreach ($subquestionids as $val) { + $state->responses[$val] = ''; + } + return true; } - function create_response($question, $nameprefix, $questionsinuse) { - // It's for question types like RANDOMSAMATCH and RANDOM that - // the true power of the pattern with this function comes to the surface. - // This implementation will stand even after a possible exclusion of - // the funtions extract_response and convert_to_response_answer_field + function restore_session_and_responses(&$question, &$state) { + global $QUIZ_QTYPES; + $responses = explode(',', $state->responses['']); + $responses = array_map(create_function('$val', + 'return explode("-", $val);'), $responses); - if (!isset($this->catrandoms[$question->category])) { - /// Need to fetch the shortanswer question ids for the category: - - $saquestions = get_records_select('quiz_questions', - " category='$question->category' - AND qtype='".SHORTANSWER."' - AND id NOT IN ($questionsinuse) "); - $this->catrandoms[$question->category] = array_keys($saquestions); - shuffle($this->catrandoms[$question->category]); - } - - /// Access question options to find out how many short-answer - /// questions we are supposed to pick... - if ($options = get_record('quiz_randomsamatch', - 'question', $question->id)) { - $questionstopick = $options->choose; - } else { - notify("Error: Missing question options! - Try to pick two shortanswer questions anyway"); - $questionstopick = 2; - } - - /// Pick the short-answer question ids and create the $response array - $response = array(); - while ($questionstopick) { - $said = array_pop($this->catrandoms[$question->category]); - if (!ereg("(^|,)$said(,|$)", $questionsinuse)) { - $response[$nameprefix.$said] = '0'; - --$questionstopick; + // Restore the previous responses + $state->responses = array(); + foreach ($responses as $response) { + $state->responses[$response[0]] = $response[1]; + $state->options->order[] = $response[0]; + if (!$wrappedquestion = get_record('quiz_questions', 'id', + $response[0])) { + notify("Couldn't get question (id=$response[0])!"); + return false; } - } + if (!$QUIZ_QTYPES[$wrappedquestion->qtype] + ->get_question_options($wrappedquestion)) { + notify("Couldn't get question options (id=$response[0])!"); + return false; + } + if (!$QUIZ_QTYPES[$wrappedquestion->qtype] + ->restore_session_and_responses($wrappedquestion, $state)) { + notify("Couldn't restore session of question (id=$response[0])!"); + return false; + } + $wrappedquestion->name_prefix = $question->name_prefix; + $wrappedquestion->maxgrade = $question->maxgrade; - if ($questionstopick) { - notify("Error: could not get enough Short-Answer questions!"); - $count = count($response); - $wanted = $count + $questionstopick; - notify("Got $count Short-Answer questions, but wanted $wanted."); + $state->options->subquestions[$wrappedquestion->id] = + clone($wrappedquestion); } - - return $response; + return true; } function extract_response($rawresponse, $nameprefix) { @@ -115,112 +182,16 @@ class quiz_randomsamatch_qtype extends quiz_match_qtype { return $response; } - function print_question_formulation_and_controls($question, - $quiz, $readonly, $answers, $correctanswers, $nameprefix) { - - // Print question formulation - - echo format_text($question->questiontext, - $question->questiontextformat, NULL, $quiz->course); - quiz_print_possible_question_image($quiz->id, $question); - - // Summarize shortanswer questions answer alternatives: - if (empty($correctanswers)) { - // Get them using the grade_response method - $tempresult = $this->grade_response($question, $nameprefix); - $saanswers = $tempresult->correctanswers; - } else { - $saanswers = $correctanswers; - } - foreach ($saanswers as $key => $saanswer) { - unset($saanswers[$key]); // Unsets the nameprefix occurence - $saanswers[$saanswer->id] = trim($saanswer->answer); - } - $saanswers = draw_rand_array($saanswers, count($saanswers)); - - // Print the shortanswer questions and input controls: - echo ''; - foreach ($question->response as $inputname => $response) { - if (!($saquestion = get_record('quiz_questions', 'id', - quiz_extract_posted_id($inputname, $nameprefix)))) { - notify("Error: cannot find shortanswer question for $inputname "); - continue; - } - - echo ''; - echo ''; - } - echo '
'; - echo $saquestion->questiontext; - echo ''; - if (!empty($correctanswers) - && $correctanswers[$inputname]->id == $response) { - echo ''; - choose_from_menu($saanswers, $inputname, $response); - echo '
'; - } else { - choose_from_menu($saanswers, $inputname, $response); - if ($readonly && $quiz->correctanswers - && isset($correctanswer[$inputname])) { - quiz_print_correctanswer($correctanswer[$inputname]->answer); - } - } - if ($quiz->feedback && isset($answers[$inputname]) - && $answers[$inputname]->feedback) { - quiz_print_comment($answers[$inputname]->feedback); - } - echo '
'; - } - - function grade_response($question, $nameprefix) { - global $QUIZ_QTYPES; - - $result->answers = array(); - $result->correctanswers = array(); - $result->grade = 0.0; - - foreach ($question->response as $inputname => $subresponse) { - if ($subquestion = get_record('quiz_questions', - 'id', quiz_extract_posted_id($inputname, $nameprefix), - // These two query conditions are security checks that prevents cheating... - 'qtype', SHORTANSWER, - 'category', $question->category)) { - - if ($subresponse = get_record('quiz_answers', - 'id', $subresponse)) { - $subquestion->response[$inputname] = $subresponse->answer; - } else { - $subquestion->response[$inputname] = ''; - } - - // Use the shortanswer framework to for grading... - $subresult = $QUIZ_QTYPES[SHORTANSWER] - ->grade_response($subquestion, $inputname); - - // Summarize shortanswer results - if (isset($subresult->answers[$inputname])) { - $result->answers[$inputname] = - $subresult->answers[$inputname]; - $result->grade += $result->answers[$inputname]->fraction; - if ($result->answers[$inputname]->fraction >= 1.0) { - $result->correctanswers[$inputname] = - $result->answers[$inputname]; - continue; - } - } - // Pick the first correctanswer: - foreach ($subresult->correctanswers as $correct) { - $result->correctanswers[$inputname] = $correct; - break; - } - } - } - if ($result->grade) { - $result->grade /= count($question->response); - } - return $result; + function get_sa_candidates($categorylist, $questionsinuse=0) { + return get_records_select('quiz_questions', + "qtype = '".SHORTANSWER."' " . + "AND category IN ($categorylist) " . + "AND parent = '0' " . + "AND hidden = '0'" . + "AND id NOT IN ($questionsinuse)"); } } + //// END OF CLASS //// ////////////////////////////////////////////////////////////////////////// diff --git a/mod/quiz/questiontypes/rqp/editquestion.php b/mod/quiz/questiontypes/rqp/editquestion.php new file mode 100644 index 00000000000..32ccd6762e9 --- /dev/null +++ b/mod/quiz/questiontypes/rqp/editquestion.php @@ -0,0 +1,20 @@ +dirroot . '/mod/quiz/questiontypes/rqp/lib.php'); + + $types = quiz_rqp_get_types(); + print_heading_with_help(get_string('editingrqp', 'quiz'), 'rqp', 'quiz'); + if (empty($question->id)) { + $question->options->type = ''; + $question->options->source = ''; + $question->options->format = ''; + } + else if (!$QUIZ_QTYPES[$question->qtype]->get_question_options($question)) { + $question->options->type = ''; + $question->options->source = ''; + $question->options->format = ''; + echo "

Error! Could not load the options for this question!

\n"; + } + require('rqp.html'); + +?> diff --git a/mod/quiz/questiontypes/rqp/edittype.php b/mod/quiz/questiontypes/rqp/edittype.php new file mode 100644 index 00000000000..1f085f8b9a4 --- /dev/null +++ b/mod/quiz/questiontypes/rqp/edittype.php @@ -0,0 +1,192 @@ +wwwroot . '/login/index.php'); + } + + if (!$site = get_site()) { + error('Site isn\'t defined!'); + } + + $sesskey = required_param('sesskey', PARAM_RAW); + if (!confirm_sesskey($sesskey)) { + error(get_string('confirmsesskeybad', 'error')); + } + $type->id = optional_param('id', NULL, PARAM_INT); + $type->name = optional_param('name', NULL, PARAM_RAW); + if (is_null($type->name)) { + $delete = optional_param('delete', NULL, PARAM_INT); + if (!is_null($delete)) { + $type->id = $delete; + $confirm = optional_param('confirm', NULL, PARAM_RAW); + } + } + else { + $delete = NULL; + $type->rendering_server = required_param('rendering_server', PARAM_RAW); + $type->cloning_server = required_param('cloning_server', PARAM_RAW); + } + + // Print the header + $stradmin = get_string('admin'); + $strconfiguration = get_string('configuration'); + $strmanagemodules = get_string('managemodules'); + $strmodulename = get_string('modulename', 'quiz'); + $stritemtypes = get_string('itemtypes', 'quiz'); + $streditingitemtype = get_string(is_null($type->id) ? 'addingitemtype' : 'editingitemtype', 'quiz'); + $navigation = '' . $stradmin . ' -> ' . + '' . $strconfiguration . ' -> ' . + '' . $strmanagemodules . ' -> ' . + '' . $strmodulename . ' -> ' . + '' . $stritemtypes . ' -> ' . + $streditingitemtype; + print_header($site->shortname . ': ' . $strmodulename . ': ' . $stritemtypes . ': ' . $streditingitemtype, $site->fullname, $navigation, '', '', true, '', ''); + + print_heading($streditingitemtype); + + $err = array(); + + // Save the type + while (!is_null($type->name)) { // using like if but with support for break + // Check basic settings + if (empty($type->name)) { + $err['name'] = get_string('missingitemtypename', 'quiz'); + break; + } + if (empty($type->rendering_server)) { + $err['rendering_server'] = get_string('missingrenderingserver', 'quiz'); + break; + } + // Check servers exist and work + $servers = remote_server_info($type, true, !empty($type->cloning_server)); + if (false === $servers['rendering']) { + $err['rendering_server'] = get_string('renderingserverconnectfailed', 'quiz', $type->rendering_server); + break; + } + if (!empty($type->cloning_server)) { + if (false === $servers['cloning']) { + $err['cloning_server'] = get_string('cloningserverconnectfailed', 'quiz', $type->cloning_server); + break; + } + } + if (!$servers['rendering']->rendering) { + $err['rendering_server'] = get_string('renderingserverdoesnt', 'quiz', $type->rendering_server); + break; + } + if (!empty($type->cloning_server)) { + if (!$servers['cloning']->cloning) { + if ($type->cloning_server !== $type->rendering_server) { + $err['cloning_server'] = get_string('cloningserverdoesnt', 'quiz', $type->cloning_server); + break; + } + if (!$servers['rendering']->implicitCloning) { + $err['cloning_server'] = get_string('cloningrenderingserverdoesnt', 'quiz', $type->cloning_server); + break; + } + } + } + // Set the server specific flags + $type->flags = 0; + $type->flags |= $servers['rendering']->itemCaching ? REMOTE_ITEM_CACHE : 0; + if (!empty($type->cloning_server)) { + $type->flags |= $servers['cloning']->cloning ? REMOTE_CLONING : 0; + if ($type->cloning_server === $type->rendering_server) { + $type->flags |= $servers['rendering']->implicitCloning ? REMOTE_IMPLICIT_CLONING : 0; + } + $type->flags |= $servers['cloning']->templateCaching ? REMOTE_TEMPLATE_CACHE : 0; + } + // Save the type in the database + if (!quiz_rqp_save_type($type)) { + error('Could not save the item type', 'types.php?sesskey=' . s(rawurlencode($sesskey))); + } + redirect('types.php?sesskey=' . rawurlencode($sesskey), get_string('success')); + // error and redirect both call exit so we never go round the loop + } + + if (is_null($type->name)) { + // Load the type + if (is_null($type->id)) { + $type->name = ''; + $type->rendering_server = ''; + $type->cloning_server = ''; + } else { + if (! ($type = get_record('quiz_rqp_type', 'id', $type->id))) { + error('Could not load the item type', 'types.php?sesskey=' . s(rawurlencode($sesskey))); + } + } + } + + // Display the delete form + if (!is_null($delete)) { + if (md5($delete) === $confirm) { + if (!quiz_rqp_delete_type($delete)) { + error('Could not delete the item type', 'types.php?sesskey=' . s(rawurlencode($sesskey))); + } + redirect('types.php?sesskey=' . rawurlencode($sesskey), get_string('success')); + } + notice_yesno(get_string('deleteitemtypecheck', 'quiz', $type->name), 'edittype.php?delete=' . s(rawurlencode($delete)) . '&confirm=' . s(rawurlencode(md5($delete))) . '&sesskey=' . s(rawurlencode($sesskey)), 'types.php?sesskey=' . s(rawurlencode($sesskey))); + print_footer(); + exit; + } +?> + +
+
+ + + + + + + + + + + + + + + + + +
: + + + +
: + + + +
: + + + +
+ + + + + +
+
+ + diff --git a/mod/quiz/questiontypes/rqp/icon.gif b/mod/quiz/questiontypes/rqp/icon.gif new file mode 100644 index 00000000000..5a26124cbe7 Binary files /dev/null and b/mod/quiz/questiontypes/rqp/icon.gif differ diff --git a/mod/quiz/questiontypes/rqp/lib.php b/mod/quiz/questiontypes/rqp/lib.php new file mode 100644 index 00000000000..14cb6de191d --- /dev/null +++ b/mod/quiz/questiontypes/rqp/lib.php @@ -0,0 +1,113 @@ +id)) { + return insert_record('quiz_rqp_type', $type, false); + } + return update_record('quiz_rqp_type', $type); +} + +function quiz_rqp_delete_type($id) { + return delete_records('quiz_rqp_type', 'id', $id); +} + +/** +* Creates a colon separated list of space separated values from an associative +* array of arrays +* +* An associative array of values or an associative array of arrays is imploded +* to a string by creating a colon separated list of space separated values. The +* key is treated as the first value. The {@link quiz_rqp_explode} function can +* restore the array from this string representation. +* @return string The string representation of the array. This is a colon +* separated list of space separated values. +* @param array $array An associative array of single values or an associative +* array of arrays to be imploded. +*/ +function quiz_rqp_implode($array) { + if (count($array) < 1) { + return ''; + } + $str = ''; + foreach ($array as $key => $val) { + $str .= $key . ' '; + if (is_array($val)) { + if (count($val) > 0) { + foreach ($val as $subval) { + $str .= $subval . ' '; + } + // Remove the trailing space + $str = substr($str, 0, -1); + } + } else { + $str .= $val; + } + $str .= ':'; + } + // Remove the trailing semi-colon + return substr($str, 0, -1); +} + +/** +* Recreates an associative array or an associative array of arrays from the +* string representation +* +* Takes a colon separated list of space separated values as produced by +* {@link quiz_rqp_implode} and recreates the array. If an array of single values +* is expected then an error results if an element has more than one value. +* Otherwise every value is an array. +* @return array The associative array restored from the string. Every +* element is a single value if $multi is false or an array +* if $multi is true. +* @param string $str The string to explode. This is a colon separated list of +* space separated values. +* @param boolean $multi Flag indicating if the values in the array are expected +* to be of multiple cardinality (i.e. an array of arrays +* is expected) or single values (i.e. an array of values). +* The default is false indicating an array of single +* values is expected. +*/ +function quiz_rqp_explode($str, $multi=false) { + // Explode by colon + if ($str === '') { + return array(); + } + $array = explode(':', $str); + $n = count($array); + $return = array(); + for ($i = 0; $i < $n; $i++) { + // Explode by space + $array[$i] = explode(' ', $array[$i]); + // Get the key + $key = array_shift($array[$i]); + if (array_key_exists($key, $return)) { + // Element appears twice! + return false; + } + // Save the element + if ($multi) { + $return[$key] = $array[$i]; + } else if (count($array[$i]) > 1) { + return false; + } else { + $return[$key] = $array[$i][0]; + } + } + return $return; +} + +?> diff --git a/mod/quiz/questiontypes/rqp/nusoap.php b/mod/quiz/questiontypes/rqp/nusoap.php new file mode 100644 index 00000000000..363f09e6898 --- /dev/null +++ b/mod/quiz/questiontypes/rqp/nusoap.php @@ -0,0 +1,6509 @@ + +* @version $Id$ +* @access public +*/ +class nusoap_base { + + var $title = 'NuSOAP'; + var $version = '0.6.9'; + var $revision = '$Revision$'; + var $error_str = ''; + var $debug_str = ''; + // toggles automatic encoding of special characters as entities + // (should always be true, I think) + var $charencoding = true; + + /** + * set schema version + * + * @var XMLSchemaVersion + * @access public + */ + var $XMLSchemaVersion = 'http://www.w3.org/2001/XMLSchema'; + + /** + * set charset encoding for outgoing messages + * + * @var soap_defencoding + * @access public + */ + //var $soap_defencoding = 'UTF-8'; + var $soap_defencoding = 'ISO-8859-1'; + + /** + * load namespace uris into an array of uri => prefix + * + * @var namespaces + * @access public + */ + var $namespaces = array( + 'SOAP-ENV' => 'http://schemas.xmlsoap.org/soap/envelope/', + 'xsd' => 'http://www.w3.org/2001/XMLSchema', + 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'SOAP-ENC' => 'http://schemas.xmlsoap.org/soap/encoding/', + 'si' => 'http://soapinterop.org/xsd'); + var $usedNamespaces = array(); + + /** + * load types into typemap array + * is this legacy yet? + * no, this is used by the xmlschema class to verify type => namespace mappings. + * @var typemap + * @access public + */ + var $typemap = array( + 'http://www.w3.org/2001/XMLSchema' => array( + 'string'=>'string','boolean'=>'boolean','float'=>'double','double'=>'double','decimal'=>'double', + 'duration'=>'','dateTime'=>'string','time'=>'string','date'=>'string','gYearMonth'=>'', + 'gYear'=>'','gMonthDay'=>'','gDay'=>'','gMonth'=>'','hexBinary'=>'string','base64Binary'=>'string', + // abstract "any" types + 'anyType'=>'string','anySimpleType'=>'string', + // derived datatypes + 'normalizedString'=>'string','token'=>'string','language'=>'','NMTOKEN'=>'','NMTOKENS'=>'','Name'=>'','NCName'=>'','ID'=>'', + 'IDREF'=>'','IDREFS'=>'','ENTITY'=>'','ENTITIES'=>'','integer'=>'integer','nonPositiveInteger'=>'integer', + 'negativeInteger'=>'integer','long'=>'integer','int'=>'integer','short'=>'integer','byte'=>'integer','nonNegativeInteger'=>'integer', + 'unsignedLong'=>'','unsignedInt'=>'','unsignedShort'=>'','unsignedByte'=>'','positiveInteger'=>''), + 'http://www.w3.org/2000/10/XMLSchema' => array( + 'i4'=>'','int'=>'integer','boolean'=>'boolean','string'=>'string','double'=>'double', + 'float'=>'double','dateTime'=>'string', + 'timeInstant'=>'string','base64Binary'=>'string','base64'=>'string','ur-type'=>'array'), + 'http://www.w3.org/1999/XMLSchema' => array( + 'i4'=>'','int'=>'integer','boolean'=>'boolean','string'=>'string','double'=>'double', + 'float'=>'double','dateTime'=>'string', + 'timeInstant'=>'string','base64Binary'=>'string','base64'=>'string','ur-type'=>'array'), + 'http://soapinterop.org/xsd' => array('SOAPStruct'=>'struct'), + 'http://schemas.xmlsoap.org/soap/encoding/' => array('base64'=>'string','array'=>'array','Array'=>'array'), + 'http://xml.apache.org/xml-soap' => array('Map') + ); + + /** + * entities to convert + * + * @var xmlEntities + * @access public + */ + var $xmlEntities = array('quot' => '"','amp' => '&', + 'lt' => '<','gt' => '>','apos' => "'"); + + /** + * adds debug data to the instance debug string with formatting + * + * @param string $string debug data + * @access private + */ + function debug($string){ + $this->appendDebug($this->getmicrotime().' '.get_class($this).": $string\n"); + } + + /** + * adds debug data to the instance debug string without formatting + * + * @param string $string debug data + * @access private + */ + function appendDebug($string){ + // it would be nice to use a memory stream here to use + // memory more efficiently + $this->debug_str .= $string; + } + + /** + * clears the current debug data for this instance + * + * @access public + */ + function clearDebug() { + // it would be nice to use a memory stream here to use + // memory more efficiently + $this->debug_str = ''; + } + + /** + * gets the current debug data for this instance + * + * @return debug data + * @access public + */ + function &getDebug() { + // it would be nice to use a memory stream here to use + // memory more efficiently + return $this->debug_str; + } + + /** + * gets the current debug data for this instance as an XML comment + * this may change the contents of the debug data + * + * @return debug data as an XML comment + * @access public + */ + function &getDebugAsXMLComment() { + // it would be nice to use a memory stream here to use + // memory more efficiently + while (strpos($this->debug_str, '--')) { + $this->debug_str = str_replace('--', '- -', $this->debug_str); + } + return ""; + } + + /** + * expands entities, e.g. changes '<' to '<'. + * + * @param string $val The string in which to expand entities. + * @access private + */ + function expandEntities($val) { + if ($this->charencoding) { + $val = str_replace('&', '&', $val); + $val = str_replace("'", ''', $val); + $val = str_replace('"', '"', $val); + $val = str_replace('<', '<', $val); + $val = str_replace('>', '>', $val); + } + return $val; + } + + /** + * returns error string if present + * + * @return mixed error string or false + * @access public + */ + function getError(){ + if($this->error_str != ''){ + return $this->error_str; + } + return false; + } + + /** + * sets error string + * + * @return boolean $string error string + * @access private + */ + function setError($str){ + $this->error_str = $str; + } + + /** + * detect if array is a simple array or a struct (associative array) + * + * @param $val The PHP array + * @return string (arraySimple|arrayStruct) + * @access private + */ + function isArraySimpleOrStruct($val) { + $keyList = array_keys($val); + foreach ($keyList as $keyListValue) { + if (!is_int($keyListValue)) { + return 'arrayStruct'; + } + } + return 'arraySimple'; + } + + /** + * serializes PHP values in accordance w/ section 5. Type information is + * not serialized if $use == 'literal'. + * + * @return string + * @access public + */ + function serialize_val($val,$name=false,$type=false,$name_ns=false,$type_ns=false,$attributes=false,$use='encoded'){ + if(is_object($val) && get_class($val) == 'soapval'){ + return $val->serialize($use); + } + $this->debug( "in serialize_val: $val, $name, $type, $name_ns, $type_ns, $attributes, $use"); + // if no name, use item + $name = (!$name|| is_numeric($name)) ? 'soapVal' : $name; + // if name has ns, add ns prefix to name + $xmlns = ''; + if($name_ns){ + $prefix = 'nu'.rand(1000,9999); + $name = $prefix.':'.$name; + $xmlns .= " xmlns:$prefix=\"$name_ns\""; + } + // if type is prefixed, create type prefix + if($type_ns != '' && $type_ns == $this->namespaces['xsd']){ + // need to fix this. shouldn't default to xsd if no ns specified + // w/o checking against typemap + $type_prefix = 'xsd'; + } elseif($type_ns){ + $type_prefix = 'ns'.rand(1000,9999); + $xmlns .= " xmlns:$type_prefix=\"$type_ns\""; + } + // serialize attributes if present + $atts = ''; + if($attributes){ + foreach($attributes as $k => $v){ + $atts .= " $k=\"$v\""; + } + } + // serialize null value + if (is_null($val)) { + if ($use == 'literal') { + // TODO: depends on nillable + return "<$name$xmlns $atts/>"; + } else { + return "<$name$xmlns $atts xsi:nil=\"true\"/>"; + } + } + // serialize if an xsd built-in primitive type + if($type != '' && isset($this->typemap[$this->XMLSchemaVersion][$type])){ + if (is_bool($val)) { + if ($type == 'boolean') { + $val = $val ? 'true' : 'false'; + } elseif (! $val) { + $val = 0; + } + } else if (is_string($val)) { + $val = $this->expandEntities($val); + } + if ($use == 'literal') { + return "<$name$xmlns $atts>$val"; + } else { + return "<$name$xmlns $atts xsi:type=\"xsd:$type\">$val"; + } + } + // detect type and serialize + $xml = ''; + switch(true) { + case (is_bool($val) || $type == 'boolean'): + if ($type == 'boolean') { + $val = $val ? 'true' : 'false'; + } elseif (! $val) { + $val = 0; + } + if ($use == 'literal') { + $xml .= "<$name$xmlns $atts>$val"; + } else { + $xml .= "<$name$xmlns xsi:type=\"xsd:boolean\"$atts>$val"; + } + break; + case (is_int($val) || is_long($val) || $type == 'int'): + if ($use == 'literal') { + $xml .= "<$name$xmlns $atts>$val"; + } else { + $xml .= "<$name$xmlns xsi:type=\"xsd:int\"$atts>$val"; + } + break; + case (is_float($val)|| is_double($val) || $type == 'float'): + if ($use == 'literal') { + $xml .= "<$name$xmlns $atts>$val"; + } else { + $xml .= "<$name$xmlns xsi:type=\"xsd:float\"$atts>$val"; + } + break; + case (is_string($val) || $type == 'string'): + $val = $this->expandEntities($val); + if ($use == 'literal') { + $xml .= "<$name$xmlns $atts>$val"; + } else { + $xml .= "<$name$xmlns xsi:type=\"xsd:string\"$atts>$val"; + } + break; + case is_object($val): + $name = get_class($val); + foreach(get_object_vars($val) as $k => $v){ + $pXml = isset($pXml) ? $pXml.$this->serialize_val($v,$k,false,false,false,false,$use) : $this->serialize_val($v,$k,false,false,false,false,$use); + } + $xml .= '<'.$name.'>'.$pXml.''; + break; + break; + case (is_array($val) || $type): + // detect if struct or array + $valueType = $this->isArraySimpleOrStruct($val); + if($valueType=='arraySimple' || ereg('^ArrayOf',$type)){ + $i = 0; + if(is_array($val) && count($val)> 0){ + foreach($val as $v){ + if(is_object($v) && get_class($v) == 'soapval'){ + $tt_ns = $v->type_ns; + $tt = $v->type; + } elseif (is_array($v)) { + $tt = $this->isArraySimpleOrStruct($v); + } else { + $tt = gettype($v); + } + $array_types[$tt] = 1; + $xml .= $this->serialize_val($v,'item',false,false,false,false,$use); + ++$i; + } + if(count($array_types) > 1){ + $array_typename = 'xsd:ur-type'; + } elseif(isset($tt) && isset($this->typemap[$this->XMLSchemaVersion][$tt])) { + if ($tt == 'integer') { + $tt = 'int'; + } + $array_typename = 'xsd:'.$tt; + } elseif(isset($tt) && $tt == 'arraySimple'){ + $array_typename = 'SOAP-ENC:Array'; + } elseif(isset($tt) && $tt == 'arrayStruct'){ + $array_typename = 'unnamed_struct_use_soapval'; + } else { + // if type is prefixed, create type prefix + if ($tt_ns != '' && $tt_ns == $this->namespaces['xsd']){ + $array_typename = 'xsd:' . $tt; + } elseif ($tt_ns) { + $tt_prefix = 'ns' . rand(1000, 9999); + $array_typename = "$tt_prefix:$tt"; + $xmlns .= " xmlns:$tt_prefix=\"$tt_ns\""; + } else { + $array_typename = $tt; + } + } + $array_type = $i; + if ($use == 'literal') { + $type_str = ''; + } else if (isset($type) && isset($type_prefix)) { + $type_str = " xsi:type=\"$type_prefix:$type\""; + } else { + $type_str = " xsi:type=\"SOAP-ENC:Array\" SOAP-ENC:arrayType=\"".$array_typename."[$array_type]\""; + } + // empty array + } else { + if ($use == 'literal') { + $type_str = ''; + } else if (isset($type) && isset($type_prefix)) { + $type_str = " xsi:type=\"$type_prefix:$type\""; + } else { + $type_str = " xsi:type=\"SOAP-ENC:Array\""; + } + } + $xml = "<$name$xmlns$type_str$atts>".$xml.""; + } else { + // got a struct + if(isset($type) && isset($type_prefix)){ + $type_str = " xsi:type=\"$type_prefix:$type\""; + } else { + $type_str = ''; + } + if ($use == 'literal') { + $xml .= "<$name$xmlns $atts>"; + } else { + $xml .= "<$name$xmlns$type_str$atts>"; + } + foreach($val as $k => $v){ + // Apache Map + if ($type == 'Map' && $type_ns == 'http://xml.apache.org/xml-soap') { + $xml .= ''; + $xml .= $this->serialize_val($k,'key',false,false,false,false,$use); + $xml .= $this->serialize_val($v,'value',false,false,false,false,$use); + $xml .= ''; + } else { + $xml .= $this->serialize_val($v,$k,false,false,false,false,$use); + } + } + $xml .= ""; + } + break; + default: + $xml .= 'not detected, got '.gettype($val).' for '.$val; + break; + } + return $xml; + } + + /** + * serialize message + * + * @param string body + * @param string headers optional + * @param array namespaces optional + * @param string style optional (rpc|document) + * @param string use optional (encoded|literal) + * @return string message + * @access public + */ + function serializeEnvelope($body,$headers=false,$namespaces=array(),$style='rpc',$use='encoded'){ + // TODO: add an option to automatically run utf8_encode on $body and $headers + // if $this->soap_defencoding is UTF-8. Not doing this automatically allows + // one to send arbitrary UTF-8 characters, not just characters that map to ISO-8859-1 + + // serialize namespaces + $ns_string = ''; + foreach(array_merge($this->namespaces,$namespaces) as $k => $v){ + $ns_string .= " xmlns:$k=\"$v\""; + } + if($style == 'rpc' && $use == 'encoded') { + $ns_string = ' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"' . $ns_string; + } + + // serialize headers + if($headers){ + $headers = "".$headers.""; + } + // serialize envelope + return + 'soap_defencoding .'"?'.">". + '". + $headers. + "". + $body. + "". + ""; + } + + function formatDump($str){ + $str = htmlspecialchars($str); + return nl2br($str); + } + + /** + * contracts a qualified name + * + * @param string $string qname + * @return string contracted qname + * @access private + */ + function contractQname($qname){ + // get element namespace + //$this->xdebug("Contract $qname"); + if (strrpos($qname, ':')) { + // get unqualified name + $name = substr($qname, strrpos($qname, ':') + 1); + // get ns + $ns = substr($qname, 0, strrpos($qname, ':')); + $p = $this->getPrefixFromNamespace($ns); + if ($p) { + return $p . ':' . $name; + } + return $qname; + } else { + return $qname; + } + } + + /** + * expands a qualified name + * + * @param string $string qname + * @return string expanded qname + * @access private + */ + function expandQname($qname){ + // get element prefix + if(strpos($qname,':') && !ereg('^http://',$qname)){ + // get unqualified name + $name = substr(strstr($qname,':'),1); + // get ns prefix + $prefix = substr($qname,0,strpos($qname,':')); + if(isset($this->namespaces[$prefix])){ + return $this->namespaces[$prefix].':'.$name; + } else { + return $qname; + } + } else { + return $qname; + } + } + + /** + * returns the local part of a prefixed string + * returns the original string, if not prefixed + * + * @param string + * @return string + * @access public + */ + function getLocalPart($str){ + if($sstr = strrchr($str,':')){ + // get unqualified name + return substr( $sstr, 1 ); + } else { + return $str; + } + } + + /** + * returns the prefix part of a prefixed string + * returns false, if not prefixed + * + * @param string + * @return mixed + * @access public + */ + function getPrefix($str){ + if($pos = strrpos($str,':')){ + // get prefix + return substr($str,0,$pos); + } + return false; + } + + /** + * pass it a prefix, it returns a namespace + * returns false if no namespace registered with the given prefix + * + * @param string + * @return mixed + * @access public + */ + function getNamespaceFromPrefix($prefix){ + if (isset($this->namespaces[$prefix])) { + return $this->namespaces[$prefix]; + } + //$this->setError("No namespace registered for prefix '$prefix'"); + return false; + } + + /** + * returns the prefix for a given namespace (or prefix) + * or false if no prefixes registered for the given namespace + * + * @param string + * @return mixed + * @access public + */ + function getPrefixFromNamespace($ns) { + foreach ($this->namespaces as $p => $n) { + if ($ns == $n || $ns == $p) { + $this->usedNamespaces[$p] = $n; + return $p; + } + } + return false; + } + + /** + * returns the time in ODBC canonical form with microseconds + * + * @return string + * @access public + */ + function getmicrotime() { + if (function_exists('gettimeofday')) { + $tod = gettimeofday(); + $sec = $tod['sec']; + $usec = $tod['usec']; + } else { + $sec = time(); + $usec = 0; + } + return strftime('%Y-%m-%d %H:%M:%S', $sec) . '.' . sprintf('%06d', $usec); + } + + function varDump($data) { + ob_start(); + var_dump($data); + $ret_val = ob_get_contents(); + ob_end_clean(); + return $ret_val; + } +} + +// XML Schema Datatype Helper Functions + +//xsd:dateTime helpers + +/** +* convert unix timestamp to ISO 8601 compliant date string +* +* @param string $timestamp Unix time stamp +* @access public +*/ +function timestamp_to_iso8601($timestamp,$utc=true){ + $datestr = date('Y-m-d\TH:i:sO',$timestamp); + if($utc){ + $eregStr = + '([0-9]{4})-'. // centuries & years CCYY- + '([0-9]{2})-'. // months MM- + '([0-9]{2})'. // days DD + 'T'. // separator T + '([0-9]{2}):'. // hours hh: + '([0-9]{2}):'. // minutes mm: + '([0-9]{2})(\.[0-9]*)?'. // seconds ss.ss... + '(Z|[+\-][0-9]{2}:?[0-9]{2})?'; // Z to indicate UTC, -/+HH:MM:SS.SS... for local tz's + + if(ereg($eregStr,$datestr,$regs)){ + return sprintf('%04d-%02d-%02dT%02d:%02d:%02dZ',$regs[1],$regs[2],$regs[3],$regs[4],$regs[5],$regs[6]); + } + return false; + } else { + return $datestr; + } +} + +/** +* convert ISO 8601 compliant date string to unix timestamp +* +* @param string $datestr ISO 8601 compliant date string +* @access public +*/ +function iso8601_to_timestamp($datestr){ + $eregStr = + '([0-9]{4})-'. // centuries & years CCYY- + '([0-9]{2})-'. // months MM- + '([0-9]{2})'. // days DD + 'T'. // separator T + '([0-9]{2}):'. // hours hh: + '([0-9]{2}):'. // minutes mm: + '([0-9]{2})(\.[0-9]+)?'. // seconds ss.ss... + '(Z|[+\-][0-9]{2}:?[0-9]{2})?'; // Z to indicate UTC, -/+HH:MM:SS.SS... for local tz's + if(ereg($eregStr,$datestr,$regs)){ + // not utc + if($regs[8] != 'Z'){ + $op = substr($regs[8],0,1); + $h = substr($regs[8],1,2); + $m = substr($regs[8],strlen($regs[8])-2,2); + if($op == '-'){ + $regs[4] = $regs[4] + $h; + $regs[5] = $regs[5] + $m; + } elseif($op == '+'){ + $regs[4] = $regs[4] - $h; + $regs[5] = $regs[5] - $m; + } + } + return strtotime("$regs[1]-$regs[2]-$regs[3] $regs[4]:$regs[5]:$regs[6]Z"); + } else { + return false; + } +} + +function usleepWindows($usec) +{ + $start = gettimeofday(); + + do + { + $stop = gettimeofday(); + $timePassed = 1000000 * ($stop['sec'] - $start['sec']) + + $stop['usec'] - $start['usec']; + } + while ($timePassed < $usec); +} + +?> +* @version $Id$ +* @access public +*/ +class soap_fault extends nusoap_base { + + var $faultcode; + var $faultactor; + var $faultstring; + var $faultdetail; + + /** + * constructor + * + * @param string $faultcode (client | server) + * @param string $faultactor only used when msg routed between multiple actors + * @param string $faultstring human readable error message + * @param string $faultdetail + */ + function soap_fault($faultcode,$faultactor='',$faultstring='',$faultdetail=''){ + $this->faultcode = $faultcode; + $this->faultactor = $faultactor; + $this->faultstring = $faultstring; + $this->faultdetail = $faultdetail; + } + + /** + * serialize a fault + * + * @access public + */ + function serialize(){ + $ns_string = ''; + foreach($this->namespaces as $k => $v){ + $ns_string .= "\n xmlns:$k=\"$v\""; + } + $return_msg = + 'soap_defencoding.'"?>'. + '\n". + ''. + ''. + ''.$this->expandEntities($this->faultcode).''. + ''.$this->expandEntities($this->faultactor).''. + ''.$this->expandEntities($this->faultstring).''. + ''.$this->serialize_val($this->faultdetail).''. + ''. + ''. + ''; + return $return_msg; + } +} + + + +?> +* @version $Id$ +* @access public +*/ +class XMLSchema extends nusoap_base { + + // files + var $schema = ''; + var $xml = ''; + // namespaces + var $enclosingNamespaces; + // schema info + var $schemaInfo = array(); + var $schemaTargetNamespace = ''; + // types, elements, attributes defined by the schema + var $attributes = array(); + var $complexTypes = array(); + var $complexTypeStack = array(); + var $currentComplexType = null; + var $elements = array(); + var $elementStack = array(); + var $currentElement = null; + var $simpleTypes = array(); + var $simpleTypeStack = array(); + var $currentSimpleType = null; + // imports + var $imports = array(); + // parser vars + var $parser; + var $position = 0; + var $depth = 0; + var $depth_array = array(); + var $message = array(); + var $defaultNamespace = array(); + + /** + * constructor + * + * @param string $schema schema document URI + * @param string $xml xml document URI + * @param string $namespaces namespaces defined in enclosing XML + * @access public + */ + function XMLSchema($schema='',$xml='',$namespaces=array()){ + + $this->debug('xmlschema class instantiated, inside constructor'); + // files + $this->schema = $schema; + $this->xml = $xml; + + // namespaces + $this->enclosingNamespaces = $namespaces; + $this->namespaces = array_merge($this->namespaces, $namespaces); + + // parse schema file + if($schema != ''){ + $this->debug('initial schema file: '.$schema); + $this->parseFile($schema, 'schema'); + } + + // parse xml file + if($xml != ''){ + $this->debug('initial xml file: '.$xml); + $this->parseFile($xml, 'xml'); + } + + } + + /** + * parse an XML file + * + * @param string $xml, path/URL to XML file + * @param string $type, (schema | xml) + * @return boolean + * @access public + */ + function parseFile($xml,$type){ + // parse xml file + if($xml != ""){ + $xmlStr = @join("",@file($xml)); + if($xmlStr == ""){ + $msg = 'Error reading XML from '.$xml; + $this->setError($msg); + $this->debug($msg); + return false; + } else { + $this->debug("parsing $xml"); + $this->parseString($xmlStr,$type); + $this->debug("done parsing $xml"); + return true; + } + } + return false; + } + + /** + * parse an XML string + * + * @param string $xml path or URL + * @param string $type, (schema|xml) + * @access private + */ + function parseString($xml,$type){ + // parse xml string + if($xml != ""){ + + // Create an XML parser. + $this->parser = xml_parser_create(); + // Set the options for parsing the XML data. + xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0); + + // Set the object for the parser. + xml_set_object($this->parser, $this); + + // Set the element handlers for the parser. + if($type == "schema"){ + xml_set_element_handler($this->parser, 'schemaStartElement','schemaEndElement'); + xml_set_character_data_handler($this->parser,'schemaCharacterData'); + } elseif($type == "xml"){ + xml_set_element_handler($this->parser, 'xmlStartElement','xmlEndElement'); + xml_set_character_data_handler($this->parser,'xmlCharacterData'); + } + + // Parse the XML file. + if(!xml_parse($this->parser,$xml,true)){ + // Display an error message. + $errstr = sprintf('XML error parsing XML schema on line %d: %s', + xml_get_current_line_number($this->parser), + xml_error_string(xml_get_error_code($this->parser)) + ); + $this->debug($errstr); + $this->debug("XML payload:\n" . $xml); + $this->setError($errstr); + } + + xml_parser_free($this->parser); + } else{ + $this->debug('no xml passed to parseString()!!'); + $this->setError('no xml passed to parseString()!!'); + } + } + + /** + * start-element handler + * + * @param string $parser XML parser object + * @param string $name element name + * @param string $attrs associative array of attributes + * @access private + */ + function schemaStartElement($parser, $name, $attrs) { + + // position in the total number of elements, starting from 0 + $pos = $this->position++; + $depth = $this->depth++; + // set self as current value for this depth + $this->depth_array[$depth] = $pos; + $this->message[$pos] = array('cdata' => ''); + if ($depth > 0) { + $this->defaultNamespace[$pos] = $this->defaultNamespace[$this->depth_array[$depth - 1]]; + } else { + $this->defaultNamespace[$pos] = false; + } + + // get element prefix + if($prefix = $this->getPrefix($name)){ + // get unqualified name + $name = $this->getLocalPart($name); + } else { + $prefix = ''; + } + + // loop thru attributes, expanding, and registering namespace declarations + if(count($attrs) > 0){ + foreach($attrs as $k => $v){ + // if ns declarations, add to class level array of valid namespaces + if(ereg("^xmlns",$k)){ + //$this->xdebug("$k: $v"); + //$this->xdebug('ns_prefix: '.$this->getPrefix($k)); + if($ns_prefix = substr(strrchr($k,':'),1)){ + //$this->xdebug("Add namespace[$ns_prefix] = $v"); + $this->namespaces[$ns_prefix] = $v; + } else { + $this->defaultNamespace[$pos] = $v; + if (! $this->getPrefixFromNamespace($v)) { + $this->namespaces['ns'.(count($this->namespaces)+1)] = $v; + } + } + if($v == 'http://www.w3.org/2001/XMLSchema' || $v == 'http://www.w3.org/1999/XMLSchema' || $v == 'http://www.w3.org/2000/10/XMLSchema'){ + $this->XMLSchemaVersion = $v; + $this->namespaces['xsi'] = $v.'-instance'; + } + } + } + foreach($attrs as $k => $v){ + // expand each attribute + $k = strpos($k,':') ? $this->expandQname($k) : $k; + $v = strpos($v,':') ? $this->expandQname($v) : $v; + $eAttrs[$k] = $v; + } + $attrs = $eAttrs; + } else { + $attrs = array(); + } + // find status, register data + switch($name){ + case 'all': // (optional) compositor content for a complexType + case 'choice': + case 'group': + case 'sequence': + //$this->xdebug("compositor $name for currentComplexType: $this->currentComplexType and currentElement: $this->currentElement"); + $this->complexTypes[$this->currentComplexType]['compositor'] = $name; + //if($name == 'all' || $name == 'sequence'){ + // $this->complexTypes[$this->currentComplexType]['phpType'] = 'struct'; + //} + break; + case 'attribute': // complexType attribute + //$this->xdebug("parsing attribute $attrs[name] $attrs[ref] of value: ".$attrs['http://schemas.xmlsoap.org/wsdl/:arrayType']); + $this->xdebug("parsing attribute:"); + $this->appendDebug($this->varDump($attrs)); + if (!isset($attrs['form'])) { + $attrs['form'] = $this->schemaInfo['attributeFormDefault']; + } + if (isset($attrs['http://schemas.xmlsoap.org/wsdl/:arrayType'])) { + $v = $attrs['http://schemas.xmlsoap.org/wsdl/:arrayType']; + if (!strpos($v, ':')) { + // no namespace in arrayType attribute value... + if ($this->defaultNamespace[$pos]) { + // ...so use the default + $attrs['http://schemas.xmlsoap.org/wsdl/:arrayType'] = $this->defaultNamespace[$pos] . ':' . $attrs['http://schemas.xmlsoap.org/wsdl/:arrayType']; + } + } + } + if(isset($attrs['name'])){ + $this->attributes[$attrs['name']] = $attrs; + $aname = $attrs['name']; + } elseif(isset($attrs['ref']) && $attrs['ref'] == 'http://schemas.xmlsoap.org/soap/encoding/:arrayType'){ + if (isset($attrs['http://schemas.xmlsoap.org/wsdl/:arrayType'])) { + $aname = $attrs['http://schemas.xmlsoap.org/wsdl/:arrayType']; + } else { + $aname = ''; + } + } elseif(isset($attrs['ref'])){ + $aname = $attrs['ref']; + $this->attributes[$attrs['ref']] = $attrs; + } + + if($this->currentComplexType){ // This should *always* be + $this->complexTypes[$this->currentComplexType]['attrs'][$aname] = $attrs; + } + // arrayType attribute + if(isset($attrs['http://schemas.xmlsoap.org/wsdl/:arrayType']) || $this->getLocalPart($aname) == 'arrayType'){ + $this->complexTypes[$this->currentComplexType]['phpType'] = 'array'; + $prefix = $this->getPrefix($aname); + if(isset($attrs['http://schemas.xmlsoap.org/wsdl/:arrayType'])){ + $v = $attrs['http://schemas.xmlsoap.org/wsdl/:arrayType']; + } else { + $v = ''; + } + if(strpos($v,'[,]')){ + $this->complexTypes[$this->currentComplexType]['multidimensional'] = true; + } + $v = substr($v,0,strpos($v,'[')); // clip the [] + if(!strpos($v,':') && isset($this->typemap[$this->XMLSchemaVersion][$v])){ + $v = $this->XMLSchemaVersion.':'.$v; + } + $this->complexTypes[$this->currentComplexType]['arrayType'] = $v; + } + break; + case 'complexContent': // (optional) content for a complexType + break; + case 'complexType': + array_push($this->complexTypeStack, $this->currentComplexType); + if(isset($attrs['name'])){ + $this->xdebug('processing named complexType '.$attrs['name']); + //$this->currentElement = false; + $this->currentComplexType = $attrs['name']; + $this->complexTypes[$this->currentComplexType] = $attrs; + $this->complexTypes[$this->currentComplexType]['typeClass'] = 'complexType'; + // This is for constructs like + // + // + // + // + // + if(isset($attrs['base']) && ereg(':Array$',$attrs['base'])){ + $this->xdebug('complexType is unusual array'); + $this->complexTypes[$this->currentComplexType]['phpType'] = 'array'; + } else { + $this->complexTypes[$this->currentComplexType]['phpType'] = 'struct'; + } + }else{ + $this->xdebug('processing unnamed complexType for element '.$this->currentElement); + $this->currentComplexType = $this->currentElement . '_ContainedType'; + //$this->currentElement = false; + $this->complexTypes[$this->currentComplexType] = $attrs; + $this->complexTypes[$this->currentComplexType]['typeClass'] = 'complexType'; + // This is for constructs like + // + // + // + // + // + if(isset($attrs['base']) && ereg(':Array$',$attrs['base'])){ + $this->xdebug('complexType is unusual array'); + $this->complexTypes[$this->currentComplexType]['phpType'] = 'array'; + } else { + $this->complexTypes[$this->currentComplexType]['phpType'] = 'struct'; + } + } + break; + case 'element': + array_push($this->elementStack, $this->currentElement); + // elements defined as part of a complex type should + // not really be added to $this->elements, but for some + // reason, they are + if (!isset($attrs['form'])) { + $attrs['form'] = $this->schemaInfo['elementFormDefault']; + } + if(isset($attrs['type'])){ + $this->xdebug("processing typed element ".$attrs['name']." of type ".$attrs['type']); + if (! $this->getPrefix($attrs['type'])) { + if ($this->defaultNamespace[$pos]) { + $attrs['type'] = $this->defaultNamespace[$pos] . ':' . $attrs['type']; + $this->xdebug('used default namespace to make type ' . $attrs['type']); + } + } + // This is for constructs like + // + // + // + // + // + if ($this->currentComplexType && $this->complexTypes[$this->currentComplexType]['phpType'] == 'array') { + $this->xdebug('arrayType for unusual array is ' . $attrs['type']); + $this->complexTypes[$this->currentComplexType]['arrayType'] = $attrs['type']; + } + $this->currentElement = $attrs['name']; + $this->elements[ $attrs['name'] ] = $attrs; + $this->elements[ $attrs['name'] ]['typeClass'] = 'element'; + $ename = $attrs['name']; + } elseif(isset($attrs['ref'])){ + $this->xdebug("processing element as ref to ".$attrs['ref']); + $this->currentElement = "ref to ".$attrs['ref']; + $ename = $this->getLocalPart($attrs['ref']); + } else { + $this->xdebug("processing untyped element ".$attrs['name']); + $this->currentElement = $attrs['name']; + $this->elements[ $attrs['name'] ] = $attrs; + $this->elements[ $attrs['name'] ]['typeClass'] = 'element'; + $attrs['type'] = $this->schemaTargetNamespace . ':' . $attrs['name'] . '_ContainedType'; + $this->elements[ $attrs['name'] ]['type'] = $attrs['type']; + $ename = $attrs['name']; + } + if(isset($ename) && $this->currentComplexType){ + $this->complexTypes[$this->currentComplexType]['elements'][$ename] = $attrs; + } + break; + case 'enumeration': // restriction value list member + $this->xdebug('enumeration ' . $attrs['value']); + if ($this->currentSimpleType) { + $this->simpleTypes[$this->currentSimpleType]['enumeration'][] = $attrs['value']; + } elseif ($this->currentComplexType) { + $this->complexTypes[$this->currentComplexType]['enumeration'][] = $attrs['value']; + } + break; + case 'extension': // simpleContent or complexContent type extension + $this->xdebug('extension ' . $attrs['base']); + if ($this->currentComplexType) { + $this->complexTypes[$this->currentComplexType]['extensionBase'] = $attrs['base']; + } + break; + case 'import': + if (isset($attrs['schemaLocation'])) { + //$this->xdebug('import namespace ' . $attrs['namespace'] . ' from ' . $attrs['schemaLocation']); + $this->imports[$attrs['namespace']][] = array('location' => $attrs['schemaLocation'], 'loaded' => false); + } else { + //$this->xdebug('import namespace ' . $attrs['namespace']); + $this->imports[$attrs['namespace']][] = array('location' => '', 'loaded' => true); + if (! $this->getPrefixFromNamespace($attrs['namespace'])) { + $this->namespaces['ns'.(count($this->namespaces)+1)] = $attrs['namespace']; + } + } + break; + case 'list': // simpleType value list + break; + case 'restriction': // simpleType, simpleContent or complexContent value restriction + $this->xdebug('restriction ' . $attrs['base']); + if($this->currentSimpleType){ + $this->simpleTypes[$this->currentSimpleType]['type'] = $attrs['base']; + } elseif($this->currentComplexType){ + $this->complexTypes[$this->currentComplexType]['restrictionBase'] = $attrs['base']; + if(strstr($attrs['base'],':') == ':Array'){ + $this->complexTypes[$this->currentComplexType]['phpType'] = 'array'; + } + } + break; + case 'schema': + $this->schemaInfo = $attrs; + $this->schemaInfo['schemaVersion'] = $this->getNamespaceFromPrefix($prefix); + if (isset($attrs['targetNamespace'])) { + $this->schemaTargetNamespace = $attrs['targetNamespace']; + } + if (!isset($attrs['elementFormDefault'])) { + $this->schemaInfo['elementFormDefault'] = 'unqualified'; + } + if (!isset($attrs['attributeFormDefault'])) { + $this->schemaInfo['attributeFormDefault'] = 'unqualified'; + } + break; + case 'simpleContent': // (optional) content for a complexType + break; + case 'simpleType': + array_push($this->simpleTypeStack, $this->currentSimpleType); + if(isset($attrs['name'])){ + $this->xdebug("processing simpleType for name " . $attrs['name']); + $this->currentSimpleType = $attrs['name']; + $this->simpleTypes[ $attrs['name'] ] = $attrs; + $this->simpleTypes[ $attrs['name'] ]['typeClass'] = 'simpleType'; + $this->simpleTypes[ $attrs['name'] ]['phpType'] = 'scalar'; + } else { + $this->xdebug('processing unnamed simpleType for element '.$this->currentElement); + $this->currentSimpleType = $this->currentElement . '_ContainedType'; + //$this->currentElement = false; + $this->simpleTypes[$this->currentSimpleType] = $attrs; + $this->simpleTypes[$this->currentSimpleType]['phpType'] = 'scalar'; + } + break; + case 'union': // simpleType type list + break; + default: + //$this->xdebug("do not have anything to do for element $name"); + } + } + + /** + * end-element handler + * + * @param string $parser XML parser object + * @param string $name element name + * @access private + */ + function schemaEndElement($parser, $name) { + // bring depth down a notch + $this->depth--; + // position of current element is equal to the last value left in depth_array for my depth + if(isset($this->depth_array[$this->depth])){ + $pos = $this->depth_array[$this->depth]; + } + // get element prefix + if ($prefix = $this->getPrefix($name)){ + // get unqualified name + $name = $this->getLocalPart($name); + } else { + $prefix = ''; + } + // move on... + if($name == 'complexType'){ + $this->xdebug('done processing complexType ' . ($this->currentComplexType ? $this->currentComplexType : '(unknown)')); + $this->currentComplexType = array_pop($this->complexTypeStack); + //$this->currentElement = false; + } + if($name == 'element'){ + $this->xdebug('done processing element ' . ($this->currentElement ? $this->currentElement : '(unknown)')); + $this->currentElement = array_pop($this->elementStack); + } + if($name == 'simpleType'){ + $this->xdebug('done processing simpleType ' . ($this->currentSimpleType ? $this->currentSimpleType : '(unknown)')); + $this->currentSimpleType = array_pop($this->simpleTypeStack); + } + } + + /** + * element content handler + * + * @param string $parser XML parser object + * @param string $data element content + * @access private + */ + function schemaCharacterData($parser, $data){ + $pos = $this->depth_array[$this->depth - 1]; + $this->message[$pos]['cdata'] .= $data; + } + + /** + * serialize the schema + * + * @access public + */ + function serializeSchema(){ + + $schemaPrefix = $this->getPrefixFromNamespace($this->XMLSchemaVersion); + $xml = ''; + // imports + if (sizeof($this->imports) > 0) { + foreach($this->imports as $ns => $list) { + foreach ($list as $ii) { + if ($ii['location'] != '') { + $xml .= " <$schemaPrefix:import location=\"" . $ii['location'] . '" namespace="' . $ns . "\" />\n"; + } else { + $xml .= " <$schemaPrefix:import namespace=\"" . $ns . "\" />\n"; + } + } + } + } + // complex types + foreach($this->complexTypes as $typeName => $attrs){ + $contentStr = ''; + // serialize child elements + if(isset($attrs['elements']) && (count($attrs['elements']) > 0)){ + foreach($attrs['elements'] as $element => $eParts){ + if(isset($eParts['ref'])){ + $contentStr .= " <$schemaPrefix:element ref=\"$element\"/>\n"; + } else { + $contentStr .= " <$schemaPrefix:element name=\"$element\" type=\"" . $this->contractQName($eParts['type']) . "\""; + foreach ($eParts as $aName => $aValue) { + // handle, e.g., abstract, default, form, minOccurs, maxOccurs, nillable + if ($aName != 'name' && $aName != 'type') { + $contentStr .= " $aName=\"$aValue\""; + } + } + $contentStr .= "/>\n"; + } + } + } + // attributes + if(isset($attrs['attrs']) && (count($attrs['attrs']) >= 1)){ + foreach($attrs['attrs'] as $attr => $aParts){ + $contentStr .= " <$schemaPrefix:attribute ref=\"".$this->contractQName($aParts['ref']).'"'; + if(isset($aParts['http://schemas.xmlsoap.org/wsdl/:arrayType'])){ + $this->usedNamespaces['wsdl'] = $this->namespaces['wsdl']; + $contentStr .= ' wsdl:arrayType="'.$this->contractQName($aParts['http://schemas.xmlsoap.org/wsdl/:arrayType']).'"'; + } + $contentStr .= "/>\n"; + } + } + // if restriction + if( isset($attrs['restrictionBase']) && $attrs['restrictionBase'] != ''){ + $contentStr = " <$schemaPrefix:restriction base=\"".$this->contractQName($attrs['restrictionBase'])."\">\n".$contentStr." \n"; + } + // compositor obviates complex/simple content + if(isset($attrs['compositor']) && ($attrs['compositor'] != '')){ + $contentStr = " <$schemaPrefix:$attrs[compositor]>\n".$contentStr." \n"; + } + // complex or simple content + elseif((isset($attrs['elements']) && count($attrs['elements']) > 0) || (isset($attrs['attrs']) && count($attrs['attrs']) > 0)){ + $contentStr = " <$schemaPrefix:complexContent>\n".$contentStr." \n"; + } + // finalize complex type + if($contentStr != ''){ + $contentStr = " <$schemaPrefix:complexType name=\"$typeName\">\n".$contentStr." \n"; + } else { + $contentStr = " <$schemaPrefix:complexType name=\"$typeName\"/>\n"; + } + $xml .= $contentStr; + } + // simple types + if(isset($this->simpleTypes) && count($this->simpleTypes) > 0){ + foreach($this->simpleTypes as $typeName => $eParts){ + $xml .= " <$schemaPrefix:simpleType name=\"$typeName\">\n <$schemaPrefix:restriction base=\"".$this->contractQName($eParts['type'])."\"/>\n"; + if (isset($eParts['enumeration'])) { + foreach ($eParts['enumeration'] as $e) { + $xml .= " <$schemaPrefix:enumeration value=\"$e\"/>\n"; + } + } + $xml .= " "; + } + } + // elements + if(isset($this->elements) && count($this->elements) > 0){ + foreach($this->elements as $element => $eParts){ + $xml .= " <$schemaPrefix:element name=\"$element\" type=\"".$this->contractQName($eParts['type'])."\"/>\n"; + } + } + // attributes + if(isset($this->attributes) && count($this->attributes) > 0){ + foreach($this->attributes as $attr => $aParts){ + $xml .= " <$schemaPrefix:attribute name=\"$attr\" type=\"".$this->contractQName($aParts['type'])."\"\n/>"; + } + } + // finish 'er up + $el = "<$schemaPrefix:schema targetNamespace=\"$this->schemaTargetNamespace\"\n"; + foreach (array_diff($this->usedNamespaces, $this->enclosingNamespaces) as $nsp => $ns) { + $el .= " xmlns:$nsp=\"$ns\"\n"; + } + $xml = $el . ">\n".$xml."\n"; + return $xml; + } + + /** + * adds debug data to the clas level debug string + * + * @param string $string debug data + * @access private + */ + function xdebug($string){ + $this->debug('<' . $this->schemaTargetNamespace . '> '.$string); + } + + /** + * get the PHP type of a user defined type in the schema + * PHP type is kind of a misnomer since it actually returns 'struct' for assoc. arrays + * returns false if no type exists, or not w/ the given namespace + * else returns a string that is either a native php type, or 'struct' + * + * @param string $type, name of defined type + * @param string $ns, namespace of type + * @return mixed + * @access public + */ + function getPHPType($type,$ns){ + if(isset($this->typemap[$ns][$type])){ + //print "found type '$type' and ns $ns in typemap
"; + return $this->typemap[$ns][$type]; + } elseif(isset($this->complexTypes[$type])){ + //print "getting type '$type' and ns $ns from complexTypes array
"; + return $this->complexTypes[$type]['phpType']; + } + return false; + } + + /** + * returns an array of information about a given type + * returns false if no type exists by the given name + * + * typeDef = array( + * 'elements' => array(), // refs to elements array + * 'restrictionBase' => '', + * 'phpType' => '', + * 'order' => '(sequence|all)', + * 'attrs' => array() // refs to attributes array + * ) + * + * @param string + * @return mixed + * @access public + */ + function getTypeDef($type){ + //$this->debug("in getTypeDef for type $type"); + if(isset($this->complexTypes[$type])){ + $this->xdebug("in getTypeDef, found complexType $type"); + return $this->complexTypes[$type]; + } elseif(isset($this->simpleTypes[$type])){ + $this->xdebug("in getTypeDef, found simpleType $type"); + if (!isset($this->simpleTypes[$type]['phpType'])) { + // get info for type to tack onto the simple type + // TODO: can this ever really apply (i.e. what is a simpleType really?) + $uqType = substr($this->simpleTypes[$type]['type'], strrpos($this->simpleTypes[$type]['type'], ':') + 1); + $ns = substr($this->simpleTypes[$type]['type'], 0, strrpos($this->simpleTypes[$type]['type'], ':')); + $etype = $this->getTypeDef($uqType); + if ($etype) { + $this->xdebug("in getTypeDef, found type $etype for simpleType $type"); + if (isset($etype['phpType'])) { + $this->simpleTypes[$type]['phpType'] = $etype['phpType']; + } + if (isset($etype['elements'])) { + $this->simpleTypes[$type]['elements'] = $etype['elements']; + } + } + } + return $this->simpleTypes[$type]; + } elseif(isset($this->elements[$type])){ + $this->xdebug("in getTypeDef, found element $type"); + if (!isset($this->elements[$type]['phpType'])) { + // get info for type to tack onto the element + $uqType = substr($this->elements[$type]['type'], strrpos($this->elements[$type]['type'], ':') + 1); + $ns = substr($this->elements[$type]['type'], 0, strrpos($this->elements[$type]['type'], ':')); + $etype = $this->getTypeDef($uqType); + if ($etype) { + $this->xdebug("in getTypeDef, found type $etype for element $type"); + if (isset($etype['phpType'])) { + $this->elements[$type]['phpType'] = $etype['phpType']; + } + if (isset($etype['elements'])) { + $this->elements[$type]['elements'] = $etype['elements']; + } + } elseif ($ns == 'http://www.w3.org/2001/XMLSchema') { + $this->xdebug("in getTypeDef, element $type is an XSD type"); + $this->elements[$type]['phpType'] = 'scalar'; + } + } + return $this->elements[$type]; + } elseif(isset($this->attributes[$type])){ + $this->xdebug("in getTypeDef, found attribute $type"); + return $this->attributes[$type]; + } elseif (ereg('_ContainedType$', $type)) { + $this->xdebug("in getTypeDef, have an untyped element $type"); + $typeDef['typeClass'] = 'simpleType'; + $typeDef['phpType'] = 'scalar'; + $typeDef['type'] = 'http://www.w3.org/2001/XMLSchema:string'; + return $typeDef; + } + $this->xdebug("in getTypeDef, did not find $type"); + return false; + } + + /** + * returns a sample serialization of a given type, or false if no type by the given name + * + * @param string $type, name of type + * @return mixed + * @access public + */ + function serializeTypeDef($type){ + //print "in sTD() for type $type
"; + if($typeDef = $this->getTypeDef($type)){ + $str .= '<'.$type; + if(is_array($typeDef['attrs'])){ + foreach($attrs as $attName => $data){ + $str .= " $attName=\"{type = ".$data['type']."}\""; + } + } + $str .= " xmlns=\"".$this->schema['targetNamespace']."\""; + if(count($typeDef['elements']) > 0){ + $str .= ">"; + foreach($typeDef['elements'] as $element => $eData){ + $str .= $this->serializeTypeDef($element); + } + $str .= ""; + } elseif($typeDef['typeClass'] == 'element') { + $str .= ">"; + } else { + $str .= "/>"; + } + return $str; + } + return false; + } + + /** + * returns HTML form elements that allow a user + * to enter values for creating an instance of the given type. + * + * @param string $name, name for type instance + * @param string $type, name of type + * @return string + * @access public + */ + function typeToForm($name,$type){ + // get typedef + if($typeDef = $this->getTypeDef($type)){ + // if struct + if($typeDef['phpType'] == 'struct'){ + $buffer .= ''; + foreach($typeDef['elements'] as $child => $childDef){ + $buffer .= " + + "; + } + $buffer .= '
$childDef[name] (type: ".$this->getLocalPart($childDef['type'])."):
'; + // if array + } elseif($typeDef['phpType'] == 'array'){ + $buffer .= ''; + for($i=0;$i < 3; $i++){ + $buffer .= " + + "; + } + $buffer .= '
array item (type: $typeDef[arrayType]):
'; + // if scalar + } else { + $buffer .= ""; + } + } else { + $buffer .= ""; + } + return $buffer; + } + + /** + * adds a complex type to the schema + * + * example: array + * + * addType( + * 'ArrayOfstring', + * 'complexType', + * 'array', + * '', + * 'SOAP-ENC:Array', + * array('ref'=>'SOAP-ENC:arrayType','wsdl:arrayType'=>'string[]'), + * 'xsd:string' + * ); + * + * example: PHP associative array ( SOAP Struct ) + * + * addType( + * 'SOAPStruct', + * 'complexType', + * 'struct', + * 'all', + * array('myVar'=> array('name'=>'myVar','type'=>'string') + * ); + * + * @param name + * @param typeClass (complexType|simpleType|attribute) + * @param phpType: currently supported are array and struct (php assoc array) + * @param compositor (all|sequence|choice) + * @param restrictionBase namespace:name (http://schemas.xmlsoap.org/soap/encoding/:Array) + * @param elements = array ( name = array(name=>'',type=>'') ) + * @param attrs = array( + * array( + * 'ref' => "http://schemas.xmlsoap.org/soap/encoding/:arrayType", + * "http://schemas.xmlsoap.org/wsdl/:arrayType" => "string[]" + * ) + * ) + * @param arrayType: namespace:name (http://www.w3.org/2001/XMLSchema:string) + * + */ + function addComplexType($name,$typeClass='complexType',$phpType='array',$compositor='',$restrictionBase='',$elements=array(),$attrs=array(),$arrayType=''){ + $this->complexTypes[$name] = array( + 'name' => $name, + 'typeClass' => $typeClass, + 'phpType' => $phpType, + 'compositor'=> $compositor, + 'restrictionBase' => $restrictionBase, + 'elements' => $elements, + 'attrs' => $attrs, + 'arrayType' => $arrayType + ); + + $this->xdebug("addComplexType $name:"); + $this->appendDebug($this->varDump($this->complexTypes[$name])); + } + + /** + * adds a simple type to the schema + * + * @param name + * @param restrictionBase namespace:name (http://schemas.xmlsoap.org/soap/encoding/:Array) + * @param typeClass (simpleType) + * @param phpType: (scalar) + * @see xmlschema + * + */ + function addSimpleType($name, $restrictionBase='', $typeClass='simpleType', $phpType='scalar') { + $this->simpleTypes[$name] = array( + 'name' => $name, + 'typeClass' => $typeClass, + 'phpType' => $phpType, + 'type' => $restrictionBase + ); + + $this->xdebug("addSimpleType $name:"); + $this->appendDebug($this->varDump($this->simpleTypes[$name])); + } + + /** + * adds an element to the schema + * + * @param name + * @param array $attrs attributes that must include name and type + * @see xmlschema + */ + function addElement($attrs) { + if (! $this->getPrefix($attrs['type'])) { + $attrs['type'] = $this->schemaTargetNamespace . ':' . $attrs['type']; + } + $this->elements[ $attrs['name'] ] = $attrs; + $this->elements[ $attrs['name'] ]['typeClass'] = 'element'; + + $this->xdebug("addElement " . $attrs['name']); + $this->appendDebug($this->varDump($this->elements[ $attrs['name'] ])); + } +} + + + +?> +* @version $Id$ +* @access public +*/ +class soapval extends nusoap_base { + /** + * constructor + * + * @param string $name optional name + * @param string $type optional type name + * @param mixed $value optional value + * @param string $namespace optional namespace of value + * @param string $type_namespace optional namespace of type + * @param array $attributes associative array of attributes to add to element serialization + * @access public + */ + function soapval($name='soapval',$type=false,$value=-1,$element_ns=false,$type_ns=false,$attributes=false) { + $this->name = $name; + $this->value = $value; + $this->type = $type; + $this->element_ns = $element_ns; + $this->type_ns = $type_ns; + $this->attributes = $attributes; + } + + /** + * return serialized value + * + * @return string XML data + * @access private + */ + function serialize($use='encoded') { + return $this->serialize_val($this->value,$this->name,$this->type,$this->element_ns,$this->type_ns,$this->attributes,$use); + } + + /** + * decodes a soapval object into a PHP native type + * + * @param object $soapval optional SOAPx4 soapval object, else uses self + * @return mixed + * @access public + */ + function decode(){ + return $this->value; + } +} + + + +?> +* @version $Id$ +* @access public +*/ +class soap_transport_http extends nusoap_base { + + var $url = ''; + var $uri = ''; + var $digest_uri = ''; + var $scheme = ''; + var $host = ''; + var $port = ''; + var $path = ''; + var $request_method = 'POST'; + var $protocol_version = '1.0'; + var $encoding = ''; + var $outgoing_headers = array(); + var $incoming_headers = array(); + var $incoming_cookies = array(); + var $outgoing_payload = ''; + var $incoming_payload = ''; + var $useSOAPAction = true; + var $persistentConnection = false; + var $ch = false; // cURL handle + var $username = ''; + var $password = ''; + var $authtype = ''; + var $digestRequest = array(); + var $certRequest = array(); // keys must be cainfofile, sslcertfile, sslkeyfile, passphrase + // cainfofile: certificate authority file, e.g. '$pathToPemFiles/rootca.pem' + // sslcertfile: SSL certificate file, e.g. '$pathToPemFiles/mycert.pem' + // sslkeyfile: SSL key file, e.g. '$pathToPemFiles/mykey.pem' + // passphrase: SSL key password/passphrase + + /** + * constructor + */ + function soap_transport_http($url){ + $this->setURL($url); + ereg('\$Revisio' . 'n: ([^ ]+)', $this->revision, $rev); + $this->outgoing_headers['User-Agent'] = $this->title.'/'.$this->version.' ('.$rev[1].')'; + } + + function setURL($url) { + $this->url = $url; + + $u = parse_url($url); + foreach($u as $k => $v){ + $this->debug("$k = $v"); + $this->$k = $v; + } + + // add any GET params to path + if(isset($u['query']) && $u['query'] != ''){ + $this->path .= '?' . $u['query']; + } + + // set default port + if(!isset($u['port'])){ + if($u['scheme'] == 'https'){ + $this->port = 443; + } else { + $this->port = 80; + } + } + + $this->uri = $this->path; + $this->digest_uri = $this->uri; + + // build headers + if (!isset($u['port'])) { + $this->outgoing_headers['Host'] = $this->host; + } else { + $this->outgoing_headers['Host'] = $this->host.':'.$this->port; + } + + if (isset($u['user']) && $u['user'] != '') { + $this->setCredentials(urldecode($u['user']), isset($u['pass']) ? urldecode($u['pass']) : ''); + } + } + + function connect($connection_timeout=0,$response_timeout=30){ + // For PHP 4.3 with OpenSSL, change https scheme to ssl, then treat like + // "regular" socket. + // TODO: disabled for now because OpenSSL must be *compiled* in (not just + // loaded), and until PHP5 stream_get_wrappers is not available. +// if ($this->scheme == 'https') { +// if (version_compare(phpversion(), '4.3.0') >= 0) { +// if (extension_loaded('openssl')) { +// $this->scheme = 'ssl'; +// $this->debug('Using SSL over OpenSSL'); +// } +// } +// } + $this->debug("connect connection_timeout $connection_timeout, response_timeout $response_timeout, scheme $this->scheme, host $this->host, port $this->port"); + if ($this->scheme == 'http' || $this->scheme == 'ssl') { + // use persistent connection + if($this->persistentConnection && isset($this->fp) && is_resource($this->fp)){ + if (!feof($this->fp)) { + $this->debug('Re-use persistent connection'); + return true; + } + fclose($this->fp); + $this->debug('Closed persistent connection at EOF'); + } + + // munge host if using OpenSSL + if ($this->scheme == 'ssl') { + $host = 'ssl://' . $this->host; + } else { + $host = $this->host; + } + $this->debug('calling fsockopen with host ' . $host); + + // open socket + if($connection_timeout > 0){ + $this->fp = @fsockopen( $host, $this->port, $this->errno, $this->error_str, $connection_timeout); + } else { + $this->fp = @fsockopen( $host, $this->port, $this->errno, $this->error_str); + } + + // test pointer + if(!$this->fp) { + $msg = 'Couldn\'t open socket connection to server ' . $this->url; + if ($this->errno) { + $msg .= ', Error ('.$this->errno.'): '.$this->error_str; + } else { + $msg .= ' prior to connect(). This is often a problem looking up the host name.'; + } + $this->debug($msg); + $this->setError($msg); + return false; + } + + // set response timeout + socket_set_timeout( $this->fp, $response_timeout); + + $this->debug('socket connected'); + return true; + } else if ($this->scheme == 'https') { + if (!extension_loaded('curl')) { + $this->setError('CURL Extension, or OpenSSL extension w/ PHP version >= 4.3 is required for HTTPS'); + return false; + } + $this->debug('connect using https'); + // init CURL + $this->ch = curl_init(); + // set url + $hostURL = ($this->port != '') ? "https://$this->host:$this->port" : "https://$this->host"; + // add path + $hostURL .= $this->path; + curl_setopt($this->ch, CURLOPT_URL, $hostURL); + // follow location headers (re-directs) + curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, 1); + // ask for headers in the response output + curl_setopt($this->ch, CURLOPT_HEADER, 1); + // ask for the response output as the return value + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); + // encode + // We manage this ourselves through headers and encoding +// if(function_exists('gzuncompress')){ +// curl_setopt($this->ch, CURLOPT_ENCODING, 'deflate'); +// } + // persistent connection + if ($this->persistentConnection) { + // The way we send data, we cannot use persistent connections, since + // there will be some "junk" at the end of our request. + //curl_setopt($this->ch, CURL_HTTP_VERSION_1_1, true); + $this->persistentConnection = false; + $this->outgoing_headers['Connection'] = 'close'; + } + // set timeout (NOTE: cURL does not have separate connection and response timeouts) + if ($connection_timeout != 0) { + curl_setopt($this->ch, CURLOPT_TIMEOUT, $connection_timeout); + } + + // recent versions of cURL turn on peer/host checking by default, + // while PHP binaries are not compiled with a default location for the + // CA cert bundle, so disable peer/host checking. +//curl_setopt($this->ch, CURLOPT_CAINFO, 'f:\php-4.3.2-win32\extensions\curl-ca-bundle.crt'); + curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, 0); + + // support client certificates (thanks Tobias Boes) + if ($this->authtype == 'certificate') { + curl_setopt($this->ch, CURLOPT_CAINFO, $certRequest['cainfofile']); + curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, 1); + curl_setopt($this->ch, CURLOPT_SSLCERT, $certRequest['sslcertfile']); + curl_setopt($this->ch, CURLOPT_SSLKEY, $certRequest['sslkeyfile']); + curl_setopt($this->ch, CURLOPT_SSLKEYPASSWD , $certRequest['passphrase']); + } + $this->debug('cURL connection set up'); + return true; + } else { + $this->setError('Unknown scheme ' . $this->scheme); + $this->debug('Unknown scheme ' . $this->scheme); + return false; + } + } + + /** + * send the SOAP message via HTTP + * + * @param string $data message data + * @param integer $timeout set connection timeout in seconds + * @param integer $response_timeout set response timeout in seconds + * @param array $cookies cookies to send + * @return string data + * @access public + */ + function send($data, $timeout=0, $response_timeout=30, $cookies=NULL) { + + $this->debug('entered send() with data of length: '.strlen($data)); + + $this->tryagain = true; + $tries = 0; + while ($this->tryagain) { + $this->tryagain = false; + if ($tries++ < 2) { + // make connnection + if (!$this->connect($timeout, $response_timeout)){ + return false; + } + + // send request + if (!$this->sendRequest($data, $cookies)){ + return false; + } + + // get response + $respdata = $this->getResponse(); + } else { + $this->setError('Too many tries to get an OK response'); + } + } + $this->debug('end of send()'); + return $respdata; + } + + + /** + * send the SOAP message via HTTPS 1.0 using CURL + * + * @param string $msg message data + * @param integer $timeout set connection timeout in seconds + * @param integer $response_timeout set response timeout in seconds + * @param array $cookies cookies to send + * @return string data + * @access public + */ + function sendHTTPS($data, $timeout=0, $response_timeout=30, $cookies) { + return $this->send($data, $timeout, $response_timeout, $cookies); + } + + /** + * if authenticating, set user credentials here + * + * @param string $username + * @param string $password + * @param string $authtype (basic, digest, certificate) + * @param array $digestRequest (keys must be nonce, nc, realm, qop) + * @param array $certRequest (keys must be cainfofile, sslcertfile, sslkeyfile, passphrase: see corresponding options in cURL docs) + * @access public + */ + function setCredentials($username, $password, $authtype = 'basic', $digestRequest = array(), $certRequest = array()) { + global $_SERVER; + + $this->debug("Set credentials for authtype $authtype"); + // cf. RFC 2617 + if ($authtype == 'basic') { + $this->outgoing_headers['Authorization'] = 'Basic '.base64_encode(str_replace(':','',$username).':'.$password); + } elseif ($authtype == 'digest') { + if (isset($digestRequest['nonce'])) { + $digestRequest['nc'] = isset($digestRequest['nc']) ? $digestRequest['nc']++ : 1; + + // calculate the Digest hashes (calculate code based on digest implementation found at: http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html) + + // A1 = unq(username-value) ":" unq(realm-value) ":" passwd + $A1 = $username. ':' . $digestRequest['realm'] . ':' . $password; + + // H(A1) = MD5(A1) + $HA1 = md5($A1); + + // A2 = Method ":" digest-uri-value + $A2 = 'POST:' . $this->digest_uri; + + // H(A2) + $HA2 = md5($A2); + + // KD(secret, data) = H(concat(secret, ":", data)) + // if qop == auth: + // request-digest = <"> < KD ( H(A1), unq(nonce-value) + // ":" nc-value + // ":" unq(cnonce-value) + // ":" unq(qop-value) + // ":" H(A2) + // ) <"> + // if qop is missing, + // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> + + $unhashedDigest = ''; + $nonce = isset($digestRequest['nonce']) ? $digestRequest['nonce'] : ''; + $cnonce = $nonce; + if ($digestRequest['qop'] != '') { + $unhashedDigest = $HA1 . ':' . $nonce . ':' . sprintf("%08d", $digestRequest['nc']) . ':' . $cnonce . ':' . $digestRequest['qop'] . ':' . $HA2; + } else { + $unhashedDigest = $HA1 . ':' . $nonce . ':' . $HA2; + } + + $hashedDigest = md5($unhashedDigest); + + $this->outgoing_headers['Authorization'] = 'Digest username="' . $username . '", realm="' . $digestRequest['realm'] . '", nonce="' . $nonce . '", uri="' . $this->digest_uri . '", cnonce="' . $cnonce . '", nc=' . sprintf("%08x", $digestRequest['nc']) . ', qop="' . $digestRequest['qop'] . '", response="' . $hashedDigest . '"'; + } + } elseif ($authtype == 'certificate') { + $this->certRequest = $certRequest; + } + $this->username = $username; + $this->password = $password; + $this->authtype = $authtype; + $this->digestRequest = $digestRequest; + + if (isset($this->outgoing_headers['Authorization'])) { + $this->debug('Authorization header set: ' . substr($this->outgoing_headers['Authorization'], 0, 12) . '...'); + } else { + $this->debug('Authorization header not set'); + } + } + + /** + * set the soapaction value + * + * @param string $soapaction + * @access public + */ + function setSOAPAction($soapaction) { + $this->outgoing_headers['SOAPAction'] = '"' . $soapaction . '"'; + } + + /** + * use http encoding + * + * @param string $enc encoding style. supported values: gzip, deflate, or both + * @access public + */ + function setEncoding($enc='gzip, deflate'){ + $this->protocol_version = '1.1'; + $this->outgoing_headers['Accept-Encoding'] = $enc; + if (!isset($this->outgoing_headers['Connection'])) { + $this->outgoing_headers['Connection'] = 'close'; + $this->persistentConnection = false; + } + set_magic_quotes_runtime(0); + // deprecated + $this->encoding = $enc; + } + + /** + * set proxy info here + * + * @param string $proxyhost + * @param string $proxyport + * @param string $proxyusername + * @param string $proxypassword + * @access public + */ + function setProxy($proxyhost, $proxyport, $proxyusername = '', $proxypassword = '') { + $this->uri = $this->url; + $this->host = $proxyhost; + $this->port = $proxyport; + if ($proxyusername != '' && $proxypassword != '') { + $this->outgoing_headers['Proxy-Authorization'] = ' Basic '.base64_encode($proxyusername.':'.$proxypassword); + } + } + + /** + * decode a string that is encoded w/ "chunked' transfer encoding + * as defined in RFC2068 19.4.6 + * + * @param string $buffer + * @param string $lb + * @returns string + * @access public + * @deprecated + */ + function decodeChunked($buffer, $lb){ + // length := 0 + $length = 0; + $new = ''; + + // read chunk-size, chunk-extension (if any) and CRLF + // get the position of the linebreak + $chunkend = strpos($buffer, $lb); + if ($chunkend == FALSE) { + $this->debug('no linebreak found in decodeChunked'); + return $new; + } + $temp = substr($buffer,0,$chunkend); + $chunk_size = hexdec( trim($temp) ); + $chunkstart = $chunkend + strlen($lb); + // while (chunk-size > 0) { + while ($chunk_size > 0) { + $this->debug("chunkstart: $chunkstart chunk_size: $chunk_size"); + $chunkend = strpos( $buffer, $lb, $chunkstart + $chunk_size); + + // Just in case we got a broken connection + if ($chunkend == FALSE) { + $chunk = substr($buffer,$chunkstart); + // append chunk-data to entity-body + $new .= $chunk; + $length += strlen($chunk); + break; + } + + // read chunk-data and CRLF + $chunk = substr($buffer,$chunkstart,$chunkend-$chunkstart); + // append chunk-data to entity-body + $new .= $chunk; + // length := length + chunk-size + $length += strlen($chunk); + // read chunk-size and CRLF + $chunkstart = $chunkend + strlen($lb); + + $chunkend = strpos($buffer, $lb, $chunkstart) + strlen($lb); + if ($chunkend == FALSE) { + break; //Just in case we got a broken connection + } + $temp = substr($buffer,$chunkstart,$chunkend-$chunkstart); + $chunk_size = hexdec( trim($temp) ); + $chunkstart = $chunkend; + } + return $new; + } + + /* + * Writes payload, including HTTP headers, to $this->outgoing_payload. + */ + function buildPayload($data, $cookie_str = '') { + // add content-length header + $this->outgoing_headers['Content-Length'] = strlen($data); + + // start building outgoing payload: + $req = "$this->request_method $this->uri HTTP/$this->protocol_version"; + $this->debug("HTTP request: $req"); + $this->outgoing_payload = "$req\r\n"; + + // loop thru headers, serializing + foreach($this->outgoing_headers as $k => $v){ + $hdr = $k.': '.$v; + $this->debug("HTTP header: $hdr"); + $this->outgoing_payload .= "$hdr\r\n"; + } + + // add any cookies + if ($cookie_str != '') { + $hdr = 'Cookie: '.$cookie_str; + $this->debug("HTTP header: $hdr"); + $this->outgoing_payload .= "$hdr\r\n"; + } + + // header/body separator + $this->outgoing_payload .= "\r\n"; + + // add data + $this->outgoing_payload .= $data; + } + + function sendRequest($data, $cookies = NULL) { + // build cookie string + $cookie_str = $this->getCookiesForRequest($cookies, (($this->scheme == 'ssl') || ($this->scheme == 'https'))); + + // build payload + $this->buildPayload($data, $cookie_str); + + if ($this->scheme == 'http' || $this->scheme == 'ssl') { + // send payload + if(!fputs($this->fp, $this->outgoing_payload, strlen($this->outgoing_payload))) { + $this->setError('couldn\'t write message data to socket'); + $this->debug('couldn\'t write message data to socket'); + return false; + } + $this->debug('wrote data to socket, length = ' . strlen($this->outgoing_payload)); + return true; + } else if ($this->scheme == 'https') { + // set payload + // TODO: cURL does say this should only be the verb, and in fact it + // turns out that the URI and HTTP version are appended to this, which + // some servers refuse to work with + //curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $this->outgoing_payload); + foreach($this->outgoing_headers as $k => $v){ + $curl_headers[] = "$k: $v"; + } + if ($cookie_str != '') { + $curl_headers[] = 'Cookie: ' . $cookie_str; + } + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $curl_headers); + if ($this->request_method == "POST") { + curl_setopt($this->ch, CURLOPT_POST, 1); + curl_setopt($this->ch, CURLOPT_POSTFIELDS, $data); + } else { + } + $this->debug('set cURL payload'); + return true; + } + } + + function getResponse(){ + $this->incoming_payload = ''; + + if ($this->scheme == 'http' || $this->scheme == 'ssl') { + // loop until headers have been retrieved + $data = ''; + while (!isset($lb)){ + + // We might EOF during header read. + if(feof($this->fp)) { + $this->incoming_payload = $data; + $this->debug('found no headers before EOF after length ' . strlen($data)); + $this->debug("received before EOF:\n" . $data); + $this->setError('server failed to send headers'); + return false; + } + + $tmp = fgets($this->fp, 256); + $tmplen = strlen($tmp); + $this->debug("read line of $tmplen bytes: " . trim($tmp)); + + if ($tmplen == 0) { + $this->incoming_payload = $data; + $this->debug('socket read of headers timed out after length ' . strlen($data)); + $this->debug("read before timeout:\n" . $data); + $this->setError('socket read of headers timed out'); + return false; + } + + $data .= $tmp; + $pos = strpos($data,"\r\n\r\n"); + if($pos > 1){ + $lb = "\r\n"; + } else { + $pos = strpos($data,"\n\n"); + if($pos > 1){ + $lb = "\n"; + } + } + // remove 100 header + if(isset($lb) && ereg('^HTTP/1.1 100',$data)){ + unset($lb); + $data = ''; + }// + } + // store header data + $this->incoming_payload .= $data; + $this->debug('found end of headers after length ' . strlen($data)); + // process headers + $header_data = trim(substr($data,0,$pos)); + $header_array = explode($lb,$header_data); + $this->incoming_headers = array(); + $this->incoming_cookies = array(); + foreach($header_array as $header_line){ + $arr = explode(':',$header_line, 2); + if(count($arr) > 1){ + $header_name = strtolower(trim($arr[0])); + $this->incoming_headers[$header_name] = trim($arr[1]); + if ($header_name == 'set-cookie') { + // TODO: allow multiple cookies from parseCookie + $cookie = $this->parseCookie(trim($arr[1])); + if ($cookie) { + $this->incoming_cookies[] = $cookie; + $this->debug('found cookie: ' . $cookie['name'] . ' = ' . $cookie['value']); + } else { + $this->debug('did not find cookie in ' . trim($arr[1])); + } + } + } else if (isset($header_name)) { + // append continuation line to previous header + $this->incoming_headers[$header_name] .= $lb . ' ' . $header_line; + } + } + + // loop until msg has been received + if (isset($this->incoming_headers['transfer-encoding']) && strtolower($this->incoming_headers['transfer-encoding']) == 'chunked') { + $content_length = 2147483647; // ignore any content-length header + $chunked = true; + $this->debug("want to read chunked content"); + } elseif (isset($this->incoming_headers['content-length'])) { + $content_length = $this->incoming_headers['content-length']; + $chunked = false; + $this->debug("want to read content of length $content_length"); + } else { + $content_length = 2147483647; + $chunked = false; + $this->debug("want to read content to EOF"); + } + $data = ''; + do { + if ($chunked) { + $tmp = fgets($this->fp, 256); + $tmplen = strlen($tmp); + $this->debug("read chunk line of $tmplen bytes"); + if ($tmplen == 0) { + $this->incoming_payload = $data; + $this->debug('socket read of chunk length timed out after length ' . strlen($data)); + $this->debug("read before timeout:\n" . $data); + $this->setError('socket read of chunk length timed out'); + return false; + } + $content_length = hexdec(trim($tmp)); + $this->debug("chunk length $content_length"); + } + $strlen = 0; + while (($strlen < $content_length) && (!feof($this->fp))) { + $readlen = min(8192, $content_length - $strlen); + $tmp = fread($this->fp, $readlen); + $tmplen = strlen($tmp); + $this->debug("read buffer of $tmplen bytes"); + if (($tmplen == 0) && (!feof($this->fp))) { + $this->incoming_payload = $data; + $this->debug('socket read of body timed out after length ' . strlen($data)); + $this->debug("read before timeout:\n" . $data); + $this->setError('socket read of body timed out'); + return false; + } + $strlen += $tmplen; + $data .= $tmp; + } + if ($chunked && ($content_length > 0)) { + $tmp = fgets($this->fp, 256); + $tmplen = strlen($tmp); + $this->debug("read chunk terminator of $tmplen bytes"); + if ($tmplen == 0) { + $this->incoming_payload = $data; + $this->debug('socket read of chunk terminator timed out after length ' . strlen($data)); + $this->debug("read before timeout:\n" . $data); + $this->setError('socket read of chunk terminator timed out'); + return false; + } + } + } while ($chunked && ($content_length > 0) && (!feof($this->fp))); + if (feof($this->fp)) { + $this->debug('read to EOF'); + } + $this->debug('read body of length ' . strlen($data)); + $this->incoming_payload .= $data; + $this->debug('received a total of '.strlen($this->incoming_payload).' bytes of data from server'); + + // close filepointer + if( + (isset($this->incoming_headers['connection']) && strtolower($this->incoming_headers['connection']) == 'close') || + (! $this->persistentConnection) || feof($this->fp)){ + fclose($this->fp); + $this->fp = false; + $this->debug('closed socket'); + } + + // connection was closed unexpectedly + if($this->incoming_payload == ''){ + $this->setError('no response from server'); + return false; + } + + // decode transfer-encoding +// if(isset($this->incoming_headers['transfer-encoding']) && strtolower($this->incoming_headers['transfer-encoding']) == 'chunked'){ +// if(!$data = $this->decodeChunked($data, $lb)){ +// $this->setError('Decoding of chunked data failed'); +// return false; +// } + //print "
\nde-chunked:\n---------------\n$data\n\n---------------\n
"; + // set decoded payload +// $this->incoming_payload = $header_data.$lb.$lb.$data; +// } + + } else if ($this->scheme == 'https') { + // send and receive + $this->debug('send and receive with cURL'); + $this->incoming_payload = curl_exec($this->ch); + $data = $this->incoming_payload; + + $cErr = curl_error($this->ch); + if ($cErr != '') { + $err = 'cURL ERROR: '.curl_errno($this->ch).': '.$cErr.'
'; + foreach(curl_getinfo($this->ch) as $k => $v){ + $err .= "$k: $v
"; + } + $this->debug($err); + $this->setError($err); + curl_close($this->ch); + return false; + } else { + //echo '
';
+			//var_dump(curl_getinfo($this->ch));
+			//echo '
'; + } + // close curl + $this->debug('No cURL error, closing cURL'); + curl_close($this->ch); + + // remove 100 header + if (ereg('^HTTP/1.1 100',$data)) { + if ($pos = strpos($data,"\r\n\r\n")) { + $data = ltrim(substr($data,$pos)); + } elseif($pos = strpos($data,"\n\n") ) { + $data = ltrim(substr($data,$pos)); + } + } + + // separate content from HTTP headers + if ($pos = strpos($data,"\r\n\r\n")) { + $lb = "\r\n"; + } elseif( $pos = strpos($data,"\n\n")) { + $lb = "\n"; + } else { + $this->debug('no proper separation of headers and document'); + $this->setError('no proper separation of headers and document'); + return false; + } + $header_data = trim(substr($data,0,$pos)); + $header_array = explode($lb,$header_data); + $data = ltrim(substr($data,$pos)); + $this->debug('found proper separation of headers and document'); + $this->debug('cleaned data, stringlen: '.strlen($data)); + // clean headers + foreach ($header_array as $header_line) { + $arr = explode(':',$header_line,2); + if(count($arr) > 1){ + $header_name = strtolower(trim($arr[0])); + $this->incoming_headers[$header_name] = trim($arr[1]); + if ($header_name == 'set-cookie') { + // TODO: allow multiple cookies from parseCookie + $cookie = $this->parseCookie(trim($arr[1])); + if ($cookie) { + $this->incoming_cookies[] = $cookie; + $this->debug('found cookie: ' . $cookie['name'] . ' = ' . $cookie['value']); + } else { + $this->debug('did not find cookie in ' . trim($arr[1])); + } + } + } else if (isset($header_name)) { + // append continuation line to previous header + $this->incoming_headers[$header_name] .= $lb . ' ' . $header_line; + } + } + } + + $arr = explode(' ', $header_array[0], 3); + $http_version = $arr[0]; + $http_status = intval($arr[1]); + $http_reason = count($arr) > 2 ? $arr[2] : ''; + + // see if we need to resend the request with http digest authentication + if (isset($this->incoming_headers['location']) && $http_status == 301) { + $this->debug("Got 301 $http_reason with Location: " . $this->incoming_headers['location']); + $this->setURL($this->incoming_headers['location']); + $this->tryagain = true; + return false; + } + + // see if we need to resend the request with http digest authentication + if (isset($this->incoming_headers['www-authenticate']) && $http_status == 401) { + $this->debug("Got 401 $http_reason with WWW-Authenticate: " . $this->incoming_headers['www-authenticate']); + if (substr("Digest ", $this->incoming_headers['www-authenticate'])) { + $this->debug('Server wants digest authentication'); + // remove "Digest " from our elements + $digestString = str_replace('Digest ', '', $this->incoming_headers['www-authenticate']); + + // parse elements into array + $digestElements = explode(',', $digestString); + foreach ($digestElements as $val) { + $tempElement = explode('=', trim($val), 2); + $digestRequest[$tempElement[0]] = str_replace("\"", '', $tempElement[1]); + } + + // should have (at least) qop, realm, nonce + if (isset($digestRequest['nonce'])) { + $this->setCredentials($this->username, $this->password, 'digest', $digestRequest); + $this->tryagain = true; + return false; + } + } + $this->debug('HTTP authentication failed'); + $this->setError('HTTP authentication failed'); + return false; + } + + if ( + ($http_status >= 300 && $http_status <= 307) || + ($http_status >= 400 && $http_status <= 417) || + ($http_status >= 501 && $http_status <= 505) + ) { + $this->setError("Unsupported HTTP response status $http_status $http_reason (soap_client->response has contents of the response)"); + return false; + } + + // decode content-encoding + if(isset($this->incoming_headers['content-encoding']) && $this->incoming_headers['content-encoding'] != ''){ + if(strtolower($this->incoming_headers['content-encoding']) == 'deflate' || strtolower($this->incoming_headers['content-encoding']) == 'gzip'){ + // if decoding works, use it. else assume data wasn't gzencoded + if(function_exists('gzinflate')){ + //$timer->setMarker('starting decoding of gzip/deflated content'); + // IIS 5 requires gzinflate instead of gzuncompress (similar to IE 5 and gzdeflate v. gzcompress) + // this means there are no Zlib headers, although there should be + $this->debug('The gzinflate function exists'); + $datalen = strlen($data); + if ($this->incoming_headers['content-encoding'] == 'deflate') { + if ($degzdata = @gzinflate($data)) { + $data = $degzdata; + $this->debug('The payload has been inflated to ' . strlen($data) . ' bytes'); + if (strlen($data) < $datalen) { + // test for the case that the payload has been compressed twice + $this->debug('The inflated payload is smaller than the gzipped one; try again'); + if ($degzdata = @gzinflate($data)) { + $data = $degzdata; + $this->debug('The payload has been inflated again to ' . strlen($data) . ' bytes'); + } + } + } else { + $this->debug('Error using gzinflate to inflate the payload'); + $this->setError('Error using gzinflate to inflate the payload'); + } + } elseif ($this->incoming_headers['content-encoding'] == 'gzip') { + if ($degzdata = @gzinflate(substr($data, 10))) { // do our best + $data = $degzdata; + $this->debug('The payload has been un-gzipped to ' . strlen($data) . ' bytes'); + if (strlen($data) < $datalen) { + // test for the case that the payload has been compressed twice + $this->debug('The un-gzipped payload is smaller than the gzipped one; try again'); + if ($degzdata = @gzinflate(substr($data, 10))) { + $data = $degzdata; + $this->debug('The payload has been un-gzipped again to ' . strlen($data) . ' bytes'); + } + } + } else { + $this->debug('Error using gzinflate to un-gzip the payload'); + $this->setError('Error using gzinflate to un-gzip the payload'); + } + } + //$timer->setMarker('finished decoding of gzip/deflated content'); + //print "\nde-inflated:\n---------------\n$data\n-------------\n"; + // set decoded payload + $this->incoming_payload = $header_data.$lb.$lb.$data; + } else { + $this->debug('The server sent compressed data. Your php install must have the Zlib extension compiled in to support this.'); + $this->setError('The server sent compressed data. Your php install must have the Zlib extension compiled in to support this.'); + } + } else { + $this->debug('Unsupported Content-Encoding ' . $this->incoming_headers['content-encoding']); + $this->setError('Unsupported Content-Encoding ' . $this->incoming_headers['content-encoding']); + } + } else { + $this->debug('No Content-Encoding header'); + } + + if(strlen($data) == 0){ + $this->debug('no data after headers!'); + $this->setError('no data present after HTTP headers'); + return false; + } + + return $data; + } + + function setContentType($type, $charset = false) { + $this->outgoing_headers['Content-Type'] = $type . ($charset ? '; charset=' . $charset : ''); + } + + function usePersistentConnection(){ + if (isset($this->outgoing_headers['Accept-Encoding'])) { + return false; + } + $this->protocol_version = '1.1'; + $this->persistentConnection = true; + $this->outgoing_headers['Connection'] = 'Keep-Alive'; + return true; + } + + /** + * parse an incoming Cookie into it's parts + * + * @param string $cookie_str content of cookie + * @return array with data of that cookie + * @access private + * + * TODO: allow a Set-Cookie string to be parsed into multiple cookies + */ + function parseCookie($cookie_str) { + $cookie_str = str_replace('; ', ';', $cookie_str) . ';'; + $data = split(';', $cookie_str); + $value_str = $data[0]; + + $cookie_param = 'domain='; + $start = strpos($cookie_str, $cookie_param); + if ($start > 0) { + $domain = substr($cookie_str, $start + strlen($cookie_param)); + $domain = substr($domain, 0, strpos($domain, ';')); + } else { + $domain = ''; + } + + $cookie_param = 'expires='; + $start = strpos($cookie_str, $cookie_param); + if ($start > 0) { + $expires = substr($cookie_str, $start + strlen($cookie_param)); + $expires = substr($expires, 0, strpos($expires, ';')); + } else { + $expires = ''; + } + + $cookie_param = 'path='; + $start = strpos($cookie_str, $cookie_param); + if ( $start > 0 ) { + $path = substr($cookie_str, $start + strlen($cookie_param)); + $path = substr($path, 0, strpos($path, ';')); + } else { + $path = '/'; + } + + $cookie_param = ';secure;'; + if (strpos($cookie_str, $cookie_param) !== FALSE) { + $secure = true; + } else { + $secure = false; + } + + $sep_pos = strpos($value_str, '='); + + if ($sep_pos) { + $name = substr($value_str, 0, $sep_pos); + $value = substr($value_str, $sep_pos + 1); + $cookie= array( 'name' => $name, + 'value' => $value, + 'domain' => $domain, + 'path' => $path, + 'expires' => $expires, + 'secure' => $secure + ); + return $cookie; + } + return false; + } + + /** + * sort out cookies for the current request + * + * @param array $cookies array with all cookies + * @param boolean $secure is the send-content secure or not? + * @return string for Cookie-HTTP-Header + * @access private + */ + function getCookiesForRequest($cookies, $secure=false) { + $cookie_str = ''; + if ((! is_null($cookies)) && (is_array($cookies))) { + foreach ($cookies as $cookie) { + if (! is_array($cookie)) { + continue; + } + $this->debug("check cookie for validity: ".$cookie['name'].'='.$cookie['value']); + if ((isset($cookie['expires'])) && (! empty($cookie['expires']))) { + if (strtotime($cookie['expires']) <= time()) { + $this->debug('cookie has expired'); + continue; + } + } + if ((isset($cookie['domain'])) && (! empty($cookie['domain']))) { + $domain = preg_quote($cookie['domain']); + if (! preg_match("'.*$domain$'i", $this->host)) { + $this->debug('cookie has different domain'); + continue; + } + } + if ((isset($cookie['path'])) && (! empty($cookie['path']))) { + $path = preg_quote($cookie['path']); + if (! preg_match("'^$path.*'i", $this->path)) { + $this->debug('cookie is for a different path'); + continue; + } + } + if ((! $secure) && (isset($cookie['secure'])) && ($cookie['secure'])) { + $this->debug('cookie is secure, transport is not'); + continue; + } + $cookie_str .= $cookie['name'] . '=' . $cookie['value'] . '; '; + $this->debug('add cookie to Cookie-String: ' . $cookie['name'] . '=' . $cookie['value']); + } + } + return $cookie_str; + } +} + +?> +* @version $Id$ +* @access public +*/ +class soap_server extends nusoap_base { + var $headers = array(); // HTTP headers of request + var $request = ''; // HTTP request + var $requestHeaders = ''; // SOAP headers from request (incomplete namespace resolution; special characters not escaped) (text) + var $document = ''; // SOAP body request portion (incomplete namespace resolution; special characters not escaped) (text) + var $requestSOAP = ''; // SOAP payload for request (text) + var $methodURI = ''; // requested method namespace URI + var $methodname = ''; // name of method requested + var $methodparams = array(); // method parameters from request + var $xml_encoding = ''; // character set encoding of incoming (request) messages + var $SOAPAction = ''; // SOAP Action from request + var $decode_utf8 = true; // toggles whether the parser decodes element content w/ utf8_decode() + + var $outgoing_headers = array();// HTTP headers of response + var $response = ''; // HTTP response + var $responseHeaders = ''; // SOAP headers for response (text) + var $responseSOAP = ''; // SOAP payload for response (text) + var $methodreturn = false; // method return to place in response + var $methodreturnisliteralxml = false; // whether $methodreturn is a string of literal XML + var $fault = false; // SOAP fault for response + var $result = 'successful'; // text indication of result (for debugging) + + var $operations = array(); // assoc array of operations => opData + var $wsdl = false; // wsdl instance + var $externalWSDLURL = false; // URL for WSDL + var $debug_flag = false; // whether to append debug to response as XML comment + + + /** + * constructor + * the optional parameter is a path to a WSDL file that you'd like to bind the server instance to. + * + * @param mixed $wsdl file path or URL (string), or wsdl instance (object) + * @access public + */ + function soap_server($wsdl=false){ + + // turn on debugging? + global $debug; + global $_REQUEST; + global $_SERVER; + global $HTTP_SERVER_VARS; + + if (isset($debug)) { + $this->debug_flag = $debug; + } else if (isset($_REQUEST['debug'])) { + $this->debug_flag = $_REQUEST['debug']; + } else if (isset($_SERVER['QUERY_STRING'])) { + $qs = explode('&', $_SERVER['QUERY_STRING']); + foreach ($qs as $v) { + if (substr($v, 0, 6) == 'debug=') { + $this->debug_flag = substr($v, 6); + } + } + } else if (isset($HTTP_SERVER_VARS['QUERY_STRING'])) { + $qs = explode('&', $HTTP_SERVER_VARS['QUERY_STRING']); + foreach ($qs as $v) { + if (substr($v, 0, 6) == 'debug=') { + $this->debug_flag = substr($v, 6); + } + } + } + + // wsdl + if($wsdl){ + if (is_object($wsdl) && is_a($wsdl, 'wsdl')) { + $this->wsdl = $wsdl; + $this->externalWSDLURL = $this->wsdl->wsdl; + $this->debug('Use existing wsdl instance from ' . $this->externalWSDLURL); + } else { + $this->debug('Create wsdl from ' . $wsdl); + $this->wsdl = new wsdl($wsdl); + $this->externalWSDLURL = $wsdl; + } + $this->appendDebug($this->wsdl->getDebug()); + $this->wsdl->clearDebug(); + if($err = $this->wsdl->getError()){ + die('WSDL ERROR: '.$err); + } + } + } + + /** + * processes request and returns response + * + * @param string $data usually is the value of $HTTP_RAW_POST_DATA + * @access public + */ + function service($data){ + global $QUERY_STRING; + global $_SERVER; + + if(isset($_SERVER['QUERY_STRING'])){ + $qs = $_SERVER['QUERY_STRING']; + } elseif(isset($GLOBALS['QUERY_STRING'])){ + $qs = $GLOBALS['QUERY_STRING']; + } elseif(isset($QUERY_STRING) && $QUERY_STRING != ''){ + $qs = $QUERY_STRING; + } + + if(isset($qs) && ereg('wsdl', $qs) ){ + // This is a request for WSDL + if($this->externalWSDLURL){ + if (strpos($this->externalWSDLURL,"://")!==false) { // assume URL + header('Location: '.$this->externalWSDLURL); + } else { // assume file + header("Content-Type: text/xml\r\n"); + $fp = fopen($this->externalWSDLURL, 'r'); + fpassthru($fp); + } + } elseif ($this->wsdl) { + header("Content-Type: text/xml; charset=ISO-8859-1\r\n"); + print $this->wsdl->serialize($this->debug_flag); + } else { + header("Content-Type: text/html; charset=ISO-8859-1\r\n"); + print "This service does not provide WSDL"; + } + } elseif ($data == '' && $this->wsdl) { + // print web interface + print $this->wsdl->webDescription(); + } else { + // handle the request + $this->parse_request($data); + if (! $this->fault) { + $this->invoke_method(); + } + if (! $this->fault) { + $this->serialize_return(); + } + $this->send_response(); + } + } + + /** + * parses HTTP request headers. + * + * The following fields are set by this function (when successful) + * + * headers + * request + * xml_encoding + * SOAPAction + * + * @access private + */ + function parse_http_headers() { + global $HTTP_SERVER_VARS; + global $_SERVER; + + $this->request = ''; + if(function_exists('getallheaders')){ + $this->headers = getallheaders(); + foreach($this->headers as $k=>$v){ + $this->request .= "$k: $v\r\n"; + $this->debug("$k: $v"); + } + // get SOAPAction header + if(isset($this->headers['SOAPAction'])){ + $this->SOAPAction = str_replace('"','',$this->headers['SOAPAction']); + } + // get the character encoding of the incoming request + if(isset($this->headers['Content-Type']) && strpos($this->headers['Content-Type'],'=')){ + $enc = str_replace('"','',substr(strstr($this->headers["Content-Type"],'='),1)); + if(eregi('^(ISO-8859-1|US-ASCII|UTF-8)$',$enc)){ + $this->xml_encoding = strtoupper($enc); + } else { + $this->xml_encoding = 'US-ASCII'; + } + } else { + // should be US-ASCII for HTTP 1.0 or ISO-8859-1 for HTTP 1.1 + $this->xml_encoding = 'ISO-8859-1'; + } + } elseif(isset($_SERVER) && is_array($_SERVER)){ + foreach ($_SERVER as $k => $v) { + if (substr($k, 0, 5) == 'HTTP_') { + $k = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($k, 5))))); + } else { + $k = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $k)))); + } + if ($k == 'Soapaction') { + // get SOAPAction header + $k = 'SOAPAction'; + $v = str_replace('"', '', $v); + $v = str_replace('\\', '', $v); + $this->SOAPAction = $v; + } else if ($k == 'Content-Type') { + // get the character encoding of the incoming request + if (strpos($v, '=')) { + $enc = substr(strstr($v, '='), 1); + $enc = str_replace('"', '', $enc); + $enc = str_replace('\\', '', $enc); + if (eregi('^(ISO-8859-1|US-ASCII|UTF-8)$', $enc)) { + $this->xml_encoding = strtoupper($enc); + } else { + $this->xml_encoding = 'US-ASCII'; + } + } else { + // should be US-ASCII for HTTP 1.0 or ISO-8859-1 for HTTP 1.1 + $this->xml_encoding = 'ISO-8859-1'; + } + } + $this->headers[$k] = $v; + $this->request .= "$k: $v\r\n"; + $this->debug("$k: $v"); + } + } elseif (is_array($HTTP_SERVER_VARS)) { + foreach ($HTTP_SERVER_VARS as $k => $v) { + if (substr($k, 0, 5) == 'HTTP_') { + $k = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($k, 5))))); + if ($k == 'Soapaction') { + // get SOAPAction header + $k = 'SOAPAction'; + $v = str_replace('"', '', $v); + $v = str_replace('\\', '', $v); + $this->SOAPAction = $v; + } else if ($k == 'Content-Type') { + // get the character encoding of the incoming request + if (strpos($v, '=')) { + $enc = substr(strstr($v, '='), 1); + $enc = str_replace('"', '', $enc); + $enc = str_replace('\\', '', $enc); + if (eregi('^(ISO-8859-1|US-ASCII|UTF-8)$', $enc)) { + $this->xml_encoding = strtoupper($enc); + } else { + $this->xml_encoding = 'US-ASCII'; + } + } else { + // should be US-ASCII for HTTP 1.0 or ISO-8859-1 for HTTP 1.1 + $this->xml_encoding = 'ISO-8859-1'; + } + } + $this->headers[$k] = $v; + $this->request .= "$k: $v\r\n"; + $this->debug("$k: $v"); + } + } + } + } + + /** + * parses a request + * + * The following fields are set by this function (when successful) + * + * headers + * request + * xml_encoding + * SOAPAction + * request + * requestSOAP + * methodURI + * methodname + * methodparams + * requestHeaders + * document + * + * This sets the fault field on error + * + * @param string $data XML string + * @access private + */ + function parse_request($data='') { + $this->debug('entering parse_request()'); + $this->parse_http_headers(); + $this->debug('got character encoding: '.$this->xml_encoding); + // uncompress if necessary + if (isset($this->headers['Content-Encoding']) && $this->headers['Content-Encoding'] != '') { + $this->debug('got content encoding: ' . $this->headers['Content-Encoding']); + if ($this->headers['Content-Encoding'] == 'deflate' || $this->headers['Content-Encoding'] == 'gzip') { + // if decoding works, use it. else assume data wasn't gzencoded + if (function_exists('gzuncompress')) { + if ($this->headers['Content-Encoding'] == 'deflate' && $degzdata = @gzuncompress($data)) { + $data = $degzdata; + } elseif ($this->headers['Content-Encoding'] == 'gzip' && $degzdata = gzinflate(substr($data, 10))) { + $data = $degzdata; + } else { + $this->fault('Client', 'Errors occurred when trying to decode the data'); + return; + } + } else { + $this->fault('Client', 'This Server does not support compressed data'); + return; + } + } + } + $this->request .= "\r\n".$data; + $this->requestSOAP = $data; + // parse response, get soap parser obj + $parser = new soap_parser($data,$this->xml_encoding,'',$this->decode_utf8); + // parser debug + $this->debug("parser debug: \n".$parser->getDebug()); + // if fault occurred during message parsing + if($err = $parser->getError()){ + $this->result = 'fault: error in msg parsing: '.$err; + $this->fault('Client',"error in msg parsing:\n".$err); + // else successfully parsed request into soapval object + } else { + // get/set methodname + $this->methodURI = $parser->root_struct_namespace; + $this->methodname = $parser->root_struct_name; + $this->debug('methodname: '.$this->methodname.' methodURI: '.$this->methodURI); + $this->debug('calling parser->get_response()'); + $this->methodparams = $parser->get_response(); + // get SOAP headers + $this->requestHeaders = $parser->getHeaders(); + // add document for doclit support + $this->document = $parser->document; + } + $this->debug('leaving parse_request'); + } + + /** + * invokes a PHP function for the requested SOAP method + * + * The following fields are set by this function (when successful) + * + * methodreturn + * + * Note that the PHP function that is called may also set the following + * fields to affect the response sent to the client + * + * responseHeaders + * outgoing_headers + * + * This sets the fault field on error + * + * @access private + */ + function invoke_method() { + $this->debug('entering invoke_method methodname: ' . $this->methodname . ' methodURI: ' . $this->methodURI); + // if a . is present in $this->methodname, we see if there is a class in scope, + // which could be referred to. We will also distinguish between two deliminators, + // to allow methods to be called a the class or an instance + $class = ''; + $method = ''; + if (strpos($this->methodname, '..') > 0) { + $delim = '..'; + } else if (strpos($this->methodname, '.') > 0) { + $delim = '.'; + } else { + $delim = ''; + } + + if (strlen($delim) > 0 && substr_count($this->methodname, $delim) == 1 && + class_exists(substr($this->methodname, 0, strpos($this->methodname, $delim)))) { + // get the class and method name + $class = substr($this->methodname, 0, strpos($this->methodname, $delim)); + $method = substr($this->methodname, strpos($this->methodname, $delim) + strlen($delim)); + $this->debug("class: $class method: $method delim: $delim"); + } + + // does method exist? + if ($class == '' && !function_exists($this->methodname)) { + $this->debug("function '$this->methodname' not found!"); + $this->result = 'fault: method not found'; + $this->fault('Client',"method '$this->methodname' not defined in service"); + return; + } + if ($class != '' && !in_array(strtolower($method), get_class_methods($class))) { + $this->debug("method '$this->methodname' not found in class '$class'!"); + $this->result = 'fault: method not found'; + $this->fault('Client',"method '$this->methodname' not defined in service"); + return; + } + + if($this->wsdl){ + if(!$this->opData = $this->wsdl->getOperationData($this->methodname)){ + //if( + $this->fault('Client',"Operation '$this->methodname' is not defined in the WSDL for this service"); + return; + } + $this->debug('opData:'); + $this->appendDebug($this->varDump($this->opData)); + } + $this->debug("method '$this->methodname' exists"); + // evaluate message, getting back parameters + // verify that request parameters match the method's signature + if(! $this->verify_method($this->methodname,$this->methodparams)){ + // debug + $this->debug('ERROR: request not verified against method signature'); + $this->result = 'fault: request failed validation against method signature'; + // return fault + $this->fault('Client',"Operation '$this->methodname' not defined in service."); + return; + } + + // if there are parameters to pass + $this->debug('params:'); + $this->appendDebug($this->varDump($this->methodparams)); + $this->debug("calling '$this->methodname'"); + if (!function_exists('call_user_func_array')) { + if ($class == '') { + $this->debug('calling function using eval()'); + $funcCall = "\$this->methodreturn = $this->methodname("; + } else { + if ($delim == '..') { + $this->debug('calling class method using eval()'); + $funcCall = "\$this->methodreturn = ".$class."::".$method."("; + } else { + $this->debug('calling instance method using eval()'); + // generate unique instance name + $instname = "\$inst_".time(); + $funcCall = $instname." = new ".$class."(); "; + $funcCall .= "\$this->methodreturn = ".$instname."->".$method."("; + } + } + if ($this->methodparams) { + foreach ($this->methodparams as $param) { + if (is_array($param)) { + $this->fault('Client', 'NuSOAP does not handle complexType parameters correctly when using eval; call_user_func_array must be available'); + return; + } + $funcCall .= "\"$param\","; + } + $funcCall = substr($funcCall, 0, -1); + } + $funcCall .= ');'; + $this->debug('function call: '.$funcCall); + @eval($funcCall); + } else { + if ($class == '') { + $this->debug('calling function using call_user_func_array()'); + $call_arg = "$this->methodname"; // straight assignment changes $this->methodname to lower case after call_user_func_array() + } elseif ($delim == '..') { + $this->debug('calling class method using call_user_func_array()'); + $call_arg = array ($class, $method); + } else { + $this->debug('calling instance method using call_user_func_array()'); + $instance = new $class (); + $call_arg = array(&$instance, $method); + } + $this->methodreturn = call_user_func_array($call_arg, $this->methodparams); + } + $this->debug('methodreturn:'); + $this->appendDebug($this->varDump($this->methodreturn)); + $this->debug("leaving invoke_method: called method $this->methodname, received $this->methodreturn of type ".gettype($this->methodreturn)); + } + + /** + * serializes the return value from a PHP function into a full SOAP Envelope + * + * The following fields are set by this function (when successful) + * + * responseSOAP + * + * This sets the fault field on error + * + * @access private + */ + function serialize_return() { + $this->debug('Entering serialize_return methodname: ' . $this->methodname . ' methodURI: ' . $this->methodURI); + // if fault + if (isset($this->methodreturn) && (get_class($this->methodreturn) == 'soap_fault')) { + $this->debug('got a fault object from method'); + $this->fault = $this->methodreturn; + return; + } elseif ($this->methodreturnisliteralxml) { + $return_val = $this->methodreturn; + // returned value(s) + } else { + $this->debug('got a(n) '.gettype($this->methodreturn).' from method'); + $this->debug('serializing return value'); + if($this->wsdl){ + // weak attempt at supporting multiple output params + if(sizeof($this->opData['output']['parts']) > 1){ + $opParams = $this->methodreturn; + } else { + // TODO: is this really necessary? + $opParams = array($this->methodreturn); + } + $return_val = $this->wsdl->serializeRPCParameters($this->methodname,'output',$opParams); + $this->appendDebug($this->wsdl->getDebug()); + $this->wsdl->clearDebug(); + if($errstr = $this->wsdl->getError()){ + $this->debug('got wsdl error: '.$errstr); + $this->fault('Server', 'unable to serialize result'); + return; + } + } else { + if (isset($this->methodreturn)) { + $return_val = $this->serialize_val($this->methodreturn, 'return'); + } else { + $return_val = ''; + $this->debug('in absence of WSDL, assume void return for backward compatibility'); + } + } + } + $this->debug('return value:'); + $this->appendDebug($this->varDump($return_val)); + + $this->debug('serializing response'); + if ($this->wsdl) { + $this->debug('have WSDL for serialization: style is ' . $this->opData['style']); + if ($this->opData['style'] == 'rpc') { + $this->debug('style is rpc for serialization: use is ' . $this->opData['output']['use']); + if ($this->opData['output']['use'] == 'literal') { + $payload = '<'.$this->methodname.'Response xmlns="'.$this->methodURI.'">'.$return_val.'methodname."Response>"; + } else { + $payload = 'methodname.'Response xmlns:ns1="'.$this->methodURI.'">'.$return_val.'methodname."Response>"; + } + } else { + $this->debug('style is not rpc for serialization: assume document'); + $payload = $return_val; + } + } else { + $this->debug('do not have WSDL for serialization: assume rpc/encoded'); + $payload = 'methodname.'Response xmlns:ns1="'.$this->methodURI.'">'.$return_val.'methodname."Response>"; + } + $this->result = 'successful'; + if($this->wsdl){ + //if($this->debug_flag){ + $this->appendDebug($this->wsdl->getDebug()); + // } + // Added: In case we use a WSDL, return a serialized env. WITH the usedNamespaces. + $this->responseSOAP = $this->serializeEnvelope($payload,$this->responseHeaders,$this->wsdl->usedNamespaces,$this->opData['style']); + } else { + $this->responseSOAP = $this->serializeEnvelope($payload,$this->responseHeaders); + } + $this->debug("Leaving serialize_return"); + } + + /** + * sends an HTTP response + * + * The following fields are set by this function (when successful) + * + * outgoing_headers + * response + * + * @access private + */ + function send_response() { + $this->debug('Enter send_response'); + if ($this->fault) { + $payload = $this->fault->serialize(); + $this->outgoing_headers[] = "HTTP/1.0 500 Internal Server Error"; + $this->outgoing_headers[] = "Status: 500 Internal Server Error"; + } else { + $payload = $this->responseSOAP; + // Some combinations of PHP+Web server allow the Status + // to come through as a header. Since OK is the default + // just do nothing. + // $this->outgoing_headers[] = "HTTP/1.0 200 OK"; + // $this->outgoing_headers[] = "Status: 200 OK"; + } + // add debug data if in debug mode + if(isset($this->debug_flag) && $this->debug_flag){ + $payload .= $this->getDebugAsXMLComment(); + } + $this->outgoing_headers[] = "Server: $this->title Server v$this->version"; + ereg('\$Revisio' . 'n: ([^ ]+)', $this->revision, $rev); + $this->outgoing_headers[] = "X-SOAP-Server: $this->title/$this->version (".$rev[1].")"; + // Let the Web server decide about this + //$this->outgoing_headers[] = "Connection: Close\r\n"; + $this->outgoing_headers[] = "Content-Type: text/xml; charset=$this->soap_defencoding"; + //begin code to compress payload - by John + // NOTE: there is no way to know whether the Web server will also compress + // this data. + if (strlen($payload) > 1024 && isset($this->headers) && isset($this->headers['Accept-Encoding'])) { + if (strstr($this->headers['Accept-Encoding'], 'gzip')) { + if (function_exists('gzencode')) { + if (isset($this->debug_flag) && $this->debug_flag) { + $payload .= ""; + } + $this->outgoing_headers[] = "Content-Encoding: gzip"; + $payload = gzencode($payload); + } else { + if (isset($this->debug_flag) && $this->debug_flag) { + $payload .= ""; + } + } + } elseif (strstr($this->headers['Accept-Encoding'], 'deflate')) { + // Note: MSIE requires gzdeflate output (no Zlib header and checksum), + // instead of gzcompress output, + // which conflicts with HTTP 1.1 spec (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.5) + if (function_exists('gzdeflate')) { + if (isset($this->debug_flag) && $this->debug_flag) { + $payload .= ""; + } + $this->outgoing_headers[] = "Content-Encoding: deflate"; + $payload = gzdeflate($payload); + } else { + if (isset($this->debug_flag) && $this->debug_flag) { + $payload .= ""; + } + } + } + } + //end code + $this->outgoing_headers[] = "Content-Length: ".strlen($payload); + reset($this->outgoing_headers); + foreach($this->outgoing_headers as $hdr){ + header($hdr, false); + } + print $payload; + $this->response = join("\r\n",$this->outgoing_headers)."\r\n\r\n".$payload; + } + + /** + * takes the value that was created by parsing the request + * and compares to the method's signature, if available. + * + * @param mixed + * @return boolean + * @access private + */ + function verify_method($operation,$request){ + if(isset($this->wsdl) && is_object($this->wsdl)){ + if($this->wsdl->getOperationData($operation)){ + return true; + } + } elseif(isset($this->operations[$operation])){ + return true; + } + return false; + } + + /** + * add a method to the dispatch map + * + * @param string $methodname + * @param string $in array of input values + * @param string $out array of output values + * @access public + */ + function add_to_map($methodname,$in,$out){ + $this->operations[$methodname] = array('name' => $methodname,'in' => $in,'out' => $out); + } + + /** + * register a service with the server + * + * @param string $methodname + * @param string $in assoc array of input values: key = param name, value = param type + * @param string $out assoc array of output values: key = param name, value = param type + * @param string $namespace + * @param string $soapaction + * @param string $style optional (rpc|document) + * @param string $use optional (encoded|literal) + * @param string $documentation optional Description to include in WSDL + * @access public + */ + function register($name,$in=array(),$out=array(),$namespace=false,$soapaction=false,$style=false,$use=false,$documentation=''){ + if($this->externalWSDLURL){ + die('You cannot bind to an external WSDL file, and register methods outside of it! Please choose either WSDL or no WSDL.'); + } + if(false == $namespace) { + } + if(false == $soapaction) { + $SERVER_NAME = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : $GLOBALS['SERVER_NAME']; + $SCRIPT_NAME = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : $GLOBALS['SCRIPT_NAME']; + $soapaction = "http://$SERVER_NAME$SCRIPT_NAME/$name"; + } + if(false == $style) { + $style = "rpc"; + } + if(false == $use) { + $use = "encoded"; + } + + $this->operations[$name] = array( + 'name' => $name, + 'in' => $in, + 'out' => $out, + 'namespace' => $namespace, + 'soapaction' => $soapaction, + 'style' => $style); + if($this->wsdl){ + $this->wsdl->addOperation($name,$in,$out,$namespace,$soapaction,$style,$use,$documentation); + } + return true; + } + + /** + * create a fault. this also acts as a flag to the server that a fault has occured. + * + * @param string faultcode + * @param string faultstring + * @param string faultactor + * @param string faultdetail + * @access public + */ + function fault($faultcode,$faultstring,$faultactor='',$faultdetail=''){ + $this->fault = new soap_fault($faultcode,$faultactor,$faultstring,$faultdetail); + $this->fault->soap_defencoding = $this->soap_defencoding; + } + + /** + * sets up wsdl object + * this acts as a flag to enable internal WSDL generation + * + * @param string $serviceName, name of the service + * @param string $namespace optional tns namespace + * @param string $endpoint optional URL of service endpoint + * @param string $style optional (rpc|document) WSDL style (also specified by operation) + * @param string $transport optional SOAP transport + * @param string $schemaTargetNamespace optional targetNamespace for service schema + */ + function configureWSDL($serviceName,$namespace = false,$endpoint = false,$style='rpc', $transport = 'http://schemas.xmlsoap.org/soap/http', $schemaTargetNamespace = false) + { + $SERVER_NAME = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : $GLOBALS['SERVER_NAME']; + $SERVER_PORT = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : $GLOBALS['SERVER_PORT']; + if ($SERVER_PORT == 80) { + $SERVER_PORT = ''; + } else { + $SERVER_PORT = ':' . $SERVER_PORT; + } + $SCRIPT_NAME = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : $GLOBALS['SCRIPT_NAME']; + if(false == $namespace) { + $namespace = "http://$SERVER_NAME/soap/$serviceName"; + } + + if(false == $endpoint) { + if (isset($_SERVER['HTTPS'])) { + $HTTPS = $_SERVER['HTTPS']; + } elseif (isset($GLOBALS['HTTPS'])) { + $HTTPS = $GLOBALS['HTTPS']; + } else { + $HTTPS = '0'; + } + if ($HTTPS == '1' || $HTTPS == 'on') { + $SCHEME = 'https'; + } else { + $SCHEME = 'http'; + } + $endpoint = "$SCHEME://$SERVER_NAME$SERVER_PORT$SCRIPT_NAME"; + } + + if(false == $schemaTargetNamespace) { + $schemaTargetNamespace = $namespace; + } + + $this->wsdl = new wsdl; + $this->wsdl->serviceName = $serviceName; + $this->wsdl->endpoint = $endpoint; + $this->wsdl->namespaces['tns'] = $namespace; + $this->wsdl->namespaces['soap'] = 'http://schemas.xmlsoap.org/wsdl/soap/'; + $this->wsdl->namespaces['wsdl'] = 'http://schemas.xmlsoap.org/wsdl/'; + if ($schemaTargetNamespace != $namespace) { + $this->wsdl->namespaces['types'] = $schemaTargetNamespace; + } + $this->wsdl->schemas[$schemaTargetNamespace][0] = new xmlschema('', '', $this->wsdl->namespaces); + $this->wsdl->schemas[$schemaTargetNamespace][0]->schemaTargetNamespace = $schemaTargetNamespace; + $this->wsdl->schemas[$schemaTargetNamespace][0]->imports['http://schemas.xmlsoap.org/soap/encoding/'][0] = array('location' => '', 'loaded' => true); + $this->wsdl->schemas[$schemaTargetNamespace][0]->imports['http://schemas.xmlsoap.org/wsdl/'][0] = array('location' => '', 'loaded' => true); + $this->wsdl->bindings[$serviceName.'Binding'] = array( + 'name'=>$serviceName.'Binding', + 'style'=>$style, + 'transport'=>$transport, + 'portType'=>$serviceName.'PortType'); + $this->wsdl->ports[$serviceName.'Port'] = array( + 'binding'=>$serviceName.'Binding', + 'location'=>$endpoint, + 'bindingType'=>'http://schemas.xmlsoap.org/wsdl/soap/'); + } +} + + + +?> +* @version $Id$ +* @access public +*/ +class wsdl extends nusoap_base { + // URL or filename of the root of this WSDL + var $wsdl; + // define internal arrays of bindings, ports, operations, messages, etc. + var $schemas = array(); + var $currentSchema; + var $message = array(); + var $complexTypes = array(); + var $messages = array(); + var $currentMessage; + var $currentOperation; + var $portTypes = array(); + var $currentPortType; + var $bindings = array(); + var $currentBinding; + var $ports = array(); + var $currentPort; + var $opData = array(); + var $status = ''; + var $documentation = false; + var $endpoint = ''; + // array of wsdl docs to import + var $import = array(); + // parser vars + var $parser; + var $position = 0; + var $depth = 0; + var $depth_array = array(); + // for getting wsdl + var $proxyhost = ''; + var $proxyport = ''; + var $proxyusername = ''; + var $proxypassword = ''; + var $timeout = 0; + var $response_timeout = 30; + + /** + * constructor + * + * @param string $wsdl WSDL document URL + * @param string $proxyhost + * @param string $proxyport + * @param string $proxyusername + * @param string $proxypassword + * @param integer $timeout set the connection timeout + * @param integer $response_timeout set the response timeout + * @access public + */ + function wsdl($wsdl = '',$proxyhost=false,$proxyport=false,$proxyusername=false,$proxypassword=false,$timeout=0,$response_timeout=30){ + $this->wsdl = $wsdl; + $this->proxyhost = $proxyhost; + $this->proxyport = $proxyport; + $this->proxyusername = $proxyusername; + $this->proxypassword = $proxypassword; + $this->timeout = $timeout; + $this->response_timeout = $response_timeout; + + // parse wsdl file + if ($wsdl != "") { + $this->debug('initial wsdl URL: ' . $wsdl); + $this->parseWSDL($wsdl); + } + // imports + // TODO: handle imports more properly, grabbing them in-line and nesting them + $imported_urls = array(); + $imported = 1; + while ($imported > 0) { + $imported = 0; + // Schema imports + foreach ($this->schemas as $ns => $list) { + foreach ($list as $xs) { + $wsdlparts = parse_url($this->wsdl); // this is bogusly simple! + foreach ($xs->imports as $ns2 => $list2) { + for ($ii = 0; $ii < count($list2); $ii++) { + if (! $list2[$ii]['loaded']) { + $this->schemas[$ns]->imports[$ns2][$ii]['loaded'] = true; + $url = $list2[$ii]['location']; + if ($url != '') { + $urlparts = parse_url($url); + if (!isset($urlparts['host'])) { + $url = $wsdlparts['scheme'] . '://' . $wsdlparts['host'] . + substr($wsdlparts['path'],0,strrpos($wsdlparts['path'],'/') + 1) .$urlparts['path']; + } + if (! in_array($url, $imported_urls)) { + $this->parseWSDL($url); + $imported++; + $imported_urls[] = $url; + } + } else { + $this->debug("Unexpected scenario: empty URL for unloaded import"); + } + } + } + } + } + } + // WSDL imports + $wsdlparts = parse_url($this->wsdl); // this is bogusly simple! + foreach ($this->import as $ns => $list) { + for ($ii = 0; $ii < count($list); $ii++) { + if (! $list[$ii]['loaded']) { + $this->import[$ns][$ii]['loaded'] = true; + $url = $list[$ii]['location']; + if ($url != '') { + $urlparts = parse_url($url); + if (!isset($urlparts['host'])) { + $url = $wsdlparts['scheme'] . '://' . $wsdlparts['host'] . + substr($wsdlparts['path'],0,strrpos($wsdlparts['path'],'/') + 1) .$urlparts['path']; + } + if (! in_array($url, $imported_urls)) { + $this->parseWSDL($url); + $imported++; + $imported_urls[] = $url; + } + } else { + $this->debug("Unexpected scenario: empty URL for unloaded import"); + } + } + } + } + } + // add new data to operation data + foreach($this->bindings as $binding => $bindingData) { + if (isset($bindingData['operations']) && is_array($bindingData['operations'])) { + foreach($bindingData['operations'] as $operation => $data) { + $this->debug('post-parse data gathering for ' . $operation); + $this->bindings[$binding]['operations'][$operation]['input'] = + isset($this->bindings[$binding]['operations'][$operation]['input']) ? + array_merge($this->bindings[$binding]['operations'][$operation]['input'], $this->portTypes[ $bindingData['portType'] ][$operation]['input']) : + $this->portTypes[ $bindingData['portType'] ][$operation]['input']; + $this->bindings[$binding]['operations'][$operation]['output'] = + isset($this->bindings[$binding]['operations'][$operation]['output']) ? + array_merge($this->bindings[$binding]['operations'][$operation]['output'], $this->portTypes[ $bindingData['portType'] ][$operation]['output']) : + $this->portTypes[ $bindingData['portType'] ][$operation]['output']; + if(isset($this->messages[ $this->bindings[$binding]['operations'][$operation]['input']['message'] ])){ + $this->bindings[$binding]['operations'][$operation]['input']['parts'] = $this->messages[ $this->bindings[$binding]['operations'][$operation]['input']['message'] ]; + } + if(isset($this->messages[ $this->bindings[$binding]['operations'][$operation]['output']['message'] ])){ + $this->bindings[$binding]['operations'][$operation]['output']['parts'] = $this->messages[ $this->bindings[$binding]['operations'][$operation]['output']['message'] ]; + } + if (isset($bindingData['style'])) { + $this->bindings[$binding]['operations'][$operation]['style'] = $bindingData['style']; + } + $this->bindings[$binding]['operations'][$operation]['transport'] = isset($bindingData['transport']) ? $bindingData['transport'] : ''; + $this->bindings[$binding]['operations'][$operation]['documentation'] = isset($this->portTypes[ $bindingData['portType'] ][$operation]['documentation']) ? $this->portTypes[ $bindingData['portType'] ][$operation]['documentation'] : ''; + $this->bindings[$binding]['operations'][$operation]['endpoint'] = isset($bindingData['endpoint']) ? $bindingData['endpoint'] : ''; + } + } + } + } + + /** + * parses the wsdl document + * + * @param string $wsdl path or URL + * @access private + */ + function parseWSDL($wsdl = '') + { + if ($wsdl == '') { + $this->debug('no wsdl passed to parseWSDL()!!'); + $this->setError('no wsdl passed to parseWSDL()!!'); + return false; + } + + // parse $wsdl for url format + $wsdl_props = parse_url($wsdl); + + if (isset($wsdl_props['scheme']) && ($wsdl_props['scheme'] == 'http' || $wsdl_props['scheme'] == 'https')) { + $this->debug('getting WSDL http(s) URL ' . $wsdl); + // get wsdl + $tr = new soap_transport_http($wsdl); + $tr->request_method = 'GET'; + $tr->useSOAPAction = false; + if($this->proxyhost && $this->proxyport){ + $tr->setProxy($this->proxyhost,$this->proxyport,$this->proxyusername,$this->proxypassword); + } + $tr->setEncoding('gzip, deflate'); + $wsdl_string = $tr->send('', $this->timeout, $this->response_timeout); + //$this->debug("WSDL request\n" . $tr->outgoing_payload); + //$this->debug("WSDL response\n" . $tr->incoming_payload); + $this->appendDebug($tr->getDebug()); + // catch errors + if($err = $tr->getError() ){ + $errstr = 'HTTP ERROR: '.$err; + $this->debug($errstr); + $this->setError($errstr); + unset($tr); + return false; + } + unset($tr); + $this->debug("got WSDL URL"); + } else { + // $wsdl is not http(s), so treat it as a file URL or plain file path + if (isset($wsdl_props['scheme']) && ($wsdl_props['scheme'] == 'file') && isset($wsdl_props['path'])) { + $path = isset($wsdl_props['host']) ? ($wsdl_props['host'] . ':' . $wsdl_props['path']) : $wsdl_props['path']; + } else { + $path = $wsdl; + } + $this->debug('getting WSDL file ' . $path); + if ($fp = @fopen($path, 'r')) { + $wsdl_string = ''; + while ($data = fread($fp, 32768)) { + $wsdl_string .= $data; + } + fclose($fp); + } else { + $errstr = "Bad path to WSDL file $path"; + $this->debug($errstr); + $this->setError($errstr); + return false; + } + } + $this->debug('Parse WSDL'); + // end new code added + // Create an XML parser. + $this->parser = xml_parser_create(); + // Set the options for parsing the XML data. + // xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); + xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0); + // Set the object for the parser. + xml_set_object($this->parser, $this); + // Set the element handlers for the parser. + xml_set_element_handler($this->parser, 'start_element', 'end_element'); + xml_set_character_data_handler($this->parser, 'character_data'); + // Parse the XML file. + if (!xml_parse($this->parser, $wsdl_string, true)) { + // Display an error message. + $errstr = sprintf( + 'XML error parsing WSDL from %s on line %d: %s', + $wsdl, + xml_get_current_line_number($this->parser), + xml_error_string(xml_get_error_code($this->parser)) + ); + $this->debug($errstr); + $this->debug("XML payload:\n" . $wsdl_string); + $this->setError($errstr); + return false; + } + // free the parser + xml_parser_free($this->parser); + $this->debug('Parsing WSDL done'); + // catch wsdl parse errors + if($this->getError()){ + return false; + } + return true; + } + + /** + * start-element handler + * + * @param string $parser XML parser object + * @param string $name element name + * @param string $attrs associative array of attributes + * @access private + */ + function start_element($parser, $name, $attrs) + { + if ($this->status == 'schema') { + $this->currentSchema->schemaStartElement($parser, $name, $attrs); + $this->appendDebug($this->currentSchema->getDebug()); + $this->currentSchema->clearDebug(); + } elseif (ereg('schema$', $name)) { + $this->debug('Parsing WSDL schema'); + // $this->debug("startElement for $name ($attrs[name]). status = $this->status (".$this->getLocalPart($name).")"); + $this->status = 'schema'; + $this->currentSchema = new xmlschema('', '', $this->namespaces); + $this->currentSchema->schemaStartElement($parser, $name, $attrs); + $this->appendDebug($this->currentSchema->getDebug()); + $this->currentSchema->clearDebug(); + } else { + // position in the total number of elements, starting from 0 + $pos = $this->position++; + $depth = $this->depth++; + // set self as current value for this depth + $this->depth_array[$depth] = $pos; + $this->message[$pos] = array('cdata' => ''); + // get element prefix + if (ereg(':', $name)) { + // get ns prefix + $prefix = substr($name, 0, strpos($name, ':')); + // get ns + $namespace = isset($this->namespaces[$prefix]) ? $this->namespaces[$prefix] : ''; + // get unqualified name + $name = substr(strstr($name, ':'), 1); + } + + if (count($attrs) > 0) { + foreach($attrs as $k => $v) { + // if ns declarations, add to class level array of valid namespaces + if (ereg("^xmlns", $k)) { + if ($ns_prefix = substr(strrchr($k, ':'), 1)) { + $this->namespaces[$ns_prefix] = $v; + } else { + $this->namespaces['ns' . (count($this->namespaces) + 1)] = $v; + } + if ($v == 'http://www.w3.org/2001/XMLSchema' || $v == 'http://www.w3.org/1999/XMLSchema' || $v == 'http://www.w3.org/2000/10/XMLSchema') { + $this->XMLSchemaVersion = $v; + $this->namespaces['xsi'] = $v . '-instance'; + } + } // + // expand each attribute + $k = strpos($k, ':') ? $this->expandQname($k) : $k; + if ($k != 'location' && $k != 'soapAction' && $k != 'namespace') { + $v = strpos($v, ':') ? $this->expandQname($v) : $v; + } + $eAttrs[$k] = $v; + } + $attrs = $eAttrs; + } else { + $attrs = array(); + } + // find status, register data + switch ($this->status) { + case 'message': + if ($name == 'part') { + if (isset($attrs['type'])) { + $this->debug("msg " . $this->currentMessage . ": found part $attrs[name]: " . implode(',', $attrs)); + $this->messages[$this->currentMessage][$attrs['name']] = $attrs['type']; + } + if (isset($attrs['element'])) { + $this->debug("msg " . $this->currentMessage . ": found part $attrs[name]: " . implode(',', $attrs)); + $this->messages[$this->currentMessage][$attrs['name']] = $attrs['element']; + } + } + break; + case 'portType': + switch ($name) { + case 'operation': + $this->currentPortOperation = $attrs['name']; + $this->debug("portType $this->currentPortType operation: $this->currentPortOperation"); + if (isset($attrs['parameterOrder'])) { + $this->portTypes[$this->currentPortType][$attrs['name']]['parameterOrder'] = $attrs['parameterOrder']; + } + break; + case 'documentation': + $this->documentation = true; + break; + // merge input/output data + default: + $m = isset($attrs['message']) ? $this->getLocalPart($attrs['message']) : ''; + $this->portTypes[$this->currentPortType][$this->currentPortOperation][$name]['message'] = $m; + break; + } + break; + case 'binding': + switch ($name) { + case 'binding': + // get ns prefix + if (isset($attrs['style'])) { + $this->bindings[$this->currentBinding]['prefix'] = $prefix; + } + $this->bindings[$this->currentBinding] = array_merge($this->bindings[$this->currentBinding], $attrs); + break; + case 'header': + $this->bindings[$this->currentBinding]['operations'][$this->currentOperation][$this->opStatus]['headers'][] = $attrs; + break; + case 'operation': + if (isset($attrs['soapAction'])) { + $this->bindings[$this->currentBinding]['operations'][$this->currentOperation]['soapAction'] = $attrs['soapAction']; + } + if (isset($attrs['style'])) { + $this->bindings[$this->currentBinding]['operations'][$this->currentOperation]['style'] = $attrs['style']; + } + if (isset($attrs['name'])) { + $this->currentOperation = $attrs['name']; + $this->debug("current binding operation: $this->currentOperation"); + $this->bindings[$this->currentBinding]['operations'][$this->currentOperation]['name'] = $attrs['name']; + $this->bindings[$this->currentBinding]['operations'][$this->currentOperation]['binding'] = $this->currentBinding; + $this->bindings[$this->currentBinding]['operations'][$this->currentOperation]['endpoint'] = isset($this->bindings[$this->currentBinding]['endpoint']) ? $this->bindings[$this->currentBinding]['endpoint'] : ''; + } + break; + case 'input': + $this->opStatus = 'input'; + break; + case 'output': + $this->opStatus = 'output'; + break; + case 'body': + if (isset($this->bindings[$this->currentBinding]['operations'][$this->currentOperation][$this->opStatus])) { + $this->bindings[$this->currentBinding]['operations'][$this->currentOperation][$this->opStatus] = array_merge($this->bindings[$this->currentBinding]['operations'][$this->currentOperation][$this->opStatus], $attrs); + } else { + $this->bindings[$this->currentBinding]['operations'][$this->currentOperation][$this->opStatus] = $attrs; + } + break; + } + break; + case 'service': + switch ($name) { + case 'port': + $this->currentPort = $attrs['name']; + $this->debug('current port: ' . $this->currentPort); + $this->ports[$this->currentPort]['binding'] = $this->getLocalPart($attrs['binding']); + + break; + case 'address': + $this->ports[$this->currentPort]['location'] = $attrs['location']; + $this->ports[$this->currentPort]['bindingType'] = $namespace; + $this->bindings[ $this->ports[$this->currentPort]['binding'] ]['bindingType'] = $namespace; + $this->bindings[ $this->ports[$this->currentPort]['binding'] ]['endpoint'] = $attrs['location']; + break; + } + break; + } + // set status + switch ($name) { + case 'import': + if (isset($attrs['location'])) { + $this->import[$attrs['namespace']][] = array('location' => $attrs['location'], 'loaded' => false); + $this->debug('parsing import ' . $attrs['namespace']. ' - ' . $attrs['location'] . ' (' . count($this->import[$attrs['namespace']]).')'); + } else { + $this->import[$attrs['namespace']][] = array('location' => '', 'loaded' => true); + if (! $this->getPrefixFromNamespace($attrs['namespace'])) { + $this->namespaces['ns'.(count($this->namespaces)+1)] = $attrs['namespace']; + } + $this->debug('parsing import ' . $attrs['namespace']. ' - [no location] (' . count($this->import[$attrs['namespace']]).')'); + } + break; + //wait for schema + //case 'types': + // $this->status = 'schema'; + // break; + case 'message': + $this->status = 'message'; + $this->messages[$attrs['name']] = array(); + $this->currentMessage = $attrs['name']; + break; + case 'portType': + $this->status = 'portType'; + $this->portTypes[$attrs['name']] = array(); + $this->currentPortType = $attrs['name']; + break; + case "binding": + if (isset($attrs['name'])) { + // get binding name + if (strpos($attrs['name'], ':')) { + $this->currentBinding = $this->getLocalPart($attrs['name']); + } else { + $this->currentBinding = $attrs['name']; + } + $this->status = 'binding'; + $this->bindings[$this->currentBinding]['portType'] = $this->getLocalPart($attrs['type']); + $this->debug("current binding: $this->currentBinding of portType: " . $attrs['type']); + } + break; + case 'service': + $this->serviceName = $attrs['name']; + $this->status = 'service'; + $this->debug('current service: ' . $this->serviceName); + break; + case 'definitions': + foreach ($attrs as $name => $value) { + $this->wsdl_info[$name] = $value; + } + break; + } + } + } + + /** + * end-element handler + * + * @param string $parser XML parser object + * @param string $name element name + * @access private + */ + function end_element($parser, $name){ + // unset schema status + if (/*ereg('types$', $name) ||*/ ereg('schema$', $name)) { + $this->status = ""; + $this->schemas[$this->currentSchema->schemaTargetNamespace][] = $this->currentSchema; + $this->debug('Parsing WSDL schema done'); + } + if ($this->status == 'schema') { + $this->currentSchema->schemaEndElement($parser, $name); + } else { + // bring depth down a notch + $this->depth--; + } + // end documentation + if ($this->documentation) { + //TODO: track the node to which documentation should be assigned; it can be a part, message, etc. + //$this->portTypes[$this->currentPortType][$this->currentPortOperation]['documentation'] = $this->documentation; + $this->documentation = false; + } + } + + /** + * element content handler + * + * @param string $parser XML parser object + * @param string $data element content + * @access private + */ + function character_data($parser, $data) + { + $pos = isset($this->depth_array[$this->depth]) ? $this->depth_array[$this->depth] : 0; + if (isset($this->message[$pos]['cdata'])) { + $this->message[$pos]['cdata'] .= $data; + } + if ($this->documentation) { + $this->documentation .= $data; + } + } + + function getBindingData($binding) + { + if (is_array($this->bindings[$binding])) { + return $this->bindings[$binding]; + } + } + + /** + * returns an assoc array of operation names => operation data + * + * @param string $bindingType eg: soap, smtp, dime (only soap is currently supported) + * @return array + * @access public + */ + function getOperations($bindingType = 'soap') + { + $ops = array(); + if ($bindingType == 'soap') { + $bindingType = 'http://schemas.xmlsoap.org/wsdl/soap/'; + } + // loop thru ports + foreach($this->ports as $port => $portData) { + // binding type of port matches parameter + if ($portData['bindingType'] == $bindingType) { + //$this->debug("getOperations for port $port"); + //$this->debug("port data: " . $this->varDump($portData)); + //$this->debug("bindings: " . $this->varDump($this->bindings[ $portData['binding'] ])); + // merge bindings + if (isset($this->bindings[ $portData['binding'] ]['operations'])) { + $ops = array_merge ($ops, $this->bindings[ $portData['binding'] ]['operations']); + } + } + } + return $ops; + } + + /** + * returns an associative array of data necessary for calling an operation + * + * @param string $operation , name of operation + * @param string $bindingType , type of binding eg: soap + * @return array + * @access public + */ + function getOperationData($operation, $bindingType = 'soap') + { + if ($bindingType == 'soap') { + $bindingType = 'http://schemas.xmlsoap.org/wsdl/soap/'; + } + // loop thru ports + foreach($this->ports as $port => $portData) { + // binding type of port matches parameter + if ($portData['bindingType'] == $bindingType) { + // get binding + //foreach($this->bindings[ $portData['binding'] ]['operations'] as $bOperation => $opData) { + foreach(array_keys($this->bindings[ $portData['binding'] ]['operations']) as $bOperation) { + if ($operation == $bOperation) { + $opData = $this->bindings[ $portData['binding'] ]['operations'][$operation]; + return $opData; + } + } + } + } + } + + /** + * returns an array of information about a given type + * returns false if no type exists by the given name + * + * typeDef = array( + * 'elements' => array(), // refs to elements array + * 'restrictionBase' => '', + * 'phpType' => '', + * 'order' => '(sequence|all)', + * 'attrs' => array() // refs to attributes array + * ) + * + * @param $type string + * @param $ns string + * @return mixed + * @access public + * @see xmlschema + */ + function getTypeDef($type, $ns) { + if ((! $ns) && isset($this->namespaces['tns'])) { + $ns = $this->namespaces['tns']; + $this->debug("type namespace forced to $ns"); + } + if (isset($this->schemas[$ns])) { + $this->debug("have schema for namespace $ns"); + for ($i = 0; $i < count($this->schemas[$ns]); $i++) { + $xs = &$this->schemas[$ns][$i]; + $t = $xs->getTypeDef($type); + $this->appendDebug($xs->getDebug()); + $xs->clearDebug(); + if ($t) { + if (!isset($t['phpType'])) { + // get info for type to tack onto the element + $uqType = substr($t['type'], strrpos($t['type'], ':') + 1); + $ns = substr($t['type'], 0, strrpos($t['type'], ':')); + $etype = $this->getTypeDef($uqType, $ns); + if ($etype) { + $this->debug("found type $etype for [element] $type"); + if (isset($etype['phpType'])) { + $t['phpType'] = $etype['phpType']; + } + if (isset($etype['elements'])) { + $t['elements'] = $etype['elements']; + } + } + } + return $t; + } + } + } else { + $this->debug("do not have schema for namespace $ns"); + } + return false; + } + + /** + * prints html description of services + * + * @access private + */ + function webDescription(){ + $b = ' + NuSOAP: '.$this->serviceName.' + + + + +
+

+
'.$this->serviceName.'
+ +
'; + return $b; + } + + /** + * serialize the parsed wsdl + * + * @param $debug mixed whether to put debug=1 in endpoint URL + * @return string , serialization of WSDL + * @access public + */ + function serialize($debug = 0) + { + $xml = 'namespaces as $k => $v) { + $xml .= " xmlns:$k=\"$v\""; + } + // 10.9.02 - add poulter fix for wsdl and tns declarations + if (isset($this->namespaces['wsdl'])) { + $xml .= " xmlns=\"" . $this->namespaces['wsdl'] . "\""; + } + if (isset($this->namespaces['tns'])) { + $xml .= " targetNamespace=\"" . $this->namespaces['tns'] . "\""; + } + $xml .= '>'; + // imports + if (sizeof($this->import) > 0) { + foreach($this->import as $ns => $list) { + foreach ($list as $ii) { + if ($ii['location'] != '') { + $xml .= ''; + } else { + $xml .= ''; + } + } + } + } + // types + if (count($this->schemas)>=1) { + $xml .= ''; + foreach ($this->schemas as $ns => $list) { + foreach ($list as $xs) { + $xml .= $xs->serializeSchema(); + } + } + $xml .= ''; + } + // messages + if (count($this->messages) >= 1) { + foreach($this->messages as $msgName => $msgParts) { + $xml .= ''; + if(is_array($msgParts)){ + foreach($msgParts as $partName => $partType) { + // print 'serializing '.$partType.', sv: '.$this->XMLSchemaVersion.'
'; + if (strpos($partType, ':')) { + $typePrefix = $this->getPrefixFromNamespace($this->getPrefix($partType)); + } elseif (isset($this->typemap[$this->namespaces['xsd']][$partType])) { + // print 'checking typemap: '.$this->XMLSchemaVersion.'
'; + $typePrefix = 'xsd'; + } else { + foreach($this->typemap as $ns => $types) { + if (isset($types[$partType])) { + $typePrefix = $this->getPrefixFromNamespace($ns); + } + } + if (!isset($typePrefix)) { + die("$partType has no namespace!"); + } + } + $typeDef = $this->getTypeDef($this->getLocalPart($partType), $typePrefix); + if ($typeDef['typeClass'] == 'element') { + $elementortype = 'element'; + } else { + $elementortype = 'type'; + } + $xml .= ''; + } + } + $xml .= '
'; + } + } + // bindings & porttypes + if (count($this->bindings) >= 1) { + $binding_xml = ''; + $portType_xml = ''; + foreach($this->bindings as $bindingName => $attrs) { + $binding_xml .= ''; + $binding_xml .= ''; + $portType_xml .= ''; + foreach($attrs['operations'] as $opName => $opParts) { + $binding_xml .= ''; + $binding_xml .= ''; + if (isset($opParts['input']['encodingStyle']) && $opParts['input']['encodingStyle'] != '') { + $enc_style = ' encodingStyle="' . $opParts['input']['encodingStyle'] . '"'; + } else { + $enc_style = ''; + } + $binding_xml .= ''; + if (isset($opParts['output']['encodingStyle']) && $opParts['output']['encodingStyle'] != '') { + $enc_style = ' encodingStyle="' . $opParts['output']['encodingStyle'] . '"'; + } else { + $enc_style = ''; + } + $binding_xml .= ''; + $binding_xml .= ''; + $portType_xml .= ''; + } + $portType_xml .= ''; + $portType_xml .= ''; + $portType_xml .= ''; + } + $portType_xml .= ''; + $binding_xml .= ''; + } + $xml .= $portType_xml . $binding_xml; + } + // services + $xml .= ''; + if (count($this->ports) >= 1) { + foreach($this->ports as $pName => $attrs) { + $xml .= ''; + $xml .= ''; + $xml .= ''; + } + } + $xml .= ''; + return $xml . ''; + } + + /** + * serialize a PHP value according to a WSDL message definition + * + * TODO + * - multi-ref serialization + * - validate PHP values against type definitions, return errors if invalid + * + * @param string $ type name + * @param mixed $ param value + * @return mixed new param or false if initial value didn't validate + */ + function serializeRPCParameters($operation, $direction, $parameters) + { + $this->debug('in serializeRPCParameters with operation '.$operation.', direction '.$direction.' and '.count($parameters).' param(s), and xml schema version ' . $this->XMLSchemaVersion); + + if ($direction != 'input' && $direction != 'output') { + $this->debug('The value of the \$direction argument needs to be either "input" or "output"'); + $this->setError('The value of the \$direction argument needs to be either "input" or "output"'); + return false; + } + if (!$opData = $this->getOperationData($operation)) { + $this->debug('Unable to retrieve WSDL data for operation: ' . $operation); + $this->setError('Unable to retrieve WSDL data for operation: ' . $operation); + return false; + } + $this->debug('opData:'); + $this->appendDebug($this->varDump($opData)); + + // Get encoding style for output and set to current + $encodingStyle = 'http://schemas.xmlsoap.org/soap/encoding/'; + if(($direction == 'input') && isset($opData['output']['encodingStyle']) && ($opData['output']['encodingStyle'] != $encodingStyle)) { + $encodingStyle = $opData['output']['encodingStyle']; + $enc_style = $encodingStyle; + } + + // set input params + $xml = ''; + if (isset($opData[$direction]['parts']) && sizeof($opData[$direction]['parts']) > 0) { + + $use = $opData[$direction]['use']; + $this->debug('have ' . count($opData[$direction]['parts']) . ' part(s) to serialize'); + if (is_array($parameters)) { + $parametersArrayType = $this->isArraySimpleOrStruct($parameters); + $this->debug('have ' . count($parameters) . ' parameter(s) provided as ' . $parametersArrayType . ' to serialize'); + foreach($opData[$direction]['parts'] as $name => $type) { + $this->debug('serializing part "'.$name.'" of type "'.$type.'"'); + // Track encoding style + if (isset($opData[$direction]['encodingStyle']) && $encodingStyle != $opData[$direction]['encodingStyle']) { + $encodingStyle = $opData[$direction]['encodingStyle']; + $enc_style = $encodingStyle; + } else { + $enc_style = false; + } + // NOTE: add error handling here + // if serializeType returns false, then catch global error and fault + if ($parametersArrayType == 'arraySimple') { + $p = array_shift($parameters); + $this->debug('calling serializeType w/indexed param'); + $xml .= $this->serializeType($name, $type, $p, $use, $enc_style); + } elseif (isset($parameters[$name])) { + $this->debug('calling serializeType w/named param'); + $xml .= $this->serializeType($name, $type, $parameters[$name], $use, $enc_style); + } else { + // TODO: only send nillable + $this->debug('calling serializeType w/null param'); + $xml .= $this->serializeType($name, $type, null, $use, $enc_style); + } + } + } else { + $this->debug('no parameters passed.'); + } + } + $this->debug("serializeRPCParameters returning: $xml"); + return $xml; + } + + /** + * serialize a PHP value according to a WSDL message definition + * + * TODO + * - multi-ref serialization + * - validate PHP values against type definitions, return errors if invalid + * + * @param string $ type name + * @param mixed $ param value + * @return mixed new param or false if initial value didn't validate + */ + function serializeParameters($operation, $direction, $parameters) + { + $this->debug('in serializeParameters with operation '.$operation.', direction '.$direction.' and '.count($parameters).' param(s), and xml schema version ' . $this->XMLSchemaVersion); + + if ($direction != 'input' && $direction != 'output') { + $this->debug('The value of the \$direction argument needs to be either "input" or "output"'); + $this->setError('The value of the \$direction argument needs to be either "input" or "output"'); + return false; + } + if (!$opData = $this->getOperationData($operation)) { + $this->debug('Unable to retrieve WSDL data for operation: ' . $operation); + $this->setError('Unable to retrieve WSDL data for operation: ' . $operation); + return false; + } + $this->debug('opData:'); + $this->appendDebug($this->varDump($opData)); + + // Get encoding style for output and set to current + $encodingStyle = 'http://schemas.xmlsoap.org/soap/encoding/'; + if(($direction == 'input') && isset($opData['output']['encodingStyle']) && ($opData['output']['encodingStyle'] != $encodingStyle)) { + $encodingStyle = $opData['output']['encodingStyle']; + $enc_style = $encodingStyle; + } + + // set input params + $xml = ''; + if (isset($opData[$direction]['parts']) && sizeof($opData[$direction]['parts']) > 0) { + + $use = $opData[$direction]['use']; + $this->debug("use=$use"); + $this->debug('got ' . count($opData[$direction]['parts']) . ' part(s)'); + if (is_array($parameters)) { + $parametersArrayType = $this->isArraySimpleOrStruct($parameters); + $this->debug('have ' . $parametersArrayType . ' parameters'); + foreach($opData[$direction]['parts'] as $name => $type) { + $this->debug('serializing part "'.$name.'" of type "'.$type.'"'); + // Track encoding style + if(isset($opData[$direction]['encodingStyle']) && $encodingStyle != $opData[$direction]['encodingStyle']) { + $encodingStyle = $opData[$direction]['encodingStyle']; + $enc_style = $encodingStyle; + } else { + $enc_style = false; + } + // NOTE: add error handling here + // if serializeType returns false, then catch global error and fault + if ($parametersArrayType == 'arraySimple') { + $p = array_shift($parameters); + $this->debug('calling serializeType w/indexed param'); + $xml .= $this->serializeType($name, $type, $p, $use, $enc_style); + } elseif (isset($parameters[$name])) { + $this->debug('calling serializeType w/named param'); + $xml .= $this->serializeType($name, $type, $parameters[$name], $use, $enc_style); + } else { + // TODO: only send nillable + $this->debug('calling serializeType w/null param'); + $xml .= $this->serializeType($name, $type, null, $use, $enc_style); + } + } + } else { + $this->debug('no parameters passed.'); + } + } + $this->debug("serializeParameters returning: $xml"); + return $xml; + } + + /** + * serializes a PHP value according a given type definition + * + * @param string $name , name of type (part) + * @param string $type , type of type, heh (type or element) + * @param mixed $value , a native PHP value (parameter value) + * @param string $use , use for part (encoded|literal) + * @param string $encodingStyle , use to add encoding changes to serialisation + * @param boolean $unqualified , a kludge for what should be form handling + * @return string serialization + * @access public + */ + function serializeType($name, $type, $value, $use='encoded', $encodingStyle=false, $unqualified=false) + { + $this->debug("in serializeType: $name, $type, $use, $encodingStyle, " . ($unqualified ? "unqualified" : "qualified")); + $this->debug("value:"); + $this->appendDebug($this->varDump($value)); + if($use == 'encoded' && $encodingStyle) { + $encodingStyle = ' SOAP-ENV:encodingStyle="' . $encodingStyle . '"'; + } + + // if a soap_val has been supplied, let its type override the WSDL + if (is_object($value) && get_class($value) == 'soapval') { + // TODO: get attributes from soapval? + if ($value->type_ns) { + $type = $value->type_ns . ':' . $value->type; + $forceType = true; + $this->debug("in serializeType: soapval overrides type to $type"); + } elseif ($value->type) { + $type = $value->type; + $forceType = true; + $this->debug("in serializeType: soapval overrides type to $type"); + } else { + $forceType = false; + $this->debug("in serializeType: soapval does not override type"); + } + $attrs = $value->attributes; + $value = $value->value; + $this->debug("in serializeType: soapval overrides value to $value"); + if ($attrs) { + foreach ($attrs as $n => $v) { + $value['!' . $n] = $v; + } + $this->debug("in serializeType: soapval provides attributes"); + } + } else { + $forceType = false; + } + + $xml = ''; + if (strpos($type, ':')) { + $uqType = substr($type, strrpos($type, ':') + 1); + $ns = substr($type, 0, strrpos($type, ':')); + $this->debug("got a prefixed type: $uqType, $ns"); + if ($this->getNamespaceFromPrefix($ns)) { + $ns = $this->getNamespaceFromPrefix($ns); + $this->debug("expanded prefixed type: $uqType, $ns"); + } + + if($ns == $this->XMLSchemaVersion || $ns == 'http://schemas.xmlsoap.org/soap/encoding/'){ + $this->debug('type namespace indicates XML Schema or SOAP Encoding type'); + if ($unqualified && $use == 'literal') { + $elementNS = " xmlns=\"\""; + } else { + $elementNS = ''; + } + if (is_null($value)) { + if ($use == 'literal') { + // TODO: depends on nillable + $xml = "<$name$elementNS/>"; + } else { + $xml = "<$name$elementNS xsi:nil=\"true\"/>"; + } + $this->debug("serializeType returning: $xml"); + return $xml; + } + if ($uqType == 'boolean' && !$value) { + $value = 'false'; + } elseif ($uqType == 'boolean') { + $value = 'true'; + } + if ($uqType == 'string' && gettype($value) == 'string') { + $value = $this->expandEntities($value); + } + if (($uqType == 'long' || $uqType == 'unsignedLong') && gettype($value) == 'double') { + $value = sprintf("%.0lf", $value); + } + // it's a scalar + // TODO: what about null/nil values? + // check type isn't a custom type extending xmlschema namespace + if (!$this->getTypeDef($uqType, $ns)) { + if ($use == 'literal') { + if ($forceType) { + $xml = "<$name$elementNS xsi:type=\"" . $this->getPrefixFromNamespace($ns) . ":$uqType\">$value"; + } else { + $xml = "<$name$elementNS>$value"; + } + } else { + $xml = "<$name$elementNS xsi:type=\"" . $this->getPrefixFromNamespace($ns) . ":$uqType\"$encodingStyle>$value"; + } + $this->debug("serializeType returning: $xml"); + return $xml; + } + $this->debug('custom type extends XML Schema or SOAP Encoding namespace (yuck)'); + } else if ($ns == 'http://xml.apache.org/xml-soap') { + if ($uqType == 'Map') { + $contents = ''; + foreach($value as $k => $v) { + $this->debug("serializing map element: key $k, value $v"); + $contents .= ''; + $contents .= $this->serialize_val($k,'key',false,false,false,false,$use); + $contents .= $this->serialize_val($v,'value',false,false,false,false,$use); + $contents .= ''; + } + if ($use == 'literal') { + if ($forceType) { + $xml = "<$name xsi:type=\"" . $this->getPrefixFromNamespace('http://xml.apache.org/xml-soap') . ":$uqType\">$contents"; + } else { + $xml = "<$name>$contents"; + } + } else { + $xml = "<$name xsi:type=\"" . $this->getPrefixFromNamespace('http://xml.apache.org/xml-soap') . ":$uqType\"$encodingStyle>$contents"; + } + $this->debug("serializeType returning: $xml"); + return $xml; + } + } + } else { + $this->debug("No namespace for type $type"); + $ns = ''; + $uqType = $type; + } + if(!$typeDef = $this->getTypeDef($uqType, $ns)){ + $this->setError("$type ($uqType) is not a supported type."); + $this->debug("$type ($uqType) is not a supported type."); + return false; + } else { + $this->debug("typedef:"); + $this->appendDebug($this->varDump($typeDef)); + } + $phpType = $typeDef['phpType']; + $this->debug("serializeType: uqType: $uqType, ns: $ns, phptype: $phpType, arrayType: " . (isset($typeDef['arrayType']) ? $typeDef['arrayType'] : '') ); + // if php type == struct, map value to the element names + if ($phpType == 'struct') { + if (isset($typeDef['typeClass']) && $typeDef['typeClass'] == 'element') { + $elementName = $uqType; + if (isset($typeDef['form']) && ($typeDef['form'] == 'qualified')) { + $elementNS = " xmlns=\"$ns\""; + } else { + $elementNS = " xmlns=\"\""; + } + } else { + $elementName = $name; + if ($unqualified) { + $elementNS = " xmlns=\"\""; + } else { + $elementNS = ''; + } + } + if (is_null($value)) { + if ($use == 'literal') { + // TODO: depends on nillable + $xml = "<$elementName$elementNS/>"; + } else { + $xml = "<$elementName$elementNS xsi:nil=\"true\"/>"; + } + $this->debug("serializeType returning: $xml"); + return $xml; + } + $elementAttrs = $this->serializeComplexTypeAttributes($typeDef, $value, $ns, $uqType); + if ($use == 'literal') { + if ($forceType) { + $xml = "<$elementName$elementNS$elementAttrs xsi:type=\"" . $this->getPrefixFromNamespace($ns) . ":$uqType\">"; + } else { + $xml = "<$elementName$elementNS$elementAttrs>"; + } + } else { + $xml = "<$elementName$elementNS$elementAttrs xsi:type=\"" . $this->getPrefixFromNamespace($ns) . ":$uqType\"$encodingStyle>"; + } + + $xml .= $this->serializeComplexTypeElements($typeDef, $value, $ns, $uqType, $use, $encodingStyle); + $xml .= ""; + } elseif ($phpType == 'array') { + if (isset($typeDef['form']) && ($typeDef['form'] == 'qualified')) { + $elementNS = " xmlns=\"$ns\""; + } else { + if ($unqualified) { + $elementNS = " xmlns=\"\""; + } else { + $elementNS = ''; + } + } + if (is_null($value)) { + if ($use == 'literal') { + // TODO: depends on nillable + $xml = "<$name$elementNS/>"; + } else { + $xml = "<$name$elementNS xsi:nil=\"true\"/>"; + } + $this->debug("serializeType returning: $xml"); + return $xml; + } + if (isset($typeDef['multidimensional'])) { + $nv = array(); + foreach($value as $v) { + $cols = ',' . sizeof($v); + $nv = array_merge($nv, $v); + } + $value = $nv; + } else { + $cols = ''; + } + if (is_array($value) && sizeof($value) >= 1) { + $rows = sizeof($value); + $contents = ''; + foreach($value as $k => $v) { + $this->debug("serializing array element: $k, $v of type: $typeDef[arrayType]"); + //if (strpos($typeDef['arrayType'], ':') ) { + if (!in_array($typeDef['arrayType'],$this->typemap['http://www.w3.org/2001/XMLSchema'])) { + $contents .= $this->serializeType('item', $typeDef['arrayType'], $v, $use); + } else { + $contents .= $this->serialize_val($v, 'item', $typeDef['arrayType'], null, $this->XMLSchemaVersion, false, $use); + } + } + } else { + $rows = 0; + $contents = null; + } + // TODO: for now, an empty value will be serialized as a zero element + // array. Revisit this when coding the handling of null/nil values. + if ($use == 'literal') { + $xml = "<$name$elementNS>" + .$contents + .""; + } else { + $xml = "<$name$elementNS xsi:type=\"".$this->getPrefixFromNamespace('http://schemas.xmlsoap.org/soap/encoding/').':Array" '. + $this->getPrefixFromNamespace('http://schemas.xmlsoap.org/soap/encoding/') + .':arrayType="' + .$this->getPrefixFromNamespace($this->getPrefix($typeDef['arrayType'])) + .":".$this->getLocalPart($typeDef['arrayType'])."[$rows$cols]\">" + .$contents + .""; + } + } elseif ($phpType == 'scalar') { + if (isset($typeDef['form']) && ($typeDef['form'] == 'qualified')) { + $elementNS = " xmlns=\"$ns\""; + } else { + if ($unqualified) { + $elementNS = " xmlns=\"\""; + } else { + $elementNS = ''; + } + } + if ($use == 'literal') { + if ($forceType) { + $xml = "<$name$elementNS xsi:type=\"" . $this->getPrefixFromNamespace($ns) . ":$uqType\">$value"; + } else { + $xml = "<$name$elementNS>$value"; + } + } else { + $xml = "<$name$elementNS xsi:type=\"" . $this->getPrefixFromNamespace($ns) . ":$uqType\"$encodingStyle>$value"; + } + } + $this->debug("serializeType returning: $xml"); + return $xml; + } + + /** + * serializes the attributes for a complexType + * + * @param array $typeDef + * @param mixed $value , a native PHP value (parameter value) + * @param string $ns the namespace of the type + * @param string $uqType the local part of the type + * @return string serialization + * @access public + */ + function serializeComplexTypeAttributes($typeDef, $value, $ns, $uqType) { + $xml = ''; + if (isset($typeDef['attrs']) && is_array($typeDef['attrs'])) { + $this->debug("serialize attributes for XML Schema type $ns:$uqType"); + if (is_array($value)) { + $xvalue = $value; + } elseif (is_object($value)) { + $xvalue = get_object_vars($value); + } else { + $this->debug("value is neither an array nor an object for XML Schema type $ns:$uqType"); + $xvalue = array(); + } + foreach ($typeDef['attrs'] as $aName => $attrs) { + if (isset($xvalue['!' . $aName])) { + $xname = '!' . $aName; + $this->debug("value provided for attribute $aName with key $xname"); + } elseif (isset($xvalue[$aName])) { + $xname = $aName; + $this->debug("value provided for attribute $aName with key $xname"); + } elseif (isset($attrs['default'])) { + $xname = '!' . $aName; + $xvalue[$xname] = $attrs['default']; + $this->debug('use default value of ' . $xvalue[$aName] . ' for attribute ' . $aName); + } else { + $xname = ''; + $this->debug("no value provided for attribute $aName"); + } + if ($xname) { + $xml .= " $aName=\"" . $xvalue[$xname] . "\""; + } + } + } else { + $this->debug("no attributes to serialize for XML Schema type $ns:$uqType"); + } + if (isset($typeDef['extensionBase'])) { + $ns = $this->getPrefix($typeDef['extensionBase']); + $uqType = $this->getLocalPart($typeDef['extensionBase']); + if ($this->getNamespaceFromPrefix($ns)) { + $ns = $this->getNamespaceFromPrefix($ns); + } + if ($typeDef = $this->getTypeDef($uqType, $ns)) { + $this->debug("serialize attributes for extension base $ns:$uqType"); + $xml .= $this->serializeComplexTypeAttributes($typeDef, $value, $ns, $uqType); + } else { + $this->debug("extension base $ns:$uqType is not a supported type"); + } + } + return $xml; + } + + /** + * serializes the elements for a complexType + * + * @param array $typeDef + * @param mixed $value , a native PHP value (parameter value) + * @param string $ns the namespace of the type + * @param string $uqType the local part of the type + * @param string $use , use for part (encoded|literal) + * @param string $encodingStyle , use to add encoding changes to serialisation + * @return string serialization + * @access public + */ + function serializeComplexTypeElements($typeDef, $value, $ns, $uqType, $use='encoded', $encodingStyle=false) { + $xml = ''; + if (isset($typeDef['elements']) && is_array($typeDef['elements'])) { + $this->debug("serialize elements for XML Schema type $ns:$uqType"); + if (is_array($value)) { + $xvalue = $value; + } elseif (is_object($value)) { + $xvalue = get_object_vars($value); + } else { + $this->debug("value is neither an array nor an object for XML Schema type $ns:$uqType"); + $xvalue = array(); + } + // toggle whether all elements are present - ideally should validate against schema + if (count($typeDef['elements']) != count($xvalue)){ + $optionals = true; + } + foreach ($typeDef['elements'] as $eName => $attrs) { + if (!isset($xvalue[$eName])) { + if (isset($attrs['default'])) { + $xvalue[$eName] = $attrs['default']; + $this->debug('use default value of ' . $xvalue[$eName] . ' for element ' . $eName); + } + } + // if user took advantage of a minOccurs=0, then only serialize named parameters + if (isset($optionals) && !isset($xvalue[$eName])){ + // do nothing + $this->debug("no value provided for complexType element $eName, so serialize nothing"); + } else { + // get value + if (isset($xvalue[$eName])) { + $v = $xvalue[$eName]; + } else { + $v = null; + } + if (isset($attrs['form'])) { + $unqualified = ($attrs['form'] == 'unqualified'); + } else { + $unqualified = false; + } + // TODO: if maxOccurs > 1 (not just unbounded), then allow serialization of an array + if (isset($attrs['maxOccurs']) && $attrs['maxOccurs'] == 'unbounded' && isset($v) && is_array($v) && $this->isArraySimpleOrStruct($v) == 'arraySimple') { + $vv = $v; + foreach ($vv as $k => $v) { + if (isset($attrs['type'])) { + // serialize schema-defined type + $xml .= $this->serializeType($eName, $attrs['type'], $v, $use, $encodingStyle, $unqualified); + } else { + // serialize generic type + $this->debug("calling serialize_val() for $v, $eName, false, false, false, false, $use"); + $xml .= $this->serialize_val($v, $eName, false, false, false, false, $use); + } + } + } else { + if (isset($attrs['type'])) { + // serialize schema-defined type + $xml .= $this->serializeType($eName, $attrs['type'], $v, $use, $encodingStyle, $unqualified); + } else { + // serialize generic type + $this->debug("calling serialize_val() for $v, $eName, false, false, false, false, $use"); + $xml .= $this->serialize_val($v, $eName, false, false, false, false, $use); + } + } + } + } + } else { + $this->debug("no elements to serialize for XML Schema type $ns:$uqType"); + } + if (isset($typeDef['extensionBase'])) { + $ns = $this->getPrefix($typeDef['extensionBase']); + $uqType = $this->getLocalPart($typeDef['extensionBase']); + if ($this->getNamespaceFromPrefix($ns)) { + $ns = $this->getNamespaceFromPrefix($ns); + } + if ($typeDef = $this->getTypeDef($uqType, $ns)) { + $this->debug("serialize elements for extension base $ns:$uqType"); + $xml .= $this->serializeComplexTypeElements($typeDef, $value, $ns, $uqType, $use, $encodingStyle); + } else { + $this->debug("extension base $ns:$uqType is not a supported type"); + } + } + return $xml; + } + + /** + * adds an XML Schema complex type to the WSDL types + * + * @param name + * @param typeClass (complexType|simpleType|attribute) + * @param phpType: currently supported are array and struct (php assoc array) + * @param compositor (all|sequence|choice) + * @param restrictionBase namespace:name (http://schemas.xmlsoap.org/soap/encoding/:Array) + * @param elements = array ( name = array(name=>'',type=>'') ) + * @param attrs = array( + * array( + * 'ref' => "http://schemas.xmlsoap.org/soap/encoding/:arrayType", + * "http://schemas.xmlsoap.org/wsdl/:arrayType" => "string[]" + * ) + * ) + * @param arrayType: namespace:name (http://www.w3.org/2001/XMLSchema:string) + * @see xmlschema + * + */ + function addComplexType($name,$typeClass='complexType',$phpType='array',$compositor='',$restrictionBase='',$elements=array(),$attrs=array(),$arrayType='') { + if (count($elements) > 0) { + foreach($elements as $n => $e){ + // expand each element + foreach ($e as $k => $v) { + $k = strpos($k,':') ? $this->expandQname($k) : $k; + $v = strpos($v,':') ? $this->expandQname($v) : $v; + $ee[$k] = $v; + } + $eElements[$n] = $ee; + } + $elements = $eElements; + } + + if (count($attrs) > 0) { + foreach($attrs as $n => $a){ + // expand each attribute + foreach ($a as $k => $v) { + $k = strpos($k,':') ? $this->expandQname($k) : $k; + $v = strpos($v,':') ? $this->expandQname($v) : $v; + $aa[$k] = $v; + } + $eAttrs[$n] = $aa; + } + $attrs = $eAttrs; + } + + $restrictionBase = strpos($restrictionBase,':') ? $this->expandQname($restrictionBase) : $restrictionBase; + $arrayType = strpos($arrayType,':') ? $this->expandQname($arrayType) : $arrayType; + + $typens = isset($this->namespaces['types']) ? $this->namespaces['types'] : $this->namespaces['tns']; + $this->schemas[$typens][0]->addComplexType($name,$typeClass,$phpType,$compositor,$restrictionBase,$elements,$attrs,$arrayType); + } + + /** + * adds an XML Schema simple type to the WSDL types + * + * @param name + * @param restrictionBase namespace:name (http://schemas.xmlsoap.org/soap/encoding/:Array) + * @param typeClass (simpleType) + * @param phpType: (scalar) + * @see xmlschema + * + */ + function addSimpleType($name, $restrictionBase='', $typeClass='simpleType', $phpType='scalar') { + $restrictionBase = strpos($restrictionBase,':') ? $this->expandQname($restrictionBase) : $restrictionBase; + + $typens = isset($this->namespaces['types']) ? $this->namespaces['types'] : $this->namespaces['tns']; + $this->schemas[$typens][0]->addSimpleType($name, $restrictionBase, $typeClass, $phpType); + } + + /** + * adds an element to the WSDL types + * + * @param name + * @param array $attrs attributes that must include name and type + * @see xmlschema + */ + function addElement($attrs) { + $typens = isset($this->namespaces['types']) ? $this->namespaces['types'] : $this->namespaces['tns']; + $this->schemas[$typens][0]->addElement($attrs); + } + + /** + * register a service with the server + * + * @param string $methodname + * @param string $in assoc array of input values: key = param name, value = param type + * @param string $out assoc array of output values: key = param name, value = param type + * @param string $namespace optional The namespace for the operation + * @param string $soapaction optional The soapaction for the operation + * @param string $style (rpc|document) optional The style for the operation + * @param string $use (encoded|literal) optional The use for the parameters (cannot mix right now) + * @param string $documentation optional The description to include in the WSDL + * @access public + */ + function addOperation($name, $in = false, $out = false, $namespace = false, $soapaction = false, $style = 'rpc', $use = 'encoded', $documentation = ''){ + if ($style == 'rpc' && $use == 'encoded') { + $encodingStyle = 'http://schemas.xmlsoap.org/soap/encoding/'; + } else { + $encodingStyle = ''; + } + + if ($style == 'document') { + $elements = array(); + foreach ($in as $n => $t) { + $elements[$n] = array('name' => $n, 'type' => $t); + } + $this->addComplexType($name . 'RequestType', 'complexType', 'struct', 'all', '', $elements); + $this->addElement(array('name' => $name, 'type' => $name . 'RequestType')); + $in = array('parameters' => 'tns:' . $name); + + $elements = array(); + foreach ($out as $n => $t) { + $elements[$n] = array('name' => $n, 'type' => $t); + } + $this->addComplexType($name . 'ResponseType', 'complexType', 'struct', 'all', '', $elements); + $this->addElement(array('name' => $name . 'Response', 'type' => $name . 'ResponseType')); + $out = array('parameters' => 'tns:' . $name . 'Response'); + } + + // get binding + $this->bindings[ $this->serviceName . 'Binding' ]['operations'][$name] = + array( + 'name' => $name, + 'binding' => $this->serviceName . 'Binding', + 'endpoint' => $this->endpoint, + 'soapAction' => $soapaction, + 'style' => $style, + 'input' => array( + 'use' => $use, + 'namespace' => $namespace, + 'encodingStyle' => $encodingStyle, + 'message' => $name . 'Request', + 'parts' => $in), + 'output' => array( + 'use' => $use, + 'namespace' => $namespace, + 'encodingStyle' => $encodingStyle, + 'message' => $name . 'Response', + 'parts' => $out), + 'namespace' => $namespace, + 'transport' => 'http://schemas.xmlsoap.org/soap/http', + 'documentation' => $documentation); + // add portTypes + // add messages + if($in) + { + foreach($in as $pName => $pType) + { + if(strpos($pType,':')) { + $pType = $this->getNamespaceFromPrefix($this->getPrefix($pType)).":".$this->getLocalPart($pType); + } + $this->messages[$name.'Request'][$pName] = $pType; + } + } else { + $this->messages[$name.'Request']= '0'; + } + if($out) + { + foreach($out as $pName => $pType) + { + if(strpos($pType,':')) { + $pType = $this->getNamespaceFromPrefix($this->getPrefix($pType)).":".$this->getLocalPart($pType); + } + $this->messages[$name.'Response'][$pName] = $pType; + } + } else { + $this->messages[$name.'Response']= '0'; + } + return true; + } +} +?> +* @version $Id$ +* @access public +*/ +class soap_parser extends nusoap_base { + + var $xml = ''; + var $xml_encoding = ''; + var $method = ''; + var $root_struct = ''; + var $root_struct_name = ''; + var $root_struct_namespace = ''; + var $root_header = ''; + var $document = ''; // incoming SOAP body (text) + // determines where in the message we are (envelope,header,body,method) + var $status = ''; + var $position = 0; + var $depth = 0; + var $default_namespace = ''; + var $namespaces = array(); + var $message = array(); + var $parent = ''; + var $fault = false; + var $fault_code = ''; + var $fault_str = ''; + var $fault_detail = ''; + var $depth_array = array(); + var $debug_flag = true; + var $soapresponse = NULL; + var $responseHeaders = ''; // incoming SOAP headers (text) + var $body_position = 0; + // for multiref parsing: + // array of id => pos + var $ids = array(); + // array of id => hrefs => pos + var $multirefs = array(); + // toggle for auto-decoding element content + var $decode_utf8 = true; + + /** + * constructor + * + * @param string $xml SOAP message + * @param string $encoding character encoding scheme of message + * @param string $method + * @param string $decode_utf8 whether to decode UTF-8 to ISO-8859-1 + * @access public + */ + function soap_parser($xml,$encoding='UTF-8',$method='',$decode_utf8=true){ + $this->xml = $xml; + $this->xml_encoding = $encoding; + $this->method = $method; + $this->decode_utf8 = $decode_utf8; + + // Check whether content has been read. + if(!empty($xml)){ + // Check XML encoding + $pos_xml = strpos($xml, '', $pos_xml + 2) - $pos_xml + 1); + if (preg_match("/encoding=[\"']([^\"']*)[\"']/", $xml_decl, $res)) { + $xml_encoding = $res[1]; + if (strtoupper($xml_encoding) != $encoding) { + $err = "Charset from HTTP Content-Type '" . $encoding . "' does not match encoding from XML declaration '" . $xml_encoding . "'"; + $this->debug($err); + if ($encoding != 'ISO-8859-1' || strtoupper($xml_encoding) != 'UTF-8') { + $this->setError($err); + return; + } + // when HTTP says ISO-8859-1 (the default) and XML says UTF-8 (the typical), assume the other endpoint is just sloppy and proceed + } else { + $this->debug('Charset from HTTP Content-Type matches encoding from XML declaration'); + } + } else { + $this->debug('No encoding specified in XML declaration'); + } + } else { + $this->debug('No XML declaration'); + } + $this->debug('Entering soap_parser(), length='.strlen($xml).', encoding='.$encoding); + // Create an XML parser - why not xml_parser_create_ns? + $this->parser = xml_parser_create($this->xml_encoding); + // Set the options for parsing the XML data. + //xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); + xml_parser_set_option($this->parser, XML_OPTION_CASE_FOLDING, 0); + xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, $this->xml_encoding); + // Set the object for the parser. + xml_set_object($this->parser, $this); + // Set the element handlers for the parser. + xml_set_element_handler($this->parser, 'start_element','end_element'); + xml_set_character_data_handler($this->parser,'character_data'); + + // Parse the XML file. + if(!xml_parse($this->parser,$xml,true)){ + // Display an error message. + $err = sprintf('XML error parsing SOAP payload on line %d: %s', + xml_get_current_line_number($this->parser), + xml_error_string(xml_get_error_code($this->parser))); + $this->debug($err); + $this->debug("XML payload:\n" . $xml); + $this->setError($err); + } else { + $this->debug('parsed successfully, found root struct: '.$this->root_struct.' of name '.$this->root_struct_name); + // get final value + $this->soapresponse = $this->message[$this->root_struct]['result']; + // get header value: no, because this is documented as XML string +// if($this->root_header != '' && isset($this->message[$this->root_header]['result'])){ +// $this->responseHeaders = $this->message[$this->root_header]['result']; +// } + // resolve hrefs/ids + if(sizeof($this->multirefs) > 0){ + foreach($this->multirefs as $id => $hrefs){ + $this->debug('resolving multirefs for id: '.$id); + $idVal = $this->buildVal($this->ids[$id]); + unset($idVal['!id']); + foreach($hrefs as $refPos => $ref){ + $this->debug('resolving href at pos '.$refPos); + $this->multirefs[$id][$refPos] = $idVal; + } + } + } + } + xml_parser_free($this->parser); + } else { + $this->debug('xml was empty, didn\'t parse!'); + $this->setError('xml was empty, didn\'t parse!'); + } + } + + /** + * start-element handler + * + * @param string $parser XML parser object + * @param string $name element name + * @param string $attrs associative array of attributes + * @access private + */ + function start_element($parser, $name, $attrs) { + // position in a total number of elements, starting from 0 + // update class level pos + $pos = $this->position++; + // and set mine + $this->message[$pos] = array('pos' => $pos,'children'=>'','cdata'=>''); + // depth = how many levels removed from root? + // set mine as current global depth and increment global depth value + $this->message[$pos]['depth'] = $this->depth++; + + // else add self as child to whoever the current parent is + if($pos != 0){ + $this->message[$this->parent]['children'] .= '|'.$pos; + } + // set my parent + $this->message[$pos]['parent'] = $this->parent; + // set self as current parent + $this->parent = $pos; + // set self as current value for this depth + $this->depth_array[$this->depth] = $pos; + // get element prefix + if(strpos($name,':')){ + // get ns prefix + $prefix = substr($name,0,strpos($name,':')); + // get unqualified name + $name = substr(strstr($name,':'),1); + } + // set status + if($name == 'Envelope'){ + $this->status = 'envelope'; + } elseif($name == 'Header'){ + $this->root_header = $pos; + $this->status = 'header'; + } elseif($name == 'Body'){ + $this->status = 'body'; + $this->body_position = $pos; + // set method + } elseif($this->status == 'body' && $pos == ($this->body_position+1)){ + $this->status = 'method'; + $this->root_struct_name = $name; + $this->root_struct = $pos; + $this->message[$pos]['type'] = 'struct'; + $this->debug("found root struct $this->root_struct_name, pos $this->root_struct"); + } + // set my status + $this->message[$pos]['status'] = $this->status; + // set name + $this->message[$pos]['name'] = htmlspecialchars($name); + // set attrs + $this->message[$pos]['attrs'] = $attrs; + + // loop through atts, logging ns and type declarations + $attstr = ''; + foreach($attrs as $key => $value){ + $key_prefix = $this->getPrefix($key); + $key_localpart = $this->getLocalPart($key); + // if ns declarations, add to class level array of valid namespaces + if($key_prefix == 'xmlns'){ + if(ereg('^http://www.w3.org/[0-9]{4}/XMLSchema$',$value)){ + $this->XMLSchemaVersion = $value; + $this->namespaces['xsd'] = $this->XMLSchemaVersion; + $this->namespaces['xsi'] = $this->XMLSchemaVersion.'-instance'; + } + $this->namespaces[$key_localpart] = $value; + // set method namespace + if($name == $this->root_struct_name){ + $this->methodNamespace = $value; + } + // if it's a type declaration, set type + } elseif($key_localpart == 'type'){ + $value_prefix = $this->getPrefix($value); + $value_localpart = $this->getLocalPart($value); + $this->message[$pos]['type'] = $value_localpart; + $this->message[$pos]['typePrefix'] = $value_prefix; + if(isset($this->namespaces[$value_prefix])){ + $this->message[$pos]['type_namespace'] = $this->namespaces[$value_prefix]; + } else if(isset($attrs['xmlns:'.$value_prefix])) { + $this->message[$pos]['type_namespace'] = $attrs['xmlns:'.$value_prefix]; + } + // should do something here with the namespace of specified type? + } elseif($key_localpart == 'arrayType'){ + $this->message[$pos]['type'] = 'array'; + /* do arrayType ereg here + [1] arrayTypeValue ::= atype asize + [2] atype ::= QName rank* + [3] rank ::= '[' (',')* ']' + [4] asize ::= '[' length~ ']' + [5] length ::= nextDimension* Digit+ + [6] nextDimension ::= Digit+ ',' + */ + $expr = '([A-Za-z0-9_]+):([A-Za-z]+[A-Za-z0-9_]+)\[([0-9]+),?([0-9]*)\]'; + if(ereg($expr,$value,$regs)){ + $this->message[$pos]['typePrefix'] = $regs[1]; + $this->message[$pos]['arrayTypePrefix'] = $regs[1]; + if (isset($this->namespaces[$regs[1]])) { + $this->message[$pos]['arrayTypeNamespace'] = $this->namespaces[$regs[1]]; + } else if (isset($attrs['xmlns:'.$regs[1]])) { + $this->message[$pos]['arrayTypeNamespace'] = $attrs['xmlns:'.$regs[1]]; + } + $this->message[$pos]['arrayType'] = $regs[2]; + $this->message[$pos]['arraySize'] = $regs[3]; + $this->message[$pos]['arrayCols'] = $regs[4]; + } + // specifies nil value (or not) + } elseif ($key_localpart == 'nil'){ + $this->message[$pos]['nil'] = ($value == 'true' || $value == '1'); + // some other attribute + } elseif ($key != 'href' && $key != 'xmlns' && $key_localpart != 'encodingStyle' && $key_localpart != 'root') { + $this->message[$pos]['xattrs']['!' . $key] = $value; + } + + if ($key == 'xmlns') { + $this->default_namespace = $value; + } + // log id + if($key == 'id'){ + $this->ids[$value] = $pos; + } + // root + if($key_localpart == 'root' && $value == 1){ + $this->status = 'method'; + $this->root_struct_name = $name; + $this->root_struct = $pos; + $this->debug("found root struct $this->root_struct_name, pos $pos"); + } + // for doclit + $attstr .= " $key=\"$value\""; + } + // get namespace - must be done after namespace atts are processed + if(isset($prefix)){ + $this->message[$pos]['namespace'] = $this->namespaces[$prefix]; + $this->default_namespace = $this->namespaces[$prefix]; + } else { + $this->message[$pos]['namespace'] = $this->default_namespace; + } + if($this->status == 'header'){ + if ($this->root_header != $pos) { + $this->responseHeaders .= "<" . (isset($prefix) ? $prefix . ':' : '') . "$name$attstr>"; + } + } elseif($this->root_struct_name != ''){ + $this->document .= "<" . (isset($prefix) ? $prefix . ':' : '') . "$name$attstr>"; + } + } + + /** + * end-element handler + * + * @param string $parser XML parser object + * @param string $name element name + * @access private + */ + function end_element($parser, $name) { + // position of current element is equal to the last value left in depth_array for my depth + $pos = $this->depth_array[$this->depth--]; + + // get element prefix + if(strpos($name,':')){ + // get ns prefix + $prefix = substr($name,0,strpos($name,':')); + // get unqualified name + $name = substr(strstr($name,':'),1); + } + + // build to native type + if(isset($this->body_position) && $pos > $this->body_position){ + // deal w/ multirefs + if(isset($this->message[$pos]['attrs']['href'])){ + // get id + $id = substr($this->message[$pos]['attrs']['href'],1); + // add placeholder to href array + $this->multirefs[$id][$pos] = 'placeholder'; + // add set a reference to it as the result value + $this->message[$pos]['result'] =& $this->multirefs[$id][$pos]; + // build complexType values + } elseif($this->message[$pos]['children'] != ''){ + // if result has already been generated (struct/array) + if(!isset($this->message[$pos]['result'])){ + $this->message[$pos]['result'] = $this->buildVal($pos); + } + // build complexType values of attributes and possibly simpleContent + } elseif (isset($this->message[$pos]['xattrs'])) { + if (isset($this->message[$pos]['nil']) && $this->message[$pos]['nil']) { + $this->message[$pos]['xattrs']['!'] = null; + } elseif (isset($this->message[$pos]['cdata']) && $this->message[$pos]['cdata'] != '') { + if (isset($this->message[$pos]['type'])) { + $this->message[$pos]['xattrs']['!'] = $this->decodeSimple($this->message[$pos]['cdata'], $this->message[$pos]['type'], isset($this->message[$pos]['type_namespace']) ? $this->message[$pos]['type_namespace'] : ''); + } else { + $parent = $this->message[$pos]['parent']; + if (isset($this->message[$parent]['type']) && ($this->message[$parent]['type'] == 'array') && isset($this->message[$parent]['arrayType'])) { + $this->message[$pos]['xattrs']['!'] = $this->decodeSimple($this->message[$pos]['cdata'], $this->message[$parent]['arrayType'], isset($this->message[$parent]['arrayTypeNamespace']) ? $this->message[$parent]['arrayTypeNamespace'] : ''); + } else { + $this->message[$pos]['xattrs']['!'] = $this->message[$pos]['cdata']; + } + } + } + $this->message[$pos]['result'] = $this->message[$pos]['xattrs']; + // set value of simpleType (or nil complexType) + } else { + //$this->debug('adding data for scalar value '.$this->message[$pos]['name'].' of value '.$this->message[$pos]['cdata']); + if (isset($this->message[$pos]['nil']) && $this->message[$pos]['nil']) { + $this->message[$pos]['xattrs']['!'] = null; + } elseif (isset($this->message[$pos]['type'])) { + $this->message[$pos]['result'] = $this->decodeSimple($this->message[$pos]['cdata'], $this->message[$pos]['type'], isset($this->message[$pos]['type_namespace']) ? $this->message[$pos]['type_namespace'] : ''); + } else { + $parent = $this->message[$pos]['parent']; + if (isset($this->message[$parent]['type']) && ($this->message[$parent]['type'] == 'array') && isset($this->message[$parent]['arrayType'])) { + $this->message[$pos]['result'] = $this->decodeSimple($this->message[$pos]['cdata'], $this->message[$parent]['arrayType'], isset($this->message[$parent]['arrayTypeNamespace']) ? $this->message[$parent]['arrayTypeNamespace'] : ''); + } else { + $this->message[$pos]['result'] = $this->message[$pos]['cdata']; + } + } + + /* add value to parent's result, if parent is struct/array + $parent = $this->message[$pos]['parent']; + if($this->message[$parent]['type'] != 'map'){ + if(strtolower($this->message[$parent]['type']) == 'array'){ + $this->message[$parent]['result'][] = $this->message[$pos]['result']; + } else { + $this->message[$parent]['result'][$this->message[$pos]['name']] = $this->message[$pos]['result']; + } + } + */ + } + } + + // for doclit + if($this->status == 'header'){ + if ($this->root_header != $pos) { + $this->responseHeaders .= ""; + } + } elseif($pos >= $this->root_struct){ + $this->document .= ""; + } + // switch status + if($pos == $this->root_struct){ + $this->status = 'body'; + $this->root_struct_namespace = $this->message[$pos]['namespace']; + } elseif($name == 'Body'){ + $this->status = 'envelope'; + } elseif($name == 'Header'){ + $this->status = 'envelope'; + } elseif($name == 'Envelope'){ + // + } + // set parent back to my parent + $this->parent = $this->message[$pos]['parent']; + } + + /** + * element content handler + * + * @param string $parser XML parser object + * @param string $data element content + * @access private + */ + function character_data($parser, $data){ + $pos = $this->depth_array[$this->depth]; + if ($this->xml_encoding=='UTF-8'){ + // TODO: add an option to disable this for folks who want + // raw UTF-8 that, e.g., might not map to iso-8859-1 + // TODO: this can also be handled with xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, "ISO-8859-1"); + if($this->decode_utf8){ + $data = utf8_decode($data); + } + } + $this->message[$pos]['cdata'] .= $data; + // for doclit + if($this->status == 'header'){ + $this->responseHeaders .= $data; + } else { + $this->document .= $data; + } + } + + /** + * get the parsed message + * + * @return mixed + * @access public + */ + function get_response(){ + return $this->soapresponse; + } + + /** + * get the parsed headers + * + * @return string XML or empty if no headers + * @access public + */ + function getHeaders(){ + return $this->responseHeaders; + } + + /** + * decodes entities + * + * @param string $text string to translate + * @access private + */ + function decode_entities($text){ + foreach($this->entities as $entity => $encoded){ + $text = str_replace($encoded,$entity,$text); + } + return $text; + } + + /** + * decodes simple types into PHP variables + * + * @param string $value value to decode + * @param string $type XML type to decode + * @param string $typens XML type namespace to decode + * @access private + */ + function decodeSimple($value, $type, $typens) { + // TODO: use the namespace! + if ((!isset($type)) || $type == 'string' || $type == 'long' || $type == 'unsignedLong') { + return (string) $value; + } + if ($type == 'int' || $type == 'integer' || $type == 'short' || $type == 'byte') { + return (int) $value; + } + if ($type == 'float' || $type == 'double' || $type == 'decimal') { + return (double) $value; + } + if ($type == 'boolean') { + if (strtolower($value) == 'false' || strtolower($value) == 'f') { + return false; + } + return (boolean) $value; + } + if ($type == 'base64' || $type == 'base64Binary') { + return base64_decode($value); + } + // obscure numeric types + if ($type == 'nonPositiveInteger' || $type == 'negativeInteger' + || $type == 'nonNegativeInteger' || $type == 'positiveInteger' + || $type == 'unsignedInt' + || $type == 'unsignedShort' || $type == 'unsignedByte') { + return (int) $value; + } + // everything else + return (string) $value; + } + + /** + * builds response structures for compound values (arrays/structs) + * + * @param string $pos position in node tree + * @access private + */ + function buildVal($pos){ + if(!isset($this->message[$pos]['type'])){ + $this->message[$pos]['type'] = ''; + } + $this->debug('inside buildVal() for '.$this->message[$pos]['name']."(pos $pos) of type ".$this->message[$pos]['type']); + // if there are children... + if($this->message[$pos]['children'] != ''){ + $children = explode('|',$this->message[$pos]['children']); + array_shift($children); // knock off empty + // md array + if(isset($this->message[$pos]['arrayCols']) && $this->message[$pos]['arrayCols'] != ''){ + $r=0; // rowcount + $c=0; // colcount + foreach($children as $child_pos){ + $this->debug("got an MD array element: $r, $c"); + $params[$r][] = $this->message[$child_pos]['result']; + $c++; + if($c == $this->message[$pos]['arrayCols']){ + $c = 0; + $r++; + } + } + // array + } elseif($this->message[$pos]['type'] == 'array' || $this->message[$pos]['type'] == 'Array' || ereg('^ArrayOf', $this->message[$pos]['type'])){ + $this->debug('adding array '.$this->message[$pos]['name']); + foreach($children as $child_pos){ + $params[] = &$this->message[$child_pos]['result']; + } + // apache Map type: java hashtable + } elseif($this->message[$pos]['type'] == 'Map' && $this->message[$pos]['type_namespace'] == 'http://xml.apache.org/xml-soap'){ + foreach($children as $child_pos){ + $kv = explode("|",$this->message[$child_pos]['children']); + $params[$this->message[$kv[1]]['result']] = &$this->message[$kv[2]]['result']; + } + // generic compound type + //} elseif($this->message[$pos]['type'] == 'SOAPStruct' || $this->message[$pos]['type'] == 'struct') { + } else { + // Apache Vector type: treat as an array + if ($this->message[$pos]['type'] == 'Vector' && $this->message[$pos]['type_namespace'] == 'http://xml.apache.org/xml-soap') { + $notstruct = 1; + } else { + $notstruct = 0; + } + // + foreach($children as $child_pos){ + if($notstruct){ + $params[] = &$this->message[$child_pos]['result']; + } else { + if (isset($params[$this->message[$child_pos]['name']])) { + // de-serialize repeated element name into an array + if ((!is_array($params[$this->message[$child_pos]['name']])) || (!isset($params[$this->message[$child_pos]['name']][0]))) { + $params[$this->message[$child_pos]['name']] = array($params[$this->message[$child_pos]['name']]); + } + $params[$this->message[$child_pos]['name']][] = &$this->message[$child_pos]['result']; + } else { + $params[$this->message[$child_pos]['name']] = &$this->message[$child_pos]['result']; + } + } + } + } + if (isset($this->message[$pos]['xattrs'])) { + foreach ($this->message[$pos]['xattrs'] as $n => $v) { + $params[$n] = $v; + } + } + return is_array($params) ? $params : array(); + } else { + $this->debug('no children'); + if(strpos($this->message[$pos]['cdata'],'&')){ + return strtr($this->message[$pos]['cdata'],array_flip($this->entities)); + } else { + return $this->message[$pos]['cdata']; + } + } + } +} + + + +?>call( string methodname [ ,array parameters] ); +* +* // bye bye client +* unset($soapclient); +* +* @author Dietrich Ayala +* @version $Id$ +* @access public +*/ +class soap_client extends nusoap_base { + + var $username = ''; + var $password = ''; + var $authtype = ''; + var $certRequest = array(); + var $requestHeaders = false; // SOAP headers in request (text) + var $responseHeaders = ''; // SOAP headers from response (incomplete namespace resolution) (text) + var $document = ''; // SOAP body response portion (incomplete namespace resolution) (text) + var $endpoint; + var $proxyhost = ''; + var $proxyport = ''; + var $proxyusername = ''; + var $proxypassword = ''; + var $xml_encoding = ''; // character set encoding of incoming (response) messages + var $http_encoding = false; + var $timeout = 0; // HTTP connection timeout + var $response_timeout = 30; // HTTP response timeout + var $endpointType = ''; // soap|wsdl, empty for WSDL initialization error + var $persistentConnection = false; + var $defaultRpcParams = false; // This is no longer used + var $request = ''; // HTTP request + var $response = ''; // HTTP response + var $responseData = ''; // SOAP payload of response + var $cookies = array(); // Cookies from response or for request + var $decode_utf8 = true; // toggles whether the parser decodes element content w/ utf8_decode() + var $operations = array(); // WSDL operations, empty for WSDL initialization error + + /** + * fault related variables + * + * @var fault + * @var faultcode + * @var faultstring + * @var faultdetail + * @access public + */ + var $fault, $faultcode, $faultstring, $faultdetail; + + /** + * constructor + * + * @param mixed $endpoint SOAP server or WSDL URL (string), or wsdl instance (object) + * @param bool $wsdl optional, set to true if using WSDL + * @param int $portName optional portName in WSDL document + * @param string $proxyhost + * @param string $proxyport + * @param string $proxyusername + * @param string $proxypassword + * @param integer $timeout set the connection timeout + * @param integer $response_timeout set the response timeout + * @access public + */ + function soap_client($endpoint,$wsdl = false,$proxyhost = false,$proxyport = false,$proxyusername = false, $proxypassword = false, $timeout = 0, $response_timeout = 30){ + $this->endpoint = $endpoint; + $this->proxyhost = $proxyhost; + $this->proxyport = $proxyport; + $this->proxyusername = $proxyusername; + $this->proxypassword = $proxypassword; + $this->timeout = $timeout; + $this->response_timeout = $response_timeout; + + // make values + if($wsdl){ + if (is_object($endpoint) && is_a($endpoint, 'wsdl')) { + $this->wsdl = $endpoint; + $this->endpoint = $this->wsdl->wsdl; + $this->wsdlFile = $this->endpoint; + $this->debug('existing wsdl instance created from ' . $this->endpoint); + } else { + $this->wsdlFile = $this->endpoint; + + // instantiate wsdl object and parse wsdl file + $this->debug('instantiating wsdl class with doc: '.$endpoint); + $this->wsdl =& new wsdl($this->wsdlFile,$this->proxyhost,$this->proxyport,$this->proxyusername,$this->proxypassword,$this->timeout,$this->response_timeout); + } + $this->appendDebug($this->wsdl->getDebug()); + $this->wsdl->clearDebug(); + // catch errors + if($errstr = $this->wsdl->getError()){ + $this->debug('got wsdl error: '.$errstr); + $this->setError('wsdl error: '.$errstr); + } elseif($this->operations = $this->wsdl->getOperations()){ + $this->debug( 'got '.count($this->operations).' operations from wsdl '.$this->wsdlFile); + $this->endpointType = 'wsdl'; + } else { + $this->debug( 'getOperations returned false'); + $this->setError('no operations defined in the WSDL document!'); + } + } else { + $this->endpointType = 'soap'; + } + } + + /** + * calls method, returns PHP native type + * + * @param string $method SOAP server URL or path + * @param array $params An array, associative or simple, of the parameters + * for the method call, or a string that is the XML + * for the call. For rpc style, this call will + * wrap the XML in a tag named after the method, as + * well as the SOAP Envelope and Body. For document + * style, this will only wrap with the Envelope and Body. + * IMPORTANT: when using an array with document style, + * in which case there + * is really one parameter, the root of the fragment + * used in the call, which encloses what programmers + * normally think of parameters. A parameter array + * *must* include the wrapper. + * @param string $namespace optional method namespace (WSDL can override) + * @param string $soapAction optional SOAPAction value (WSDL can override) + * @param boolean $headers optional array of soapval objects for headers + * @param boolean $rpcParams optional (no longer used) + * @param string $style optional (rpc|document) the style to use when serializing parameters (WSDL can override) + * @param string $use optional (encoded|literal) the use when serializing parameters (WSDL can override) + * @return mixed + * @access public + */ + function call($operation,$params=array(),$namespace='http://tempuri.org',$soapAction='',$headers=false,$rpcParams=null,$style='rpc',$use='encoded'){ + $this->operation = $operation; + $this->fault = false; + $this->setError(''); + $this->request = ''; + $this->response = ''; + $this->responseData = ''; + $this->faultstring = ''; + $this->faultcode = ''; + $this->opData = array(); + + $this->debug("call: $operation, $params, $namespace, $soapAction, $headers, $style, $use; endpointType: $this->endpointType"); + if ($headers) { + $this->requestHeaders = $headers; + } + // serialize parameters + if($this->endpointType == 'wsdl' && $opData = $this->getOperationData($operation)){ + // use WSDL for operation + $this->opData = $opData; + $this->debug("opData:"); + $this->appendDebug($this->varDump($opData)); + if (isset($opData['soapAction'])) { + $soapAction = $opData['soapAction']; + } + $this->endpoint = $opData['endpoint']; + $namespace = isset($opData['input']['namespace']) ? $opData['input']['namespace'] : $namespace; + $style = $opData['style']; + $use = $opData['input']['use']; + // add ns to ns array + if($namespace != '' && !isset($this->wsdl->namespaces[$namespace])){ + $nsPrefix = 'ns' . rand(1000, 9999); + $this->wsdl->namespaces[$nsPrefix] = $namespace; + } + $nsPrefix = $this->wsdl->getPrefixFromNamespace($namespace); + // serialize payload + if (is_string($params)) { + $this->debug("serializing param string for WSDL operation $operation"); + $payload = $params; + } elseif (is_array($params)) { + $this->debug("serializing param array for WSDL operation $operation"); + $payload = $this->wsdl->serializeRPCParameters($operation,'input',$params); + } else { + $this->debug('params must be array or string'); + $this->setError('params must be array or string'); + return false; + } + $usedNamespaces = $this->wsdl->usedNamespaces; + // Partial fix for multiple encoding styles in the same function call + $encodingStyle = 'http://schemas.xmlsoap.org/soap/encoding/'; + if (isset($opData['output']['encodingStyle']) && $encodingStyle != $opData['output']['encodingStyle']) { + $methodEncodingStyle = ' SOAP-ENV:encodingStyle="' . $opData['output']['encodingStyle'] . '"'; + } else { + $methodEncodingStyle = ''; + } + $this->appendDebug($this->wsdl->getDebug()); + $this->wsdl->clearDebug(); + if ($errstr = $this->wsdl->getError()) { + $this->debug('got wsdl error: '.$errstr); + $this->setError('wsdl error: '.$errstr); + return false; + } + } elseif($this->endpointType == 'wsdl') { + // operation not in WSDL + $this->appendDebug($this->wsdl->getDebug()); + $this->wsdl->clearDebug(); + $this->setError( 'operation '.$operation.' not present.'); + $this->debug("operation '$operation' not present."); + return false; + } else { + // no WSDL + //$this->namespaces['ns1'] = $namespace; + $nsPrefix = 'ns' . rand(1000, 9999); + // serialize + $payload = ''; + if (is_string($params)) { + $this->debug("serializing param string for operation $operation"); + $payload = $params; + } elseif (is_array($params)) { + $this->debug("serializing param array for operation $operation"); + foreach($params as $k => $v){ + $payload .= $this->serialize_val($v,$k,false,false,false,false,$use); + } + } else { + $this->debug('params must be array or string'); + $this->setError('params must be array or string'); + return false; + } + $usedNamespaces = array(); + $methodEncodingStyle = ''; + } + // wrap RPC calls with method element + if ($style == 'rpc') { + if ($use == 'literal') { + $this->debug("wrapping RPC request with literal method element"); + if ($namespace) { + $payload = "<$operation xmlns=\"$namespace\">" . $payload . ""; + } else { + $payload = "<$operation>" . $payload . ""; + } + } else { + $this->debug("wrapping RPC request with encoded method element"); + if ($namespace) { + $payload = "<$nsPrefix:$operation$methodEncodingStyle xmlns:$nsPrefix=\"$namespace\">" . + $payload . + ""; + } else { + $payload = "<$operation$methodEncodingStyle>" . + $payload . + ""; + } + } + } + // serialize envelope + $soapmsg = $this->serializeEnvelope($payload,$this->requestHeaders,$usedNamespaces,$style,$use); + $this->debug("endpoint: $this->endpoint, soapAction: $soapAction, namespace: $namespace, style: $style, use: $use"); + $this->debug('SOAP message length: ' . strlen($soapmsg) . ' contents: ' . substr($soapmsg, 0, 1000)); + // send + $return = $this->send($this->getHTTPBody($soapmsg),$soapAction,$this->timeout,$this->response_timeout); + if($errstr = $this->getError()){ + $this->debug('Error: '.$errstr); + return false; + } else { + $this->return = $return; + $this->debug('sent message successfully and got a(n) '.gettype($return).' back: '); + $this->appendDebug($this->varDump($return)); + + // fault? + if(is_array($return) && isset($return['faultcode'])){ + $this->debug('got fault'); + $this->setError($return['faultcode'].': '.$return['faultstring']); + $this->fault = true; + foreach($return as $k => $v){ + $this->$k = $v; + $this->debug("$k = $v
"); + } + return $return; + } elseif ($style == 'document') { + // NOTE: if the response is defined to have multiple parts (i.e. unwrapped), + // we are only going to return the first part here...sorry about that + return $return; + } else { + // array of return values + if(is_array($return)){ + // multiple 'out' parameters, which we return wrapped up + // in the array + if(sizeof($return) > 1){ + return $return; + } + // single 'out' parameter (normally the return value) + $return = array_shift($return); + $this->debug('return shifted value: '); + $this->appendDebug($this->varDump($return)); + return $return; + // nothing returned (ie, echoVoid) + } else { + return ""; + } + } + } + } + + /** + * get available data pertaining to an operation + * + * @param string $operation operation name + * @return array array of data pertaining to the operation + * @access public + */ + function getOperationData($operation){ + if(isset($this->operations[$operation])){ + return $this->operations[$operation]; + } + $this->debug("No data for operation: $operation"); + } + + /** + * send the SOAP message + * + * Note: if the operation has multiple return values + * the return value of this method will be an array + * of those values. + * + * @param string $msg a SOAPx4 soapmsg object + * @param string $soapaction SOAPAction value + * @param integer $timeout set connection timeout in seconds + * @param integer $response_timeout set response timeout in seconds + * @return mixed native PHP types. + * @access private + */ + function send($msg, $soapaction = '', $timeout=0, $response_timeout=30) { + $this->checkCookies(); + // detect transport + switch(true){ + // http(s) + case ereg('^http',$this->endpoint): + $this->debug('transporting via HTTP'); + if($this->persistentConnection == true && is_object($this->persistentConnection)){ + $http =& $this->persistentConnection; + } else { + $http = new soap_transport_http($this->endpoint); + if ($this->persistentConnection) { + $http->usePersistentConnection(); + } + } + $http->setContentType($this->getHTTPContentType(), $this->getHTTPContentTypeCharset()); + $http->setSOAPAction($soapaction); + if($this->proxyhost && $this->proxyport){ + $http->setProxy($this->proxyhost,$this->proxyport,$this->proxyusername,$this->proxypassword); + } + if($this->authtype != '') { + $http->setCredentials($this->username, $this->password, $this->authtype, array(), $this->certRequest); + } + if($this->http_encoding != ''){ + $http->setEncoding($this->http_encoding); + } + $this->debug('sending message, length: '.strlen($msg)); + if(ereg('^http:',$this->endpoint)){ + //if(strpos($this->endpoint,'http:')){ + $this->responseData = $http->send($msg,$timeout,$response_timeout,$this->cookies); + } elseif(ereg('^https',$this->endpoint)){ + //} elseif(strpos($this->endpoint,'https:')){ + //if(phpversion() == '4.3.0-dev'){ + //$response = $http->send($msg,$timeout,$response_timeout); + //$this->request = $http->outgoing_payload; + //$this->response = $http->incoming_payload; + //} else + $this->responseData = $http->sendHTTPS($msg,$timeout,$response_timeout,$this->cookies); + } else { + $this->setError('no http/s in endpoint url'); + } + $this->request = $http->outgoing_payload; + $this->response = $http->incoming_payload; + $this->appendDebug($http->getDebug()); + $this->UpdateCookies($http->incoming_cookies); + + // save transport object if using persistent connections + if ($this->persistentConnection) { + $http->clearDebug(); + if (!is_object($this->persistentConnection)) { + $this->persistentConnection = $http; + } + } + + if($err = $http->getError()){ + $this->setError('HTTP Error: '.$err); + return false; + } elseif($this->getError()){ + return false; + } else { + $this->debug('got response, length: '. strlen($this->responseData).' type: '.$http->incoming_headers['content-type']); + return $this->parseResponse($http->incoming_headers, $this->responseData); + } + break; + default: + $this->setError('no transport found, or selected transport is not yet supported!'); + return false; + break; + } + } + + /** + * processes SOAP message returned from server + * + * @param array $headers The HTTP headers + * @param string $data unprocessed response data from server + * @return mixed value of the message, decoded into a PHP type + * @access protected + */ + function parseResponse($headers, $data) { + $this->debug('Entering parseResponse() for data of length ' . strlen($data) . ' and type ' . $headers['content-type']); + if (!strstr($headers['content-type'], 'text/xml')) { + $this->setError('Response not of type text/xml'); + return false; + } + if (strpos($headers['content-type'], '=')) { + $enc = str_replace('"', '', substr(strstr($headers["content-type"], '='), 1)); + $this->debug('Got response encoding: ' . $enc); + if(eregi('^(ISO-8859-1|US-ASCII|UTF-8)$',$enc)){ + $this->xml_encoding = strtoupper($enc); + } else { + $this->xml_encoding = 'US-ASCII'; + } + } else { + // should be US-ASCII for HTTP 1.0 or ISO-8859-1 for HTTP 1.1 + $this->xml_encoding = 'ISO-8859-1'; + } + $this->debug('Use encoding: ' . $this->xml_encoding . ' when creating soap_parser'); + $parser = new soap_parser($data,$this->xml_encoding,$this->operation,$this->decode_utf8); + // add parser debug data to our debug + $this->appendDebug($parser->getDebug()); + // if parse errors + if($errstr = $parser->getError()){ + $this->setError( $errstr); + // destroy the parser object + unset($parser); + return false; + } else { + // get SOAP headers + $this->responseHeaders = $parser->getHeaders(); + // get decoded message + $return = $parser->get_response(); + // add document for doclit support + $this->document = $parser->document; + // destroy the parser object + unset($parser); + // return decode message + return $return; + } + } + + /** + * set the SOAP headers + * + * @param $headers string XML + * @access public + */ + function setHeaders($headers){ + $this->requestHeaders = $headers; + } + + /** + * get the response headers + * + * @return string + * @access public + */ + function getHeaders(){ + return $this->responseHeaders; + } + + /** + * set proxy info here + * + * @param string $proxyhost + * @param string $proxyport + * @param string $proxyusername + * @param string $proxypassword + * @access public + */ + function setHTTPProxy($proxyhost, $proxyport, $proxyusername = '', $proxypassword = '') { + $this->proxyhost = $proxyhost; + $this->proxyport = $proxyport; + $this->proxyusername = $proxyusername; + $this->proxypassword = $proxypassword; + } + + /** + * if authenticating, set user credentials here + * + * @param string $username + * @param string $password + * @param string $authtype (basic|digest|certificate) + * @param array $certRequest (keys must be cainfofile, sslcertfile, sslkeyfile, passphrase: see corresponding options in cURL docs) + * @access public + */ + function setCredentials($username, $password, $authtype = 'basic', $certRequest = array()) { + $this->username = $username; + $this->password = $password; + $this->authtype = $authtype; + $this->certRequest = $certRequest; + } + + /** + * use HTTP encoding + * + * @param string $enc + * @access public + */ + function setHTTPEncoding($enc='gzip, deflate'){ + $this->http_encoding = $enc; + } + + /** + * use HTTP persistent connections if possible + * + * @access public + */ + function useHTTPPersistentConnection(){ + $this->persistentConnection = true; + } + + /** + * gets the default RPC parameter setting. + * If true, default is that call params are like RPC even for document style. + * Each call() can override this value. + * + * This is no longer used. + * + * @access public + * @deprecated + */ + function getDefaultRpcParams() { + return $this->defaultRpcParams; + } + + /** + * sets the default RPC parameter setting. + * If true, default is that call params are like RPC even for document style + * Each call() can override this value. + * + * This is no longer used. + * + * @param boolean $rpcParams + * @access public + * @deprecated + */ + function setDefaultRpcParams($rpcParams) { + $this->defaultRpcParams = $rpcParams; + } + + /** + * dynamically creates proxy class, allowing user to directly call methods from wsdl + * + * @return object soap_proxy object + * @access public + */ + function getProxy(){ + if ($this->endpointType != 'wsdl') { + $this->setError('The getProxy method only works for a WSDL client'); + return null; + } + $evalStr = ''; + foreach($this->operations as $operation => $opData){ + if($operation != ''){ + // create param string + $paramStr = ''; + if(sizeof($opData['input']['parts']) > 0){ + foreach($opData['input']['parts'] as $name => $type){ + $paramStr .= "\$$name,"; + } + $paramStr = substr($paramStr,0,strlen($paramStr)-1); + } + $opData['namespace'] = !isset($opData['namespace']) ? 'http://testuri.com' : $opData['namespace']; + $evalStr .= "function " . str_replace('.', '__', $operation) . " ($paramStr) { + // load params into array + \$params = array($paramStr); + return \$this->call('$operation',\$params,'".$opData['namespace']."','".(isset($opData['soapAction']) ? $opData['soapAction'] : '')."'); + }"; + unset($paramStr); + } + } + $r = rand(); + $evalStr = 'class soap_proxy_'.$r.' extends soap_client { + '.$evalStr.' + }'; + //print "proxy class:
$evalStr
"; + // eval the class + eval($evalStr); + // instantiate proxy object + eval("\$proxy = new soap_proxy_$r('');"); + // transfer current wsdl data to the proxy thereby avoiding parsing the wsdl twice + $proxy->endpointType = 'wsdl'; + $proxy->wsdlFile = $this->wsdlFile; + $proxy->wsdl = $this->wsdl; + $proxy->operations = $this->operations; + $proxy->defaultRpcParams = $this->defaultRpcParams; + // transfer other state + $proxy->username = $this->username; + $proxy->password = $this->password; + $proxy->authtype = $this->authtype; + $proxy->proxyhost = $this->proxyhost; + $proxy->proxyport = $this->proxyport; + $proxy->proxyusername = $this->proxyusername; + $proxy->proxypassword = $this->proxypassword; + $proxy->timeout = $this->timeout; + $proxy->response_timeout = $this->response_timeout; + $proxy->http_encoding = $this->http_encoding; + $proxy->persistentConnection = $this->persistentConnection; + $proxy->requestHeaders = $this->requestHeaders; + $proxy->soap_defencoding = $this->soap_defencoding; + return $proxy; + } + + /** + * gets the HTTP body for the current request. + * + * @param string $soapmsg The SOAP payload + * @return string The HTTP body, which includes the SOAP payload + * @access protected + */ + function getHTTPBody($soapmsg) { + return $soapmsg; + } + + /** + * gets the HTTP content type for the current request. + * + * Note: getHTTPBody must be called before this. + * + * @return string the HTTP content type for the current request. + * @access protected + */ + function getHTTPContentType() { + return 'text/xml'; + } + + /** + * gets the HTTP content type charset for the current request. + * returns false for non-text content types. + * + * Note: getHTTPBody must be called before this. + * + * @return string the HTTP content type charset for the current request. + * @access protected + */ + function getHTTPContentTypeCharset() { + return $this->soap_defencoding; + } + + /* + * whether or not parser should decode utf8 element content + * + * @return always returns true + * @access public + */ + function decodeUTF8($bool){ + $this->decode_utf8 = $bool; + return true; + } + + /** + * adds a new Cookie into $this->cookies array + * + * @param string $name Cookie Name + * @param string $value Cookie Value + * @return if cookie-set was successful returns true, else false + * @access public + */ + function setCookie($name, $value) { + if (strlen($name) == 0) { + return false; + } + $this->cookies[] = array('name' => $name, 'value' => $value); + return true; + } + + /** + * gets all Cookies + * + * @return array with all internal cookies + * @access public + */ + function getCookies() { + return $this->cookies; + } + + /** + * checks all Cookies and delete those which are expired + * + * @return always return true + * @access public + */ + function checkCookies() { + if (sizeof($this->cookies) == 0) { + return true; + } + $this->debug('checkCookie: check ' . sizeof($this->cookies) . ' cookies'); + $curr_cookies = $this->cookies; + $this->cookies = array(); + foreach ($curr_cookies as $cookie) { + if (! is_array($cookie)) { + $this->debug('Remove cookie that is not an array'); + continue; + } + if ((isset($cookie['expires'])) && (! empty($cookie['expires']))) { + if (strtotime($cookie['expires']) > time()) { + $this->cookies[] = $cookie; + } else { + $this->debug('Remove expired cookie ' . $cookie['name']); + } + } else { + $this->cookies[] = $cookie; + } + } + $this->debug('checkCookie: '.sizeof($this->cookies).' cookies left in array'); + return true; + } + + /** + * updates the current cookies with a new set + * + * @param array $cookies new cookies with which to update current ones + * @return always return true + * @access public + */ + function UpdateCookies($cookies) { + if (sizeof($this->cookies) == 0) { + // no existing cookies: take whatever is new + if (sizeof($cookies) > 0) { + $this->debug('Setting new cookie(s)'); + $this->cookies = $cookies; + } + return true; + } + if (sizeof($cookies) == 0) { + // no new cookies: keep what we've got + return true; + } + // merge + foreach ($cookies as $newCookie) { + if (!is_array($newCookie)) { + continue; + } + if ((!isset($newCookie['name'])) || (!isset($newCookie['value']))) { + continue; + } + $newName = $newCookie['name']; + + $found = false; + for ($i = 0; $i < count($this->cookies); $i++) { + $cookie = $this->cookies[$i]; + if (!is_array($cookie)) { + continue; + } + if (!isset($cookie['name'])) { + continue; + } + if ($newName != $cookie['name']) { + continue; + } + $newDomain = isset($newCookie['domain']) ? $newCookie['domain'] : 'NODOMAIN'; + $domain = isset($cookie['domain']) ? $cookie['domain'] : 'NODOMAIN'; + if ($newDomain != $domain) { + continue; + } + $newPath = isset($newCookie['path']) ? $newCookie['path'] : 'NOPATH'; + $path = isset($cookie['path']) ? $cookie['path'] : 'NOPATH'; + if ($newPath != $path) { + continue; + } + $this->cookies[$i] = $newCookie; + $found = true; + $this->debug('Update cookie ' . $newName . '=' . $newCookie['value']); + break; + } + if (! $found) { + $this->debug('Add cookie ' . $newName . '=' . $newCookie['value']); + $this->cookies[] = $newCookie; + } + } + return true; + } +} + +// Backwards compatibility definition +// This won't work where native PHP SOAP support is present +// so the new name should be used whenever possible +if (!class_exists('SoapClient')) { + class soapclient extends soap_client { + } +} +?> diff --git a/mod/quiz/questiontypes/rqp/questiontype.php b/mod/quiz/questiontypes/rqp/questiontype.php new file mode 100644 index 00000000000..52ad1751da0 --- /dev/null +++ b/mod/quiz/questiontypes/rqp/questiontype.php @@ -0,0 +1,460 @@ +dirroot . '/mod/quiz/questiontypes/rqp/lib.php'); +require_once('remote.php'); + +/** +* RQP question type class +*/ +class quiz_rqp_qtype extends quiz_default_questiontype { + + /** + * Name of the rqp question type + * + * @ return string 'rqp' + */ + function name() { + return 'rqp'; + } + + /** + * Save the type-specific options + * + * This also saves additional information that it receives from + * an RQP_itemInformation call to the RQP server + */ + function save_question_options($form) { + global $CFG; + + // Check source type + if (!$type = get_record('quiz_rqp_type', 'id', $form->type)) { + $result->notice = get_string('invalidsourcetype', 'quiz'); + return $result; + } + + // Create the object to be stored in quiz_rqp table + $options = new object; + $options->question = $form->id; + $options->type = $form->type; + $options->type_name = $type->name; + $options->source = $form->source; + $options->format = $form->format; + $options->rendering_server = $type->rendering_server; + $options->cloning_server = $type->cloning_server; + + // Check source file + $item = remote_item_info($options); + if (false === $item || is_soap_fault($item)) { + $result->notice = get_string('invalidsource', 'quiz', $options); + return $result; + } + if ($item->sourceErrors) { + array_walk($item->sourceErrors, + create_function('&$val', '$val = $val->message;')); + $result->notice = get_string('invalidsource', 'quiz', $options) . + '
' . implode('
', array_values($item->sourceErrors)); + return $result; + } + // Time dependent items are not supported by the quiz module yet + if ($item->timeDependent) { + $result->noticeyesno = get_string('notimedependentitems', 'quiz'); + return $result; + } + + // Set the format and item specific flags + $options->format = $item->format; + $options->maxscore = $item->maxScore; + $options->flags = 0; + $options->flags |= $item->template ? REMOTE_TEMPLATE : 0; + $options->flags |= $item->adaptive ? REMOTE_ADAPTIVE : 0; + + // Check for cloning support if item is a template + if ($item->template && !($type->flags & REMOTE_TEMPLATES_SUPPORTED)) { + $result->notice = get_string('itemrequirescloning', 'quiz'); + return $result; + } + // Save the options + if ($old = get_record('quiz_rqp', 'question', $form->id)) { + $old->type = $options->type; + $old->source = $options->source; + $old->format = $options->format; + $old->flags = $options->flags; + $old->maxscore = $options->maxscore; + if (!update_record('quiz_rqp', $old)) { + $result->error = "Could not update quiz rqp options! (id=$old->id)"; + return $result; + } + } else { + if (!insert_record('quiz_rqp', $options)) { + $result->error = 'Could not insert quiz rqp options!'; + return $result; + } + } + return true; + } + + /** + * Loads the question type specific options for the question. + * + * This function loads all question type specific options for the + * question from the database into the $question->options field. + * @return bool Indicates success or failure. + * @param object $question The question object for the question. + */ + function get_question_options(&$question) { + + $options =& $question->options; + if (! ($options = get_record('quiz_rqp', 'question', $question->id))) { + return false; + } + if (!$type = get_record('quiz_rqp_type', 'id', $options->type)) { + return false; + } + $options->type_name = $type->name; + $options->rendering_server = $type->rendering_server; + $options->cloning_server = $type->cloning_server; + $options->flags &= $type->flags; + return true; + } + + /** + * Return a value or array of values which will give full marks if graded as + * the $state->responses field + * + * The correct answers are obtained from the RQP server via the + * RQP_SessionInformation operation + * @return mixed An array of values giving the responses corresponding + * to the (or a) correct answer to the question. + * @param object $question The question for which the correct answer is to + * be retrieved. + * @param object $state The state object that corresponds to the question, + * for which a correct answer is needed. + */ + function get_correct_responses(&$question, &$state) { + $info = remote_session_info($question, $state); + if (false === $info || is_soap_fault($info)) { + return null; + } + return $info->correctResponses; + } + + /** + * Creates empty session and response information for the question + * + * This function is called to start a question session. Empty question type + * specific session data and empty response data is added to the state object. + * @return bool Indicates success or failure. + * @param object $question The question for which the session is to be created. + * @param object $state The state to create the session for. This is passed by + * reference and will be updated. + * @param object $quiz The quiz for which the session is to be started. + * (not used) + * @param object $attempt The quiz attempt for which the session is to be + * started. (not used) + */ + function create_session_and_responses(&$question, &$state, $quiz, $attempt) { + $state->responses = array('' => ''); + $state->options->persistent_data = ''; + $state->options->template_vars = array(); + return true; + } + + /** + * Restores the session data and most recent responses for the given state + * + * This function loads any session data associated with the question session + * in the given state from the quiz_rqp_states table into the state object. + * @return bool Indicates success or failure. + * @param object $question The question object for the question including any + * question type specific information. + * @param object $state The saved state to load the session for. This + * object is updated to include the question + * type specific session information and responses + * (it is passed by reference). + */ + function restore_session_and_responses(&$question, &$state) { + if (!$options = get_record('quiz_rqp_states', 'stateid', $state->id)) { + return false; + } + $state->responses = quiz_rqp_explode($options->responses); + $state->options->persistent_data = $options->persistent_data; + $state->options->template_vars = + quiz_rqp_explode($options->template_vars, true); + return true; + } + + /** + * Saves the session data and responses for the question in a new state + * + * This function saves all session data from the state object into the + * quiz_rqp_states table + * @return bool Indicates success or failure. + * @param object $question The question object for the question including + * the question type specific information. + * @param object $state The state for which the question type specific + * data and responses should be saved. + */ + function save_session_and_responses(&$question, &$state) { + $options->stateid = $state->id; + $options->responses = quiz_rqp_implode($state->responses); + $options->persistent_data = $state->options->persistent_data; + $options->template_vars = + quiz_rqp_implode($state->options->template_vars); + if (!insert_record('quiz_rqp_states', $options)) { + return false; + } + return true; + } + + /** + * Prints the main content of the question including any interactions + * + * This function prints the main content of the question which it obtains + * from the RQP server via the Render operation. It also updates + * $state->options->persistent_data and $state->options->template_vars + * with the values returned by the RQP server. + * @param object $question The question to be rendered. + * @param object $state The state to render the question in. The grading + * information is in ->grade, ->raw_grade and + * ->penalty. The current responses are in + * ->responses. The last graded state is in + * ->last_graded (hence the most recently graded + * responses are in ->last_graded->responses). + * @param object $quiz The quiz to which the question belongs. + * @param object $options An object describing the rendering options. + * The fields are: + * ->readonly Review / interactive mode + * ->feedback Show feedback for the graded + * responses + * ->validation Show how the current responses + * responses were interpreted + * ->correct_responses Show solutions + * These are all boolean values. + */ + function print_question_formulation_and_controls(&$question, &$state, + $quiz, $options) { + + // Use the render output created during grading if it exists + if (isset($state->options->renderoutput)) { + $output =& $state->options->renderoutput; + } else { + // Otherwise perform a render operation + $output = remote_render($question, $state, false, + $options->readonly ? 'readonly' : 'normal'); + if (false === $output || is_soap_fault($output)) { + unset($output); + notify('Error: Rendering failed!'); + exit; + } + } + $state->options->persistent_data = $output->persistentData; + $state->options->template_vars = $output->templateVars; + // Print the head (this may not work, really it should be in the head + // section of the html document but moodle doesn't allow question types + // to put things there) + if (isset($output->output[RQP_URI_COMPONENT . 'head'])) { + echo $output->output[RQP_URI_COMPONENT . 'head']->output; + } + // Print the title + if (isset($output->output[RQP_URI_COMPONENT . 'title'])) { + echo '

' . $output->output[RQP_URI_COMPONENT . 'title']->output + . "

\n"; + } + // Print the stem + if (isset($output->output[RQP_URI_COMPONENT . 'stem'])) { + echo '
'; + echo $output->output[RQP_URI_COMPONENT . 'stem']->output; + echo '
'; + } + // Print the interactions + if (isset($output->output[RQP_URI_COMPONENT . 'interactions'])) { + echo '
'; + echo $output->output[RQP_URI_COMPONENT . 'interactions']->output; + echo '
'; + } + // Print the last answer + if (isset($output->output[RQP_URI_COMPONENT . 'lastAnswer'])) { + echo '
'; + echo $output->output[RQP_URI_COMPONENT . 'lastAnswer']->output; + echo '
'; + } + // Print the validation when required + if ($options->validation) { + if (isset($output->output[RQP_URI_COMPONENT . 'validation'])) { + echo '
'; + echo $output->output[RQP_URI_COMPONENT . 'validation']->output; + echo '
'; + } + } + // Print the feedback when required + if ($options->feedback) { + if (isset($output->output[RQP_URI_COMPONENT . 'feedback'])) { + echo '
'; + echo $output->output[RQP_URI_COMPONENT . 'feedback']->output; + echo '
'; + } + } + // Print the solution when required + if ($options->correct_responses) { + if (isset($output->output[RQP_URI_COMPONENT . 'solution'])) { + echo $output->output[RQP_URI_COMPONENT . 'solution']->output; + echo ''; + } + } + // Note: hint(s) and modal feedback are ignored; moodle does not support + // them yet. + // Remove the render output created during grading (if any) + unset($state->options->renderoutput); + } + + /** + * Prints the submit and validate buttons + * @param object $question The question for which the buttons are to be printed + * @param object $state The state the question is in (not used) + * @param object $quiz The quiz to which the question belongs. + * @param object $options An object describing the rendering options. + * (not used. This function should only have been called + * if the options were such that the buttons are required) + */ + function print_question_submit_buttons(&$question, &$state, $quiz, $options) { + echo ' '; + if ($quiz->optionflags & QUIZ_ADAPTIVE) { + echo ''; + } + } + + /** + * Renders the question for printing and returns the LaTeX source produced + * + * This function calls the RQP_Render operation to render the question + * suitable for a printed problem or solution sheet in LaTeX and returns + * the rendered output. It does not advance the state of the question session. + * @return string The LaTeX output. + * @param object $question The question to be rendered. + * @param object $state The state to render the question in. + * @param object $quiz The quiz to which the question belongs. + * @param string $type Indicates if the question or the solution is to be + * rendered. Possible values are 'question' and + * 'solution'. + */ + /*function get_texsource(&$question, &$state, $quiz, $type) { + // Perform the RQP render operation requesting tex output + $output = remote_render($question, $state, 'print', RQP_URI_STATE . + ('solution' === $type ? 'solution' : 'normal')); + if (false === $output || is_soap_fault($output)) { + notify('Error: Rendering failed!'); + exit; + } + if (isset($output->bodyParts[RQP_URI_FORMAT . 'text/latex2e-parts/body'])) { + $output = $output->bodyParts[RQP_URI_FORMAT . 'text/latex2e-parts/body']->content; + } + else { + $output = ''; + } + return $output; + }*/ + + /** + * Performs response processing and grading + * + * This function calls RQP_Render to perform response processing and grading + * and updates the state accordingly. It also caches the rendering output in case + * it is needed later. + * @return boolean Indicates success or failure. + * @param object $question The question to be graded. + * @param object $state The state to render the question in. The grading + * information is in ->grade, ->raw_grade and + * ->penalty. The currently responses are in + * ->responses. The last graded state is in + * ->last_graded (hence the most recently graded + * responses are in ->last_graded->responses). The + * question type specific information is also + * included. The ->raw_grade and ->penalty fields + * are updated. The ->grade field is computed + * automatically. The cumulative penalty is set + * in ->penalty by adding to the penalty from the + * most recently graded state. The method is able to + * close the question session (preventing any further + * attempts at this question) by setting + * $state->event to QUIZ_EVENTCLOSE. + * @param object $quiz The quiz to which the question belongs. + */ + function grade_responses(&$question, &$state, $quiz) { + // Perform the grading and rendering + $output = remote_render($question, $state, QUIZ_EVENTGRADE == $state->event + || QUIZ_EVENTCLOSE == $state->event, 'normal'); + if (false === $output || is_soap_fault($output)) { + unset($output); + return false; + } + $state->options->persistent_data = $output->persistentData; + $state->options->template_vars = $output->templateVars; + // Save the rendering results for later + $state->options->renderoutput = $output; + if (isset($output->outcomeVars[RQP_URI_OUTCOME . 'rawScore'])) { + $state->raw_grade = (float) $output->outcomeVars[RQP_URI_OUTCOME . + 'rawScore'][0]; + if (isset($output->outcomeVars[RQP_URI_OUTCOME . 'penalty'])) { + $state->penalty = (float) $output->outcomeVars[RQP_URI_OUTCOME . + 'penalty'][0]; + } else { + $state->penalty = 0; + } + } else if (isset($output->outcomeVars[RQP_URI_OUTCOME . 'grade'])) { + // This won't work quite as we would like but it is the best we can + // do given that the server won't tell us the information we need + $state->raw_grade = (float) $output->outcomeVars[RQP_URI_OUTCOME . + 'grade'][0]; + $state->penalty = 0; + } else { + $state->raw_grade = 0; + $state->penalty = 0; + } + $state->raw_grade = ($state->raw_grade * ((float) $question->maxgrade)) + / ((float) $question->options->maxscore); + return true; + } + + /** + * Includes configuration settings for the question type on the quiz admin + * page + * + * Returns an array of objects describing the options for the question type + * to be included on the quiz module admin page. + * This is currently only a link to the server setup page types.php + * @return array Array of objects describing the configuration options to + * be included on the quiz module admin page. + */ + function get_config_options() { + global $CFG; + + $link->name = 'managetypes'; + $link->link = 'types.php'; + return array($link); + } + +} +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[RQP]= new quiz_rqp_qtype(); + +?> diff --git a/mod/quiz/questiontypes/rqp/remote.php b/mod/quiz/questiontypes/rqp/remote.php new file mode 100644 index 00000000000..0080898358c --- /dev/null +++ b/mod/quiz/questiontypes/rqp/remote.php @@ -0,0 +1,205 @@ +rendering_server +* @return boolean Indicates success or failure +*/ +function remote_rendering_connect($options) { + global $remote_connections; + + if (!array_key_exists($options->rendering_server, $remote_connections)) { + $remote_connections[$options->rendering_server] = + rqp_connect($options->rendering_server); + } + if (!$remote_connections[$options->rendering_server]) { + unset($remote_connections[$options->rendering_server]); + return false; + } + return true; +} + +/** +* Create connection to cloning server if it does not already exist +* +* @param object $options This holds in particular the URL of the rendering +* server in $options->cloning_server +* @return boolean Indicates success or failure +*/ +function remote_cloning_connect($options) { + global $remote_connections; + + if (!array_key_exists($options->cloning_server, $remote_connections)) { + $remote_connections[$options->cloning_server] = + rqp_connect($options->cloning_server); + } + if (!$remote_connections[$options->cloning_server]) { + unset($remote_connections[$options->cloning_server]); + return false; + } + return true; +} + +function remote_server_info($options, $rendering_server=true, + $cloning_server=true) { + global $remote_connections; + + $return = array(); + if ($rendering_server) { + if (remote_rendering_connect($options)) { + $return['rendering'] = + rqp_server_info($remote_connections[$options->rendering_server]); + if (is_soap_fault($return['rendering'])) { + $return['rendering'] = false; + } + } + else { + $return['rendering'] = false; + } + } + if ($cloning_server) { + if (remote_cloning_connect($options)) { + $return['cloning'] = + rqp_server_info($remote_connections[$options->cloning_server]); + if (is_soap_fault($return['cloning'])) { + $return['cloning'] = false; + } + } + else { + $return['cloning'] = false; + } + } + return $return; +} + +function remote_item_info(&$options, $allinpackage=false) { + global $remote_connections; + + if (!remote_rendering_connect($options)) { + return false; + } + if ($allinpackage) { + $items = rqp_item_info($remote_connections[$options->rendering_server], + $options->source, $options->format, 0); + if ($items->length > 0) { + $items = array($items); + for ($i = 1; $i < $items[0]->length; $i++) { + $items[$i] = + rqp_item_info($remote_connections[$options->rendering_server], + $options->source, $options->format, $i); + } + } + return $items; + } + return rqp_item_info($remote_connections[$options->rendering_server], + $options->source, $options->format, 0); +} + +/** + * Perform a remote rendering operation on the RQP question + * + * @param string $output One of 'normal', 'readonly' or 'print'. + */ +function remote_render(&$question, &$state, $advanceState=false, + $output='normal', $from=0, $to=0) { + global $remote_connections; + + // Make the code more readable + $options =& $question->options; + + // Add prefix to response variable names + $responses = array(); + foreach ($state->responses as $key => $resp) { + $responses[$question->name_prefix . $key] = $resp; + } + + // Prepare the render format + if ('print' === $output) { + $renderFormat = RQP_URI_FORMAT . 'latex-2e'; + } else if ('readonly' === $output) { + $renderFormat = RQP_URI_FORMAT . 'xhtml-1.0-print'; + } else { + $renderFormat = RQP_URI_FORMAT . 'xhtml-1.0-web'; + } + // Perform the RQP operation + if (!remote_rendering_connect($options)) { + return false; + } + $output = array(); + for ($i = $from; $i <= $to; $i++) { + $output[$i] = rqp_render($remote_connections[$options->rendering_server], + $options->source, $options->format, $state->options->persistent_data, + $question->name_prefix, $responses, $advanceState, + $renderFormat, $state->options->template_vars, 0, '', '', '', $i); + if (false === $output[$i] || is_soap_fault($output[$i])) { + return $output[$i]; + } + } + if ($from == $to) { + return $output[$from]; + } + return $output; +} + +function remote_session_info(&$question, &$state, $from=0, $to=0) { + global $remote_connections; + + // Make the code more readable + $options =& $question->options; + + // Perform the RQP operation + if (!remote_rendering_connect($options)) { + return false; + } + $info = array(); + for ($i = $from; $i <= $to; $i++) { + $info[$i] = rqp_session_info($remote_connections[$options->rendering_server], + $options->source, $options->format, $state->options->persistent_data, + $state->options->template_vars, 0, '', $i); + if (false === $info[$i] || is_soap_fault($info[$i])) { + return $info[$i]; + } + } + if ($from == $to) { + return $info[$from]; + } + return $info; +} + +function remote_clone($options, $vars=array()) { + global $remote_connections; + + if (!remote_cloning_connect($options)) { + return false; + } + return rqp_clone($remote_connections[$options->cloning_server], + $options->source, $options->format, $vars); +} + +?> diff --git a/mod/quiz/questiontypes/rqp/rqp.html b/mod/quiz/questiontypes/rqp/rqp.html new file mode 100644 index 00000000000..ecbceba6a19 --- /dev/null +++ b/mod/quiz/questiontypes/rqp/rqp.html @@ -0,0 +1,83 @@ +' . get_string('notypes', 'quiz') . "

\n"; + if (isadmin()) { + echo '

'.get_string('additemtype', 'quiz')."

\n"; + } +} +else { // otherwise display the page as normal +?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +qtype]->print_replacement_options($question, $course, $contextquiz); + $QUIZ_QTYPES[$question->qtype]->print_question_form_end($question); +} // end of else block for when some types have been defined +?> +
: + id, true, true, $question->category); ?> +
: + + + + + +
: + + \n"; + foreach ($types as $type) { + echo '\n"; + } + echo "\n"; + if (isset($err['type'])) { + formerr($err['type']); + } + ?> +
: + + options->source); + echo "
\n"; + if (isset($err['source'])) { + formerr($err['source']); + } + ?> +
+ + +
+
diff --git a/mod/quiz/questiontypes/rqp/rqp.php b/mod/quiz/questiontypes/rqp/rqp.php new file mode 100644 index 00000000000..3b27e37bd16 --- /dev/null +++ b/mod/quiz/questiontypes/rqp/rqp.php @@ -0,0 +1,277 @@ +$source, 'format'=>$format, 'index'=>$index)); + if (is_soap_fault($itemInfo)) { + return $itemInfo; + } + // put the sourceErrors into an associative array indexed by the + // error identifier + $sourceErrors = array(); + if (!empty($itemInfo->sourceErrors)) { + foreach ($itemInfo->sourceErrors as $error) { + $id = $error->identifier; + unset($error->identifier); + $sourceErrors[$id] = (object) $error; + } + } + $itemInfo->sourceErrors = $sourceErrors; + return $itemInfo; +} + +/** +* Process an item template to produce template variables using the RQP_ProcessTemplate operation +* +* @param SoapClient $connection The URL of the RQP server that we want to connect to +* @param string $source Item source +* @param anyURI $format Item format +* @return object Object holding the return parameters or a SoapFault. +*/ +function rqp_process_template($connection, $source, $format='') { + $return = soap_call($connection, 'RQP_ProcessTemplate', + array('source'=>$source, 'format'=>$format)); + if (is_soap_fault($return)) { + return $return; + } + $templateVars = array(); + if (!empty($return->templateVars)) { + foreach ($return->templateVars as $var) { + $templateVars[$var->identifier] = $var->values; + } + } + $return->templateVars = $templateVars; + return $return; +} + +/** +* Clone an item template using the RQP_ProcessTemplate operation +* +* @param SoapClient $connection The URL of the RQP server that we want to connect to +* @param string $source Item source +* @param anyURI $format Item format +* @param array $templateVars Associative array of template variables +* @return object Object holding the return parameters or a SoapFault. +*/ +function rqp_clone($connection, $source, $format='', $templateVars=array()) { + // make an array of key-value pairs from the template variables array + array_walk($templateVars, create_function('&$val, $key', + '$val = (object) array(\'identifier\'=>$key, \'values\'=>$val);')); + + $return = soap_call($connection, 'RQP_Clone', array('source'=>$source, + 'format'=>$format, 'templateVars'=>array_values($templateVars))); + if (is_soap_fault($return)) { + return $return; + } + $templateVars = array(); + if (!empty($return->templateVars)) { + foreach ($return->templateVars as $var) { + $templateVars[$var->identifier] = $var->values; + } + } + $return->templateVars = $templateVars; + return $return; +} + +/** +* Get runtime information about the item in the given state using the +* RQP_SessionInformation operation +* +* @param SoapClient $connection The URL of the RQP server that we want to connect to +* @param string $source Item source +* @param anyURI $format Item format +* @param string $persistentData String giving the state of the item session +* @param array $templateVars Associative array of template variables +* @param string $embedPrefix +* @return object Object holding the return parameters or a SoapFault. +*/ +function rqp_session_info($connection, $source, $format='', $persistentData='', + $templateVars=array(), $embedPrefix='', $index=0) { + // make an array of key-value pairs from the template variables array + array_walk($templateVars, create_function('&$val, $key', + '$val = (object) array(\'identifier\'=>$key, \'values\'=>$val);')); + + $return = soap_call($connection, 'RQP_SessionInformation', + array('source'=>$source, 'format'=>$format, 'index'=>$index, + 'templateVars'=>array_values($templateVars), + 'persistentData'=>$persistentData, 'embedPrefix'=>$embedPrefix)); + if (is_soap_fault($return)) { + return $return; + } + $templateVars = array(); + if (!empty($return->templateVars)) { + foreach ($return->templateVars as $var) { + $templateVars[$var->identifier] = $var->values; + } + } + $return->templateVars = $templateVars; + $responses = array(); + if (!empty($return->correctResponses)) { + foreach ($return->correctResponses as $var) { + $responses[$var->name] = $var->value; + } + } + $return->correctResponses = $responses; + return $return; +} + +/** +* Process and render the item in the given state using the RQP_Render operation +* +* @param SoapClient $connection The URL of the RQP server that we want to connect to +* @param string $source Item source +* @param anyURI $format Item format +* @param string $persistentData String giving the state of the item session +* @param string $embedPrefix +* @param array $responses Student responses from the form elements +* @param boolean $advanceState Should the item be advanced to the next state? +* @param anyURI $renderFormat +* @param array $templateVars Associative array of template variables +* @param anyURI $mediaBase +* @param anyURI $appletBase +* @param anyURI $modalFormat +* @return object Object holding the return parameters or a SoapFault. +*/ +function rqp_render($connection, $source, $format='', $persistentData='', + $embedPrefix='', $responses=array(), $advanceState=true, $renderFormat='', + $templateVars=array(), $mediaBase='', $appletBase='', + $modalFormat='', $index=0) { + // make an array of key-value pairs from the template vars array + array_walk($templateVars, create_function('&$val, $key', + '$val = (object) array(\'identifier\'=>$key, \'values\'=>$val);')); + + // make an array of name-value pairs from the responses array + array_walk($responses, create_function('&$val, $key', + '$val = (object) array(\'name\'=>$key, \'value\'=>$val);')); + + $return = soap_call($connection, 'RQP_Render', array('source'=>$source, + 'format'=>$format, 'index'=>$index, 'templateVars'=>array_values($templateVars), + 'persistentData'=>$persistentData, 'responses'=>$responses, + 'advanceState'=>$advanceState, 'embedPrefix'=>$embedPrefix, + 'appletBase'=>$appletBase, 'mediaBase'=>$mediaBase, + 'renderFormat'=>$renderFormat, 'modalFormat'=>$modalFormat)); + if (is_soap_fault($return)) { + return $return; + } + $outcomeVars = array(); + if (!empty($return->outcomeVars)) { + foreach ($return->outcomeVars as $var) { + $outcomeVars[$var->identifier] = $var->values; + } + } + $return->outcomeVars = $outcomeVars; + + $templateVars = array(); + if (!empty($return->templateVars)) { + foreach ($return->templateVars as $var) { + $templateVars[$var->identifier] = $var->values; + } + } + $return->templateVars = $templateVars; + + $output = array(); + if (!empty($return->output)) { + foreach ($return->output as $out) { + $id = $out->identifier; + unset($out->identifier); + $output[$id] = $out; + } + } + $return->output = $output; + + return $return; +} + +/** +* Call to the RQP_Author operation +* +* @param SoapClient $connection The URL of the RQP server +* @param string $source Item source +* @param anyURI $format Item format +* @param string $persistentData String giving the state of the authoring session +* @param string $embedPrefix +* @param array $responses Teacher responses from the form elements +* @param anyURI $renderFormat +* @return object Object holding the return parameters or a SoapFault. +*/ +function rqp_author($connection, $source, $format='', $persistentData='', + $embedPrefix='', $responses=array(), $renderFormat='') { + + // make an array of name-value pairs from the responses array + array_walk($responses, create_function('&$val, $key', + '$val = (object) array(\'name\'=>$key, \'value\'=>$val);')); + + return soap_call($connection, 'RQP_Author', array('source'=>$source, + 'format'=>$format, 'persistentData'=>$persistentData, 'responses'=>$responses, + 'embedPrefix'=>$embedPrefix, 'renderFormat'=>$renderFormat)); +} + +?> diff --git a/mod/quiz/questiontypes/rqp/types.php b/mod/quiz/questiontypes/rqp/types.php new file mode 100644 index 00000000000..689507b3d6a --- /dev/null +++ b/mod/quiz/questiontypes/rqp/types.php @@ -0,0 +1,73 @@ +wwwroot . '/login/index.php'); + } + + if (!$site = get_site()) { + error('Site isn\'t defined!'); + } + + $sesskey = required_param('sesskey', PARAM_RAW); + if (!confirm_sesskey($sesskey)) { + error(get_string('confirmsesskeybad', 'error')); + } + + // Print the header + $stradmin = get_string('admin'); + $strconfiguration = get_string('configuration'); + $strmanagemodules = get_string('managemodules'); + $strmodulename = get_string('modulename', 'quiz'); + $stritemtypes = get_string('itemtypes', 'quiz'); + $navigation = '' . $stradmin . ' -> ' . + '' . $strconfiguration . ' -> ' . + '' . $strmanagemodules . ' -> ' . + '' . $strmodulename . ' -> ' . + $stritemtypes; + print_header($site->shortname . ': ' . $strmodulename . ': ' . $stritemtypes, $site->fullname, $navigation, '', '', true, '', ''); + + // Get list of types + $types = quiz_rqp_get_types(); + + // Setup the table + $strname = get_string('name'); + $strservers = get_string('servers', 'quiz'); + $strcapabilities = get_string('capabilities', 'quiz'); + $straction = get_string('action'); + $stredit = get_string('edit'); + $strdelete = get_string('delete'); + $table->head = array($strname, $strservers, $strcapabilities, $straction); + $table->align = array('left', 'left', 'left', 'left'); + $table->size = array('*', '*', '*', '*'); + + if ($types) { + foreach ($types as $type) { + $link = '' . s($type->name) . ''; + $servers = get_string('rendering', 'quiz') . ': ' . s($type->rendering_server); + if ($type->cloning_server) { + $servers .= "
\n" . get_string('cloning', 'quiz') . ': ' . s($type->cloning_server); + } + $capabilities = ''; + $actions = '' . $stredit . ' ' . $strdelete . ''; + $table->data[] = array($link, $servers, $capabilities, $actions); + } + } + + // Print the table + print_heading($stritemtypes); + print_table($table); + + // Print add link + echo '

' . get_string('additemtype', 'quiz') . "

\n"; + + // Finish the page + print_footer(); + +?> diff --git a/mod/quiz/questiontypes/rqp/uni_soap.php b/mod/quiz/questiontypes/rqp/uni_soap.php new file mode 100644 index 00000000000..b1e662c5312 --- /dev/null +++ b/mod/quiz/questiontypes/rqp/uni_soap.php @@ -0,0 +1,113 @@ +call($call, $params); + if ($connection->fault) { + return @make_soap_fault($result['faultcode'], $result['faultstring'], '', $result['detail']); + } + if ($connection->error_str) { + return @make_soap_fault('server', $connection->error_str, '', $connection->response); + } + /* Fix objects being returned as associative arrays (to fit with PHP5 + SOAP support */ + return fix_object($result); + } + + function soap_serve($wsdl, $functions) { + global $HTTP_RAW_POST_DATA; + + $s = new soap_server($wsdl); + $s->service(isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : ''); + } + + function get_last_soap_messages($connection) { + return array('request'=>$connection->request, 'response'=>$connection->response); + } + + /* Fix objects being returned as associative arrays (to fit with PHP5 + SOAP support */ + function fix_object($value) { + if (is_array($value)) { + $value = array_map('fix_object', $value); + $keys = array_keys($value); + /* check for arrays of length 1 (they get given the key "item" + rather than 0 by nusoap) */ + if (1 === count($value) && 'item' === $keys[0]) { + $value = array_values($value); + } + else { + /* cast to object if it is an associative array with at least + one string key */ + foreach ($keys as $key) { + if (is_string($key)) { + $value = (object) $value; + break; + } + } + } + } + return $value; + } + + // Fix simple type encoding - not needed for nuSOAP + function soap_encode($value, $name, $type, $namespace, $encode=0) { + return $value; + } + + // Fix complex type encoding - not needed for nuSOAP + function soap_encode_object($value, $name, $type, $namespace) { + return $value; + } + + // Fix array encoding - not needed for nuSOAP + function soap_encode_array($value, $name, $type, $namespace) { + return $value; + } +} + +// In both cases... +function handle_soap_wsdl_request($wsdlfile, $address=false) { + header('Content-type: application/wsdl+xml'); + $wsdl = file_get_contents($wsdlfile); + if (false !== $address) { + if (true === $address) { + $address = (($_SERVER['SERVER_PORT'] == 443) ? 'https://' : 'http://') . $_SERVER['SERVER_NAME'] . $_SERVER['SCRIPT_NAME']; + } + $wsdl = str_replace('###SERVER_ADDRESS###', $address, $wsdl); + } + echo $wsdl; + exit; +} + +?> diff --git a/mod/quiz/questiontypes/rqp/uni_soap.php5 b/mod/quiz/questiontypes/rqp/uni_soap.php5 new file mode 100644 index 00000000000..f207ee90ad6 --- /dev/null +++ b/mod/quiz/questiontypes/rqp/uni_soap.php5 @@ -0,0 +1,118 @@ +SOAP_1_1, 'exceptions'=>true, 'trace'=>$trace)); + } + catch (SoapFault $f) { + $connection = $f; + } + catch (Exception $e) { + $connection = new SoapFault('client', 'Could not connect to the service'); + } + return $connection; +} + +/** +* Make a call to a SoapClient +* +* @param SoapClient $connection The SoapClient to call +* @param string $call Operation to be performed by client +* @param array $params Parameters for the call +* @return mixed The return parameters of the operation or a SoapFault +* If the operation returned several parameters then these +* are returned as an object rather than an array +*/ +function soap_call($connection, $call, $params) { + try { + $return = $connection->__soapCall($call, $params); + } + catch (SoapFault $f) { + $return = $f; + } + catch (Exception $e) { + $return = new SoapFault('client', 'Could call the method'); + } + // return multiple parameters using an object rather than an array + if (is_array($return)) { + $keys = array_keys($return); + $assoc = true; + foreach ($keys as $key) { + if (!is_string($key)) { + $assoc = false; + break; + } + } + if ($assoc) + $return = (object) $return; + } + return $return; +} + +function soap_serve($wsdl, $functions) { + // create server object + $s = new SoapServer($wsdl); + // export functions + foreach ($functions as $func) + $s->addFunction($func); + // handle the request + $s->handle(); +} + +function make_soap_fault($faultcode, $faultstring, $faultactor='', $detail='', $faultname='', $headerfault='') { + return new SoapFault($faultcode, $faultstring, $faultactor, $detail, $faultname, $headerfault); +} + +function get_last_soap_messages($connection) { + return array('request'=>$connection->__getLastRequest(), 'response'=>$connection->__getLastResponse()); +} + +// Fix simple type encoding - work around a bug in PHP +function soap_encode($value, $name, $type, $namespace, $encode=XSD_STRING) { + $value = new SoapVar($value, $encode, $type, $namespace); + if ('' === $name) + return $value; + return new SoapParam($value, $name); +} + +// Fix complex type encoding - work around a bug in PHP +function soap_encode_object($value, $name, $type, $namespace) { + if (!is_object($value)) + return $value; + $value = new SoapVar($value, SOAP_ENC_OBJECT, $type, $namespace); + if ('' === $name) + return $value; + return new SoapParam($value, $name); +} + +// Fix array encoding - work around a bug in PHP +function soap_encode_array($value, $name, $type, $namespace) { + if (!is_array($value)) + return $value; + $value = new SoapVar($value, SOAP_ENC_ARRAY, 'ArrayOf' . $type, $namespace); + if ('' === $name) + return $value; + return new SoapParam($value, $name); +} + +?> diff --git a/mod/quiz/questiontypes/shortanswer/questiontype.php b/mod/quiz/questiontypes/shortanswer/questiontype.php index 663d0de147d..fd40b7ff01b 100644 --- a/mod/quiz/questiontypes/shortanswer/questiontype.php +++ b/mod/quiz/questiontypes/shortanswer/questiontype.php @@ -1,204 +1,229 @@ -prefix}quiz_shortanswer sa, - {$CFG->prefix}quiz_answers a - WHERE sa.question = '$question->id' - AND sa.question = a.question " - . $addedcondition); - - } - - function name() { - return 'shortanswer'; - } - - function save_question_options($question) { - if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { - $oldanswers = array(); - } - - $answers = array(); - $maxfraction = -1; - - // Insert all the new answers - foreach ($question->answer as $key => $dataanswer) { - if ($dataanswer != "") { - if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it - $answer = $oldanswer; - $answer->answer = trim($dataanswer); - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!update_record("quiz_answers", $answer)) { - $result->error = "Could not update quiz answer! (id=$answer->id)"; - return $result; - } - } else { // This is a completely new answer - unset($answer); - $answer->answer = trim($dataanswer); - $answer->question = $question->id; - $answer->fraction = $question->fraction[$key]; - $answer->feedback = $question->feedback[$key]; - if (!$answer->id = insert_record("quiz_answers", $answer)) { - $result->error = "Could not insert quiz answer!"; - return $result; - } - } - $answers[] = $answer->id; - if ($question->fraction[$key] > $maxfraction) { - $maxfraction = $question->fraction[$key]; - } - } - } - - if ($options = get_record("quiz_shortanswer", "question", $question->id)) { - $options->answers = implode(",",$answers); - $options->usecase = $question->usecase; - if (!update_record("quiz_shortanswer", $options)) { - $result->error = "Could not update quiz shortanswer options! (id=$options->id)"; - return $result; - } - } else { - unset($options); - $options->question = $question->id; - $options->answers = implode(",",$answers); - $options->usecase = $question->usecase; - if (!insert_record("quiz_shortanswer", $options)) { - $result->error = "Could not insert quiz shortanswer options!"; - return $result; - } - } - - /// Perform sanity checks on fractional grades - if ($maxfraction != 1) { - $maxfraction = $maxfraction * 100; - $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction); - return $result; - } else { - return true; - } - } - - function print_question_formulation_and_controls($question, - $quiz, $readonly, $answers, $correctanswers, $nameprefix) { - /// This implementation is also used by question type NUMERICAL - - /// Print question text and media - - echo format_text($question->questiontext, - $question->questiontextformat, - NULL, $quiz->course); - quiz_print_possible_question_image($quiz->id, $question); - - /// Print input controls - - $stranswer = get_string("answer", "quiz"); - if (isset($question->response[$nameprefix])) { - $value = ' value="'.htmlSpecialChars($question->response[$nameprefix]).'" '; - } else { - $value = ' value="" '; - } - $inputname = ' name="'.$nameprefix.'" '; - echo "

$stranswer:

"; - - if ($quiz->feedback && isset($answers[$nameprefix]) - && $feedback = $answers[$nameprefix]->feedback) { - quiz_print_comment($feedback); - } - if ($readonly && $quiz->correctanswers) { - $delimiter = ''; - $correct = ''; - foreach ($correctanswers as $correctanswer) { - $correct .= $delimiter.$correctanswer->answer; - $delimiter = ', '; - } - quiz_print_correctanswer($correct); - } - } - - function grade_response($question, $nameprefix, $addedanswercondition='') { - - if (isset($question->response[$nameprefix])) { - $response0 = trim(stripslashes($question->response[$nameprefix])); - } else { - $response0 = ''; - } - $answers = $this->get_answers($question, $addedanswercondition); - - /// Determine ->answers[] - $result->answers = array(); - if ('' !== $response0) { - - /// These are things to protect in the strings when wildcards are used - $search = array('\\', '+', '(', ')', '[', ']', '-'); - $replace = array('\\\\', '\+', '\(', '\)', '\[', '\]', '\-'); - - foreach ($answers as $answer) { - - $answer->answer = trim($answer->answer); // Just in case - - if (empty($result->answers) || $answer->fraction - > $result->answers[$nameprefix]->fraction) { - - if (!$answer->usecase) { // Don't compare case - $response0 = strtolower($response0); - $answer0 = strtolower($answer->answer); - } else { - $answer0 = $answer->answer; - } - - if (strpos(' '.$answer0, '*')) { - $answer0 = str_replace('\*','@@@@@@',$answer0); - $answer0 = str_replace('*','.*',$answer0); - $answer0 = str_replace($search, $replace, $answer0); - $answer0 = str_replace('@@@@@@', '\*',$answer0); - - if (ereg('^'.$answer0.'$', $response0)) { - $result->answers[$nameprefix] = $answer; - } - - } else if ($answer0 == $response0) { - $result->answers[$nameprefix] = $answer; - } - } - } - } - - - $result->grade = isset($result->answers[$nameprefix]) - ? $result->answers[$nameprefix]->fraction - : 0.0; - $result->correctanswers = quiz_extract_correctanswers($answers, - $nameprefix); - return $result; - } -} -//// END OF CLASS //// - -////////////////////////////////////////////////////////////////////////// -//// INITIATION - Without this line the question type is not in use... /// -////////////////////////////////////////////////////////////////////////// -$QUIZ_QTYPES[SHORTANSWER]= new quiz_shortanswer_qtype(); - -?> +options = get_record('quiz_shortanswer', 'question', $question->id)) { + notify('Error: Missing question options!'); + return false; + } + + if (!$question->options->answers = get_records('quiz_answers', 'question', + $question->id)) { + notify('Error: Missing question answers!'); + return false; + } + return true; + } + + function save_question_options($question) { + if (!$oldanswers = get_records("quiz_answers", "question", $question->id, "id ASC")) { + $oldanswers = array(); + } + + $answers = array(); + $maxfraction = -1; + + // Insert all the new answers + foreach ($question->answer as $key => $dataanswer) { + if ($dataanswer != "") { + if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it + $answer = $oldanswer; + $answer->answer = trim($dataanswer); + $answer->fraction = $question->fraction[$key]; + $answer->feedback = $question->feedback[$key]; + if (!update_record("quiz_answers", $answer)) { + $result->error = "Could not update quiz answer! (id=$answer->id)"; + return $result; + } + } else { // This is a completely new answer + unset($answer); + $answer->answer = trim($dataanswer); + $answer->question = $question->id; + $answer->fraction = $question->fraction[$key]; + $answer->feedback = $question->feedback[$key]; + if (!$answer->id = insert_record("quiz_answers", $answer)) { + $result->error = "Could not insert quiz answer!"; + return $result; + } + } + $answers[] = $answer->id; + if ($question->fraction[$key] > $maxfraction) { + $maxfraction = $question->fraction[$key]; + } + } + } + + if ($options = get_record("quiz_shortanswer", "question", $question->id)) { + $options->answers = implode(",",$answers); + $options->usecase = $question->usecase; + if (!update_record("quiz_shortanswer", $options)) { + $result->error = "Could not update quiz shortanswer options! (id=$options->id)"; + return $result; + } + } else { + unset($options); + $options->question = $question->id; + $options->answers = implode(",",$answers); + $options->usecase = $question->usecase; + if (!insert_record("quiz_shortanswer", $options)) { + $result->error = "Could not insert quiz shortanswer options!"; + return $result; + } + } + + /// Perform sanity checks on fractional grades + if ($maxfraction != 1) { + $maxfraction = $maxfraction * 100; + $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction); + return $result; + } else { + return true; + } + } + + function print_question_formulation_and_controls(&$question, &$state, $quiz, $options) { + /// This implementation is also used by question type NUMERICAL + + $answers = &$question->options->answers; + $correctanswers = $this->get_correct_responses($question, $state); + $readonly = empty($options->readonly) ? '' : 'disabled="disabled"'; + $nameprefix = $question->name_prefix; + + /// Print question text and media + + echo format_text($question->questiontext, + $question->questiontextformat, + NULL, $quiz->course); + quiz_print_possible_question_image($quiz->id, $question); + + /// Print input controls + + $stranswer = get_string("answer", "quiz"); + if (isset($state->responses[''])) { + $value = ' value="'.htmlSpecialChars($state->responses['']).'" '; + } else { + $value = ' value="" '; + } + $inputname = ' name="'.$nameprefix.'" '; + echo "

$stranswer:

"; + + if ($options->feedback) { + $testedstate = clone($state); + $teststate = clone($state); + foreach($answers as $answer) { + $teststate->responses[''] = trim($answer->answer); + if($this->compare_responses($question, $testedstate, $teststate)) { + quiz_print_comment($answer->feedback); + break; + } + } + } + + if ($options->readonly && $options->correct_responses) { + $delimiter = ''; + $correct = ''; + if ($correctanswers) { + foreach ($correctanswers as $correctanswer) { + $correct .= $delimiter.$correctanswer; + $delimiter = ', '; + } + } + quiz_print_correctanswer($correct); + } + } + + function grade_responses(&$question, &$state, $quiz) { + $answers = &$question->options->answers; + $testedstate = clone($state); + $teststate = clone($state); + + foreach($answers as $answer) { + $teststate->responses[''] = trim($answer->answer); + if($this->compare_responses($question, $testedstate, $teststate)) { + if (empty($state->raw_grade) && $state->raw_grade < $answer->fraction) { + $state->raw_grade = $answer->fraction; + } + } + } + if (empty($state->raw_grade)) { + $state->raw_grade = 0; + } + + // Make sure we don't assign negative or too high marks + $state->raw_grade = min(max((float) $state->raw_grade, + 0.0), 1.0) * $question->maxgrade; + $state->penalty = $question->penalty; + // Only allow one attempt at the question + // $state->event = QUIZ_EVENTCLOSE; + return true; + } + + function compare_responses(&$question, &$state, &$teststate) { + if (isset($state->responses[''])) { + $response0 = trim(stripslashes($state->responses[''])); + } else { + $response0 = ''; + } + + if (isset($teststate->responses[''])) { + $response1 = trim(stripslashes($teststate->responses[''])); + } else { + $response1 = ''; + } + + if (!$question->options->usecase) { // Don't compare case + $response0 = strtolower($response0); + $response1 = strtolower($response1); + } + + /// These are things to protect in the strings when wildcards are used + $search = array('\\', '+', '(', ')', '[', ']', '-'); + $replace = array('\\\\', '\+', '\(', '\)', '\[', '\]', '\-'); + + if (strpos(' '.$response1, '*')) { + $answer0 = str_replace('\*','@@@@@@',$response1); + $answer0 = str_replace('*','.*',$response1); + $answer0 = str_replace($search, $replace, $response1); + $answer0 = str_replace('@@@@@@', '\*',$response1); + + if (ereg('^'.$response1.'$', $response0)) { + return true; + } + + } else if ($response1 == $response0) { + return true; + } + + return false; + } + + function print_question_grading_details(&$question, &$state, $quiz, $options) { + // do nothing + } +} +//// END OF CLASS //// + +////////////////////////////////////////////////////////////////////////// +//// INITIATION - Without this line the question type is not in use... /// +////////////////////////////////////////////////////////////////////////// +$QUIZ_QTYPES[SHORTANSWER]= new quiz_shortanswer_qtype(); + +?> diff --git a/mod/quiz/questiontypes/shortanswer/shortanswer.html b/mod/quiz/questiontypes/shortanswer/shortanswer.html index b789ea3230c..f00136c4395 100644 --- a/mod/quiz/questiontypes/shortanswer/shortanswer.html +++ b/mod/quiz/questiontypes/shortanswer/shortanswer.html @@ -31,7 +31,7 @@ "; } print_textarea($usehtmleditor, 15, 60, 630, 300, "questiontext", $question->questiontext); @@ -65,7 +65,7 @@ : - + + : + + " /> + + + + : - @@ -115,7 +123,7 @@ -qtype]->print_replacement_options($question, $course, $contextquiz); $QUIZ_QTYPES[$question->qtype]->print_question_form_end($question); ?> diff --git a/mod/quiz/questiontypes/truefalse/questiontype.php b/mod/quiz/questiontypes/truefalse/questiontype.php index df7692aba8b..ec648f32cc0 100644 --- a/mod/quiz/questiontypes/truefalse/questiontype.php +++ b/mod/quiz/questiontypes/truefalse/questiontype.php @@ -78,92 +78,156 @@ class quiz_truefalse_qtype extends quiz_default_questiontype { return true; } - function print_question_formulation_and_controls($question, - $quiz, $readonly, $answers, $correctanswers, $nameprefix) { - + /** + * Loads the question type specific options for the question. + */ + function get_question_options(&$question) { // Get additional information from database + // and attach it to the question object + if (!$question->options = get_record('quiz_truefalse', 'question', + $question->id)) { + notify('Error: Missing question options!'); + return false; + } + // Load possible answers + if (!$answers = get_records('quiz_answers', 'question', + $question->id)) { + notify('Error: Missing question answers!'); + return false; + } + $question->options->answers = array( + 'true' => $answers[$question->options->trueanswer], + 'false' => $answers[$question->options->falseanswer]); - 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!"); - } + return true; + } + + + /** + * Prints the main content of the question including any interactions + */ + function print_question_formulation_and_controls(&$question, &$state, + $quiz, $options) { + + $answers = &$question->options->answers; + $correctanswers = $this->get_correct_responses($question, $state); + $readonly = $options->readonly ? ' disabled="disabled"' : ''; // Print question formulation - echo format_text($question->questiontext, $question->questiontextformat, NULL, $quiz->course); quiz_print_possible_question_image($quiz->id, $question); - // Print input controls + // Update the answer strings + $stranswer = get_string('answer', 'quiz'); + $strlastanswer = get_string('lastanswer', 'quiz'); - $stranswer = get_string("answer", "quiz"); - - if (!$true->answer) { - $true->answer = get_string("true", "quiz"); + if (!$answers['true']->answer) { + $answers['true']->answer = get_string('true', 'quiz'); } - if (!$false->answer) { - $false->answer = get_string("false", "quiz"); + if (!$answers['false']->answer) { + $answers['false']->answer = get_string('false', 'quiz'); } - $truechecked = ""; - $falsechecked = ""; - - if (!isset($question->response[$nameprefix])) { - $question->response[$nameprefix] = ''; + // Work out the selected answer and last marked answer + $selected = ''; + $marked = ''; + $teststate = clone($state); + $teststate->responses[''] = $answers['true']->answer; + if ($this->compare_responses($question, $state, $teststate)) { + $selected = 'true'; } - if ($true->id == $question->response[$nameprefix]) { - $truechecked = 'checked="checked"'; - } else if ($false->id == $question->response[$nameprefix]) { - $falsechecked = 'checked="checked"'; + if ($this->compare_responses($question, $state->last_graded, + $teststate)) { + $marked = 'true'; + } + $teststate->responses[''] = $answers['false']->answer; + if ($this->compare_responses($question, $state, $teststate)) { + $selected = 'false'; + } + if ($this->compare_responses($question, $state->last_graded, + $teststate)) { + $marked = 'false'; } - $truecorrect = ""; - $falsecorrect = ""; - if ($readonly && $quiz->correctanswers) { - if (!empty($correctanswers[$nameprefix.$true->id])) { - $truecorrect = 'class="highlight"'; - } - if (!empty($correctanswers[$nameprefix.$false->id])) { - $falsecorrect = 'class="highlight"'; - } + /* Work out the correct answer if feedback or correct responses are + requested */ + if ($options->feedback || $options->correct_responses) { + $correctstate = clone($state); + $correctstate->responses[''] = $correctanswers['']; + $correct = ''; + if (!is_null($correctstate->responses)) { + $teststate->responses[''] = $answers['true']->answer; + if ($this->compare_responses($question, $correctstate, + $teststate)) { + $correct = 'true'; + } + $teststate->responses[''] = $answers['false']->answer; + if ($this->compare_responses($question, $correctstate, + $teststate)) { + $correct = 'false'; + } + } } - $inputname = ' name="'.$nameprefix.'" '; - echo ""; - echo "
$stranswer:  "; - echo "id\" alt=\"".s($true->answer)."\" />$true->answer"; - echo ""; - echo "id\" alt=\"".s($false->answer)."\" />$false->answer"; - echo "

";// changed from clear=ALL jm - if ($quiz->feedback && isset($answers[$nameprefix]) - && $feedback = $answers[$nameprefix]->feedback) { - quiz_print_comment( - "$feedback"); + + // Work out which radio button to select (if either) + $truechecked = ('true' === $selected) ? ' checked="checked"' : ''; + $falsechecked = ('false' === $selected) ? ' checked="checked"' : ''; + + // Work out which answer is correct if we need to highlight it + if ($options->correct_responses) { + $truecorrect = ('true' === $correct) ? ' class="highlight"' : ''; + $falsecorrect = ('false' === $correct) ? ' class="highlight"' : ''; + } else { + $truecorrect = ''; + $falsecorrect = ''; + } + + // Print the controls + $inputname = ' name="'.$question->name_prefix.'" '; + echo ''; + echo ''; + echo '' . s($answers['true']->answer); + echo ''; + echo ''; + p($answers['false']->answer); + if (!empty($marked) && (!$options->readonly || $marked !== $selected)) { + /* This should never happen but it is here both for robustness and + to serve as an example for question type authors */ + echo ''; + echo ''; + if ('true' === $marked) { + echo ''; + } else { + echo ''; + } + } + echo '
'; + echo $stranswer . ':  
'; + echo $strlastanswer . ':  ' . $answers['true']->answer; + echo ''; + echo $answers['false']->answer . '

'; + + if ($options->feedback && !empty($marked)) { + quiz_print_comment($answers[$marked]->feedback); } } - function grade_response($question, $nameprefix) { - - $answers = get_records("quiz_answers", "question", $question->id); - if (isset($question->response[$nameprefix]) - && isset($answers[$question->response[$nameprefix]])) { - $result->answers = array($nameprefix - => $answers[$question->response[$nameprefix]]); - $result->grade = $result->answers[$nameprefix]->fraction; - - } else { - $result->answers = array(); - $result->grade = 0.0; - } - $result->correctanswers = quiz_extract_correctanswers($answers, - $nameprefix); - - return $result; + /** + * This questiontype does not have to print any additional grading details + * + * @param object $question + * @param object $state + * @param object $quiz + * @param object $options + */ + function print_question_grading_details(&$question, &$state, $quiz, $options) { + // do nothing } } //// END OF CLASS //// diff --git a/mod/quiz/report.php b/mod/quiz/report.php index 5abf39408ed..4151f0488a2 100644 --- a/mod/quiz/report.php +++ b/mod/quiz/report.php @@ -8,7 +8,7 @@ optional_variable($id); // Course Module ID, or optional_variable($q); // quiz ID - optional_variable($mode, "overview"); // Report mode + optional_variable($mode, "simplestat"); // Report mode if ($id) { if (! $cm = get_record("course_modules", "id", $id)) { @@ -41,50 +41,29 @@ error("You are not allowed to use this script"); } - add_to_log($course->id, "quiz", "report", "report.php?id=$cm->id", "$quiz->id", "$cm->id"); - -/// Print the page header - if (empty($noheader)) { - - $strquizzes = get_string("modulenameplural", "quiz"); - $strquiz = get_string("modulename", "quiz"); - $strreport = get_string("report", "quiz"); - - print_header_simple(format_string($quiz->name), "", - "id\">$strquizzes - -> id\">".format_string($quiz->name,true)." -> $strreport", - "", "", true, update_module_button($cm->id, $course->id, $strquiz), navmenu($course, $cm)); - - print_heading(format_string($quiz->name)); - - - /// Print list of available quiz reports - - $allreports = get_list_of_plugins("mod/quiz/report"); - $reportlist = array ("overview", "regrade"); // Standard reports we want to show first - - foreach ($allreports as $report) { - if (!in_array($report, $reportlist)) { - $reportlist[] = $report; - } - } - - $tabs = array(); - $row = array(); - $currenttab = ''; - foreach ($reportlist as $report) { - $row[] = new tabobject($report, "report.php?id=$cm->id&mode=$report", - get_string("report$report", "quiz")); - if ($report == $mode) { - $currenttab = $report; - } - } - $tabs[] = $row; - - print_tabs($tabs, $currenttab); - + // if no questions have been set up yet redirect to edit.php + if (!$quiz->questions and isteacheredit($course->id)) { + redirect('edit.php?quizid='.$quiz->id); } + add_to_log($course->id, "quiz", "report", "report.php?id=$cm->id", "$quiz->id", "$cm->id"); + +/// Define some strings + + $strquizzes = get_string("modulenameplural", "quiz"); + $strquiz = get_string("modulename", "quiz"); + +/// Print the page header + + print_header_simple(format_string($quiz->name), "", + "id\">$strquizzes + -> ".format_string($quiz->name), + "", "", true, update_module_button($cm->id, $course->id, $strquiz), navmenu($course, $cm)); + +/// Print the tabs + + $currenttab = 'reports'; + include('tabs.php'); /// Open the selected quiz report and display it @@ -103,9 +82,8 @@ error("Error occurred during pre-processing!"); } - if (empty($noheader)) { - print_footer($course); - } +/// Print footer + print_footer($course); ?> diff --git a/mod/quiz/report/fullstat/report.php b/mod/quiz/report/fullstat/report.php index 8768ff45fe7..2d5ad801585 100755 --- a/mod/quiz/report/fullstat/report.php +++ b/mod/quiz/report/fullstat/report.php @@ -8,6 +8,9 @@ class quiz_report extends quiz_default_report { function display($quiz, $cm, $course) { /// This function just displays the report + + print_heading('Not yet implemented'); + return true; global $CFG; global $download, $quests,$qtally,$table_colcount,$max_choices, $analysis,$qs_in_order,$total_user_count,$match_number, $thismin,$thismax,$myxls,$match_qs,$formatbc,$workbook,$strquestion,$showtext,$debug; @@ -29,7 +32,7 @@ class quiz_report extends quiz_default_report { $strdiscrimination = get_string('discrimination', 'quiz'); //Get the question ids - //$showtext causes M/C text to whos in top table. + //$showtext causes M/C text to whos in top table. //This could be made into a user toggle if we want to complicate matters $showtext = 1; $containsMCTF = 0; //used to toggle title in final listing @@ -86,7 +89,7 @@ class quiz_report extends quiz_default_report { } } } - } + } } } @@ -102,7 +105,7 @@ class quiz_report extends quiz_default_report { $userdata[$thisresp->userid][$thisresp->attemptno]['name']=fullname($thisresp); $userdata[$thisresp->userid][$thisresp->attemptno]['attemptid']=$thisresp->aid; } - + if($debug and !$download){ print("

User Data

"); print_object($userdata); @@ -114,7 +117,7 @@ class quiz_report extends quiz_default_report { // //print_object($thisattempt); $reportline++; $data_tally[$reportline][$thisattempt['attemptid']][] = $thisattempt['name']; - $data_tally[$reportline][$thisattempt['attemptid']][] =round(($thisattempt['grade']/$quiz->sumgrades)*100,0); + $data_tally[$reportline][$thisattempt['attemptid']][] =round(($thisattempt['grade']/$quiz->sumgrades)*100,0); //now for each question, record response as it should be printed and whether right, wrong or skipped //SHORTASNSWER the answer as in $userdata; TF or MULTI need response looked by from cid from $quests //MATCH needs elaborate processing @@ -173,7 +176,7 @@ class quiz_report extends quiz_default_report { print("

Data Tally

"); print_object($data_tally); } - + //Create a list of all attempts with their scores for item analysis //Also create $data2 that has attempt id as key foreach ($data_tally as $thistally){ @@ -184,7 +187,7 @@ class quiz_report extends quiz_default_report { } } } - + //now go through scores from top to bottom and from $data2 accumulate number correct for top 1/3 and bottom 1/3 of scorers if ($scores) { arsort($scores); @@ -223,7 +226,7 @@ class quiz_report extends quiz_default_report { } } } - + if($debug and !$download){ print("

Scores

"); print_object($scores); @@ -232,7 +235,7 @@ class quiz_report extends quiz_default_report { print("

Bottom Scores

"); print_object($bott_scores); } - + //Create here an array with the response analysis data for use with both screen display & Excel // 2 dimensional array has as many cells across as items + title, as many down as $max_choices // plus one row [0] for correct items @@ -297,7 +300,7 @@ class quiz_report extends quiz_default_report { $current_column += $match_number[$qid] -1; break; default: - + break; } } @@ -325,7 +328,7 @@ class quiz_report extends quiz_default_report { $format =& $workbook->add_format(); $format->set_bold(0); $formaty =& $workbook->add_format(); - $formaty->set_bg_color('yellow'); + $formaty->set_bg_color('yellow'); $formatyc =& $workbook->add_format(); $formatyc->set_bg_color('yellow'); //bold text on yellow bg $formatyc->set_bold(1); @@ -392,7 +395,7 @@ class quiz_report extends quiz_default_report { } //now print the lines of answers - + $row = 2; foreach ($data_tally as $thisuserno=>$thisuser){ foreach($thisuser as $thisattemptno=>$thisattempt){ @@ -401,7 +404,7 @@ class quiz_report extends quiz_default_report { //$thisitemkeys 1 & 2 are name and total score //There needs to be a 3-way branch, keys0 & 1 just print $thisitem //else if $thisitem['qtype'] = 5, then processing for MATCH is needed - //else the data to be printed is in $thisitem['data'] and + //else the data to be printed is in $thisitem['data'] and //$thisitem['score'] == 1 shows that the item was correct if ($thisitem['score'] < 1) {$thiscolor = $formatred;} else {$thiscolor = $formatblue;} $col++; @@ -571,7 +574,7 @@ class quiz_report extends quiz_default_report { if ($download == "txt") { /// Print header to force download - header("Content-Type: application/download\n"); + header("Content-Type: application/download\n"); $downloadfilename = clean_filename("$course->shortname ".format_string($quiz->name,true)); header("Content-Disposition: attachment; filename=\"$downloadfilename.txt\""); header("Expires: 0"); @@ -582,7 +585,7 @@ class quiz_report extends quiz_default_report { echo format_string($quiz->name,true); echo "\n"; - + /// Print all the user data $colcount = count($question_ids); foreach ($data_tally as $thisuserno=>$thisuser){ @@ -697,9 +700,9 @@ class quiz_report extends quiz_default_report { print("\n"); } } - + print("
\n"); - + if($debug and !$download){ print("

Qtally

"); print_object($qtally); @@ -730,7 +733,7 @@ class quiz_report extends quiz_default_report { $nowdata = $analysis[$i][$j]; if (strpos($nowdata,"//")>0) {$nowdata = str_replace("//","
",$nowdata);} print(" $nowdata"); - + } } print("\n"); @@ -797,7 +800,7 @@ class quiz_report extends quiz_default_report { } } print("\n"); - + echo "
\n"; echo "\n"; echo "
"; @@ -821,7 +824,7 @@ function qr_quiz_responses($quiz) { // Given any quiz number, get all responses and place in // $response object global $CFG; - + $resp_recs =get_records_sql("SELECT r.id as rid, r.attempt, r.answer, r.question, a.attempt as attemptno, a.id as aid, a.quiz, a.userid, a.sumgrades, u.id as uid, u.lastname, u.firstname FROM {$CFG->prefix}quiz_responses r, {$CFG->prefix}quiz_attempts a, {$CFG->prefix}user u WHERE a.id = r.attempt AND a.quiz = '$quiz' AND a.userid = u.id ORDER BY u.lastname ASC, u.firstname ASC, r.id ASC"); return $resp_recs; } diff --git a/mod/quiz/report/overview/report.php b/mod/quiz/report/overview/report.php deleted file mode 100644 index 3fc6085764c..00000000000 --- a/mod/quiz/report/overview/report.php +++ /dev/null @@ -1,284 +0,0 @@ -libdir.'/tablelib.php'); - -/// Overview report just displays a big table of all the attempts - -class quiz_report extends quiz_default_report { - - function display($quiz, $cm, $course) { /// This function just displays the report - - global $CFG, $SESSION, $db; - - $strreallydel = addslashes(get_string('deleteattemptcheck','quiz')); - $strnoattempts = get_string('noattempts','quiz'); - $strtimeformat = get_string('strftimedatetime'); - - $action = optional_param('action', ''); - - switch($action) { - case 'delete': /// Some attempts need to be deleted - - $attemptids = isset($_POST['attemptid']) ? $_POST['attemptid'] : array(); - if(!is_array($attemptids) || empty($attemptids)) { - break; - } - - foreach($attemptids as $num => $attemptid) { - if(empty($attemptid)) { - unset($attemptids[$num]); - } - } - - foreach($attemptids as $attemptid) { - if ($todelete = get_record('quiz_attempts', 'id', $attemptid)) { - - delete_records('quiz_attempts', 'id', $attemptid); - delete_records('quiz_responses', 'attempt', $attemptid); - - // Search quiz_attempts for other instances by this user. - // If none, then delete record for this quiz, this user from quiz_grades - // else recalculate best grade - - $userid = $todelete->userid; - if (!record_exists('quiz_attempts', 'userid', $userid, 'quiz', $quiz->id)) { - delete_records('quiz_grades', 'userid', $userid,'quiz', $quiz->id); - } else { - quiz_save_best_grade($quiz, $userid); - } - } - } - break; - } - - if(!isset($SESSION->quiz_overview_table)) { - $SESSION->quiz_overview_table = array('noattempts' => false, 'teacherattempts' => true, 'detailedmarks' => false); - } - - foreach($SESSION->quiz_overview_table as $option => $value) { - $urlparam = optional_param($option, NULL); - if($urlparam === NULL) { - $$option = $value; - } - else { - $$option = $SESSION->quiz_overview_table[$option] = $urlparam; - } - } - - $tablecolumns = array('checkbox', 'picture', 'fullname', 'timefinish', 'duration', 'sumgrades'); - $tableheaders = array(NULL, '', get_string('fullname'), get_string('attemptedon', 'quiz'), get_string('attemptduration', 'quiz'), get_string('grade', 'quiz').'/'.$quiz->grade); - - if($detailedmarks) { - $questions = explode(',', $quiz->questions); - foreach($questions as $number => $questionid) { - $tablecolumns[] = '$'.$questionid; - $tableheaders[] = '#'.($number + 1); - } - } - - $table = new flexible_table('mod-quiz-report-overview-report'); - - $table->define_columns($tablecolumns); - $table->define_headers($tableheaders); - $table->define_baseurl($CFG->wwwroot.'/mod/quiz/report.php?id='.$cm->id); - - $table->sortable(true); - $table->collapsible(true); - $table->initialbars(true); - - $table->column_suppress('picture'); - $table->column_suppress('fullname'); - - $table->column_class('picture', 'picture'); - - $table->set_attribute('cellspacing', '0'); - $table->set_attribute('id', 'attempts'); - $table->set_attribute('class', 'generaltable generalbox'); - - // Start working -- this is necessary as soon as the niceties are over - $table->setup(); - - - /// Check to see if groups are being used in this quiz - if ($groupmode = groupmode($course, $cm)) { // Groups are being used - $currentgroup = setup_and_print_groups($course, $groupmode, "report.php?id=$cm->id&mode=overview"); - } else { - $currentgroup = false; - } - - /// Get all teachers and students - if ($currentgroup) { - $users = get_group_users($currentgroup); - } - else { - $users = get_course_users($course->id); - } - - if(!$teacherattempts) { - $teachers = get_course_teachers($course->id); - if(!empty($teachers)) { - $keys = array_keys($teachers); - } - foreach($keys as $key) { - unset($users[$key]); - } - } - - if(empty($users)) { - print_heading($strnoattempts); - return true; - } - - // Construct the SQL - - if($where = $table->get_sql_where()) { - $where .= ' AND '; - } - - if($sort = $table->get_sql_sort()) { - $sortparts = explode(',', $sort); - $newsort = array(); - $firsttime = true; - foreach($sortparts as $sortpart) { - $sortpart = trim($sortpart); - if(substr($sortpart, 0, 1) == '$') { - if($firsttime) { - $qnum = intval(substr($sortpart, 1)); - $where .= '('.sql_isnull('qr.question').' OR qr.question = '.$qnum.') AND '; - $newsort[] = 'grade '.(strpos($sortpart, 'ASC')? 'ASC' : 'DESC'); - $firsttime = false; - } - } - else { - $newsort[] = $sortpart; - } - } - $sort = ' ORDER BY '.implode(', ', $newsort); - } - - $select = 'SELECT '.$db->Concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).' AS uvsa, u.id AS userid, u.firstname, u.lastname, u.picture, qa.id AS attempt, qa.sumgrades, qa.timefinish, qa.timestart, qa.timefinish - qa.timestart AS duration '; - $group = 'GROUP BY uvsa'; - $sql = 'FROM '.$CFG->prefix.'user u '. - 'LEFT JOIN '.$CFG->prefix.'quiz_attempts qa ON u.id = qa.userid '. - 'LEFT JOIN '.$CFG->prefix.'quiz_responses qr ON qr.attempt = qa.id '. - 'WHERE '.$where.'u.id IN ('.implode(',', array_keys($users)).') AND ('.($noattempts ? sql_isnull('qa.quiz').' OR ' : '') . 'qa.quiz = '.$quiz->id.') '; - - - $total = count_records_sql('SELECT COUNT(DISTINCT('.$db->Concat('u.id', '\'#\'', $db->IfNull('qa.attempt', '0')).')) '.$sql); - $table->pagesize(10, $total); - - if($table->get_page_start() !== '' && $table->get_page_size() !== '') { - $limit = ' '.sql_paging_limit($table->get_page_start(), $table->get_page_size()); - } - else { - $limit = ''; - } - - $attempts = get_records_sql($select.$sql.$group.$sort.$limit); - - if(!empty($attempts)) { - - foreach ($attempts as $attempt) { - - $picture = print_user_picture($attempt->userid, $course->id, $attempt->picture, false, true); - - $row = array( - '', - $picture, - ''.fullname($attempt).'', - empty($attempt->attempt) ? '-' : ''.userdate($attempt->timestart, $strtimeformat).'', - empty($attempt->attempt) || empty($attempt->timefinish) ? '-' : format_time($attempt->duration), - $attempt->sumgrades === NULL ? '-' : format_float($attempt->sumgrades / $quiz->sumgrades * $quiz->grade,$quiz->decimalpoints) - ); - - if($detailedmarks) { - if(empty($attempt->attempt)) { - foreach($questions as $question) { - $row[] = '-'; - } - } - else { - $responses = get_records('quiz_responses', 'attempt', $attempt->attempt, 'question'); - foreach($responses as $response) { - $row[] = $response->grade; - } - } - } - - $table->add_data($row); - } - - echo '
'; - echo '
'; - echo ''; - - } - - $table->print_html(); - - if(!empty($attempts)) { - echo ''; - echo '
'; - echo ''.get_string('selectall', 'quiz').' / '; - echo ''.get_string('selectnone', 'quiz').' '; - echo ''; - $options = array('dummy' => ' ', 'delete' => get_string('delete')); - $menu = str_replace(' 1) submitFormById(\'attemptsform\');', '', true)); - echo $menu; - echo ''; - echo ''; - echo '
'; - echo '
'; - } - else { - print_heading(get_string('noattemptsmatchingfilter', 'quiz', strtolower($course->students))); - } - - echo '
'; - echo '
'; - echo '

'.get_string('displayoptions', 'quiz').': '; - echo ''; - echo ''; - echo ''; - echo ''; - echo ' '; - echo ' '; - echo ' '; - echo ''; - echo '

'; - echo '
'; - echo '
'; - - return true; - } - - function quiz_get_user_attempts_list($quiz, $attempts, $bestgrade, $timeformat) { - /// Returns a little list of all attempts, one per line, - /// with each grade linked to the feedback report and with the best grade highlighted - /// Each also has information about date and lapsed time - - $bestgrade = format_float($bestgrade,$quiz->decimalpoints); - - foreach ($attempts as $attempt) { - $attemptgrade = format_float(($attempt->sumgrades / $quiz->sumgrades) * $quiz->grade,$quiz->decimalpoints); - $attemptdate = userdate($attempt->timestart, $timeformat); - if ($attempt->timefinish) { - $attemptlapse = format_time($attempt->timefinish - $attempt->timestart); - } else { - $attemptlapse = "..."; - } - $button = "id\" value=\"$attempt->id\" alt=\"box$attempt->id\" />"; - $revurl = "review.php?q=$quiz->id&attempt=$attempt->id"; - if ($attemptgrade == $bestgrade) { - $userattempts[] = "$button $attemptgrade $attemptdate ($attemptlapse)"; - } else { - $userattempts[] = "$button $attemptgrade $attemptdate ($attemptlapse)"; - } - } - return implode("
\n", $userattempts); - } - -} - -?> diff --git a/mod/quiz/report/regrade/report.php b/mod/quiz/report/regrade/report.php deleted file mode 100644 index 9c13cb5e533..00000000000 --- a/mod/quiz/report/regrade/report.php +++ /dev/null @@ -1,77 +0,0 @@ -id)) { - print_header(get_string("noattempts", "quiz")); - print_continue("report.php?id=$cm->id"); - print_footer($course); - exit; - } - - $users = array(); - $count->attempt = 0; - $count->changed = 0; - - foreach ($attempts as $attempt) { - - set_time_limit(120); - - if (!$attempt->timefinish) { // Skip incomplete attempts - continue; - } - - if($quiz->timelimit > 0) { - $timelimit = ($quiz->timelimit * 60) + 60; - $timetaken = $attempt->timefinish - $attempt->timestart; - if($timetaken > $timelimit) { - // Skip overdued attempts - continue; - } - } - - $count->attempt++; - - if (! $questions = quiz_get_attempt_questions($quiz, $attempt)) { - error("Could not reconstruct quiz results for attempt $attempt->id!"); - } - - if (!$result = quiz_grade_responses($quiz, $questions)) { - error("Could not re-grade this quiz attempt!"); - } - - if ($attempt->sumgrades != $result->sumgrades) { - $attempt->sumgrades = $result->sumgrades; - $count->changed++; - - if (! update_record("quiz_attempts", $attempt)) { - notify("Could not regrade attempt $attempt->id"); - } - } - - $users[$attempt->userid] = $attempt->userid; - } - - if ($users) { - foreach ($users as $userid) { - if (! quiz_save_best_grade($quiz, $userid)) { - notify("Could not save best grade for user $userid!"); - } - } - } - - print_heading(get_string("regradecomplete", "quiz")); - print_heading(get_string("regradecount", "quiz", $count)); - - return true; - - } -} - -?> diff --git a/mod/quiz/report/simplestat/report.php b/mod/quiz/report/simplestat/report.php index bf8aa9a7dc0..c8e2fe68627 100644 --- a/mod/quiz/report/simplestat/report.php +++ b/mod/quiz/report/simplestat/report.php @@ -20,11 +20,11 @@ function quiz_report_simplestat_get_attempt_responses($attempt) { global $CFG; if (!$responses = get_records_sql("SELECT q.id, q.qtype, q.category, q.questiontext, - q.defaultgrade, q.image, r.answer - FROM {$CFG->prefix}quiz_responses r, + q.defaultgrade, q.image, s.answer + FROM {$CFG->prefix}quiz_states s, {$CFG->prefix}quiz_questions q WHERE r.attempt = '$attempt->id' - AND q.id = r.question")) { + AND q.id = s.question")) { notify("Could not find any responses for that attempt!"); return false; } @@ -402,7 +402,7 @@ function quiz_report_simplestat_grade_attempt_results($quiz, $questions) { error("No questions!"); } - if (!$grades = get_records_menu("quiz_question_grades", "quiz", $quiz->id, "", "question,grade")) { + if (!$grades = get_records_menu('quiz_question_instances', 'quiz', $quiz->id, '', 'question,grade')) { error("No grades defined for these quiz questions!"); } @@ -448,6 +448,9 @@ class quiz_report extends quiz_default_report { global $CFG; global $download; + print_heading('Not yet implemented'); + return true; + optional_variable($download, ""); /// Check to see if groups are being used in this quiz @@ -550,12 +553,12 @@ class quiz_report extends quiz_default_report { /// Print names of all the fields $myxls->write_string(0,0,format_string($quiz->name,true)); $myxls->set_column(0,0,25); - + $myxls->set_column(1,$count,9); for ($i=1; $i<=$count; $i++) { $myxls->write_string(0,$i,$i); } - + /// Print all the user data $row=1; @@ -587,16 +590,16 @@ class quiz_report extends quiz_default_report { } $workbook->close(); - + exit; } - + /// If a text file is wanted, produce one if ($download == "txt") { /// Print header to force download - - header("Content-Type: application/download\n"); + + header("Content-Type: application/download\n"); $downloadfilename = clean_filename("$course->shortname ".format_string($quiz->name,true)); header("Content-Disposition: attachment; filename=$downloadfilename.txt"); header("Expires: 0"); @@ -605,13 +608,13 @@ class quiz_report extends quiz_default_report { /// Print names of all the fields - + echo format_string($quiz->name,true); for ($i=1; $i<=$count; $i++) { echo "\t$i"; } echo "\n"; - + /// Print all the user data foreach ($data as $userid => $datum) { @@ -639,7 +642,7 @@ class quiz_report extends quiz_default_report { echo "\t$percent"; } echo "\n"; - + exit; } @@ -697,7 +700,7 @@ class quiz_report extends quiz_default_report { $options["download"] = "txt"; print_single_button("report.php", $options, get_string("downloadtext")); echo "
"; - + return true; } diff --git a/mod/quiz/restorelib.php b/mod/quiz/restorelib.php index a4584f84206..1cb7c25aaba 100644 --- a/mod/quiz/restorelib.php +++ b/mod/quiz/restorelib.php @@ -28,6 +28,24 @@ // | (CL,pk->id,fk->definition) // | // | + // |....................................... + // | . + // quiz | . + // (CL,pk->id) | ----quiz_question_datasets---- . + // | | | (CL,pk->id,fk->question, | . + // |-------------------| | | fk->dataset_definition) | . + // quiz_grades | | | | . + // (UL,pk->id,fk->quiz) |---------------------| | | | . + // | | | | | . + // quiz_attempts | quiz_questions quiz_dataset_definitions + // (UL,pk->id,fk->quiz) | (CL,pk->id,fk->category,files) (CL,pk->id,fk->category) + // | |--------------------------------| | + // | quiz_question_instances | | + // | (CL,pk->id,fk->quiz,fk->question) | | + // | | | quiz_dataset_items + // |---------------------| | (CL,pk->id,fk->definition) + // quiz_states | + // (UL,pk->id, fk->attempt, fk->question_instance) | // | // -------------------------------------------------------------------------------------------------------------- // | | | | | | | @@ -88,7 +106,7 @@ // - quiz_question_grades // - quiz_attempts // - quiz_grades - // - quiz_responses + // - quiz_states // This step is the standard mod backup. (course dependent). //STEP 1. Restore categories/questions and associated structures @@ -327,7 +345,7 @@ $question->hidden = backup_todb($que_info['#']['HIDDEN']['0']['#']); // If it is a random question then hide it - if ($question->qtype == 4) { + if ($question->qtype = RANDOM) { $question->hidden = 1; } @@ -1394,39 +1412,39 @@ return $status; } - //This function restores the quiz_question_grades - function quiz_question_grades_restore_mods($quiz_id,$info,$restore) { + //This function restores the quiz_question_instances + function quiz_question_instances_restore_mods($quiz_id,$info,$restore) { global $CFG; $status = true; - //Get the quiz_question_grades array - $grades = $info['MOD']['#']['QUESTION_GRADES']['0']['#']['QUESTION_GRADE']; + //Get the quiz_question_instances array + $instances = $info['MOD']['#']['QUESTION_INSTANCES']['0']['#']['QUESTION_INSTANCE']; - //Iterate over question_grades - for($i = 0; $i < sizeof($grades); $i++) { - $gra_info = $grades[$i]; - //traverse_xmlize($gra_info); //Debug + //Iterate over question_instances + for($i = 0; $i < sizeof($instances); $i++) { + $ins_info = $instances[$i]; + //traverse_xmlize($ins_info); //Debug //print_object ($GLOBALS['traverse_array']); //Debug //$GLOBALS['traverse_array']=""; //Debug //We'll need this later!! - $oldid = backup_todb($gra_info['#']['ID']['0']['#']); + $oldid = backup_todb($ins_info['#']['ID']['0']['#']); - //Now, build the QUESTION_GRADES record structure - $grade->quiz = $quiz_id; - $grade->question = backup_todb($gra_info['#']['QUESTION']['0']['#']); - $grade->grade = backup_todb($gra_info['#']['GRADE']['0']['#']); + //Now, build the QUESTION_INSTANCES record structure + $instance->quiz = $quiz_id; + $instance->question = backup_todb($ins_info['#']['QUESTION']['0']['#']); + $instance->grade = backup_todb($ins_info['#']['GRADE']['0']['#']); //We have to recode the question field - $question = backup_getid($restore->backup_unique_code,"quiz_questions",$grade->question); + $question = backup_getid($restore->backup_unique_code,"quiz_questions",$instance->question); if ($question) { - $grade->question = $question->new_id; + $instance->question = $question->new_id; } - //The structure is equal to the db, so insert the quiz_question_grades - $newid = insert_record ("quiz_question_grades",$grade); + //The structure is equal to the db, so insert the quiz_question_instances + $newid = insert_record ("quiz_question_instances",$instance); //Do some output if (($i+1) % 10 == 0) { @@ -1439,7 +1457,78 @@ if ($newid) { //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"quiz_question_grades",$oldid, + backup_putid($restore->backup_unique_code,"quiz_question_instances",$oldid, + $newid); + } else { + $status = false; + } + } + + return $status; + } + + //This function restores the quiz_question_versions + function quiz_question_versions_restore_mods($quiz_id,$info,$restore) { + + global $CFG, $USER; + + $status = true; + + //Get the quiz_question_versions array + $versions = $info['MOD']['#']['QUESTION_VERSIONS']['0']['#']['QUESTION_VERSION']; + + //Iterate over question_versions + for($i = 0; $i < sizeof($versions); $i++) { + $ver_info = $versions[$i]; + //traverse_xmlize($ver_info); //Debug + //print_object ($GLOBALS['traverse_array']); //Debug + //$GLOBALS['traverse_array']=""; //Debug + + //We'll need this later!! + $oldid = backup_todb($ver_info['#']['ID']['0']['#']); + + //Now, build the QUESTION_VERSIONS record structure + $version->quiz = $quiz_id; + $version->oldquestion = backup_todb($ver_info['#']['OLDQUESTION']['0']['#']); + $version->newquestion = backup_todb($ver_info['#']['NEWQUESTION']['0']['#']); + $version->userid = backup_todb($ver_info['#']['USERID']['0']['#']); + $version->timestamp = backup_todb($ver_info['#']['TIMESTAMP']['0']['#']); + + //We have to recode the oldquestion field + $question = backup_getid($restore->backup_unique_code,"quiz_questions",$version->oldquestion); + if ($question) { + $version->oldquestion = $question->new_id; + } + + //We have to recode the newquestion field + $question = backup_getid($restore->backup_unique_code,"quiz_questions",$version->newquestion); + if ($question) { + $version->newquestion = $question->new_id; + } + + //We have to recode the userid field + $user = backup_getid($restore->backup_unique_code,"user",$version->userid); + if ($user) { + $version->userid = $user->new_id; + } else { //Assign to current user + $version->userid = $USER->id; + } + + //The structure is equal to the db, so insert the quiz_question_versions + $newid = insert_record ("quiz_question_versions",$version); + + //Do some output + if (($i+1) % 10 == 0) { + echo "."; + if (($i+1) % 200 == 0) { + echo "
"; + } + backup_flush(300); + } + + if ($newid) { + //We have the newid, update backup_ids + backup_putid($restore->backup_unique_code,"quiz_question_versions",$oldid, $newid); } else { $status = false; @@ -1572,8 +1661,8 @@ //We have the newid, update backup_ids backup_putid($restore->backup_unique_code,"quiz_attempts",$oldid, $newid); - //Now process quiz_responses - $status = quiz_responses_restore_mods($newid,$att_info,$restore); + //Now process quiz_states + $status = quiz_states_restore_mods($newid,$att_info,$restore); } else { $status = false; } @@ -1582,18 +1671,18 @@ return $status; } - //This function restores the quiz_responses - function quiz_responses_restore_mods($attempt_id,$info,$restore) { + //This function restores the quiz_states + function quiz_states_restore_mods($attempt_id,$info,$restore) { global $CFG; $status = true; - //Get the quiz_responses array - $responses = $info['#']['RESPONSES']['0']['#']['RESPONSE']; - //Iterate over responses - for($i = 0; $i < sizeof($responses); $i++) { - $res_info = $responses[$i]; + //Get the quiz_states array + $states = $info['#']['STATES']['0']['#']['STATE']; + //Iterate over states + for($i = 0; $i < sizeof($states); $i++) { + $res_info = $states[$i]; //traverse_xmlize($res_info); //Debug //print_object ($GLOBALS['traverse_array']); //Debug //$GLOBALS['traverse_array']=""; //Debug @@ -1609,9 +1698,15 @@ $response->grade = backup_todb($res_info['#']['GRADE']['0']['#']); //We have to recode the question field - $question = backup_getid($restore->backup_unique_code,"quiz_questions",$response->question); + $question = backup_getid($restore->backup_unique_code,"quiz_questions",$state->question); if ($question) { - $response->question = $question->new_id; + $state->question = $question->new_id; + } + + //We have to recode the originalquestion field + $question = backup_getid($restore->backup_unique_code,"quiz_questions",$response->originalquestion); + if ($question) { + $response->originalquestion = $question->new_id; } //We have to recode the originalquestion field @@ -1623,7 +1718,7 @@ //We have to recode the answer field //It depends of the question type !! //We get the question first - $question = get_record("quiz_questions","id",$response->question); + $question = get_record("quiz_questions","id",$state->question); //It exists if ($question) { //Depending of the qtype, we make different recodes @@ -1633,16 +1728,16 @@ break; case 2: //TRUEFALSE QTYPE //The answer is one answer id. We must recode it - $answer = backup_getid($restore->backup_unique_code,"quiz_answers",$response->answer); + $answer = backup_getid($restore->backup_unique_code,"quiz_answers",$state->answer); if ($answer) { - $response->answer = $answer->new_id; + $state->answer = $answer->new_id; } break; case 3: //MULTICHOICE QTYPE //The answer is a comma separated list of answers. We must recode them $answer_field = ""; $in_first = true; - $tok = strtok($response->answer,","); + $tok = strtok($state->answer,","); while ($tok) { //Get the answer from backup_ids $answer = backup_getid($restore->backup_unique_code,"quiz_answers",$tok); @@ -1657,20 +1752,20 @@ //check for next $tok = strtok(","); } - $response->answer = $answer_field; + $state->answer = $answer_field; break; case 4: //RANDOM QTYPE //The answer links to another question id, we must recode it - $answer_link = backup_getid($restore->backup_unique_code,"quiz_questions",$response->answer); + $answer_link = backup_getid($restore->backup_unique_code,"quiz_questions",$state->answer); if ($answer_link) { - $response->answer = $answer_link->new_id; + $state->answer = $answer_link->new_id; } break; case 5: //MATCH QTYPE //The answer is a comma separated list of hypen separated math_subs (for question and answer) $answer_field = ""; $in_first = true; - $tok = strtok($response->answer,","); + $tok = strtok($state->answer,","); while ($tok) { //Extract the match_sub for the question and the answer $exploded = explode("-",$tok); @@ -1695,13 +1790,13 @@ //check for next $tok = strtok(","); } - $response->answer = $answer_field; + $state->answer = $answer_field; break; case 6: //RANDOMSAMATCH QTYPE //The answer is a comma separated list of hypen separated question_id and answer_id. We must recode them $answer_field = ""; $in_first = true; - $tok = strtok($response->answer,","); + $tok = strtok($state->answer,","); while ($tok) { //Extract the question_id and the answer_id $exploded = explode("-",$tok); @@ -1726,7 +1821,7 @@ //check for next $tok = strtok(","); } - $response->answer = $answer_field; + $state->answer = $answer_field; break; case 7: //DESCRIPTION QTYPE //Nothing to do (there is no awser to this qtype) @@ -1739,7 +1834,7 @@ //The answer is a comma separated list of hypen separated multianswer_id and answers. We must recode them. $answer_field = ""; $in_first = true; - $tok = strtok($response->answer,","); + $tok = strtok($state->answer,","); while ($tok) { //Extract the multianswer_id and the answer $exploded = explode("-",$tok); @@ -1775,7 +1870,7 @@ //check for next $tok = strtok(","); } - $response->answer = $answer_field; + $state->answer = $answer_field; break; case 10: //CALCULATED QTYPE //Nothing to do. The response is a text. @@ -1790,7 +1885,7 @@ } //The structure is equal to the db, so insert the quiz_attempts - $newid = insert_record ("quiz_responses",$response); + $newid = insert_record ("quiz_states",$state); //Do some output if (($i+1) % 10 == 0) { @@ -1803,7 +1898,7 @@ if ($newid) { //We have the newid, update backup_ids - backup_putid($restore->backup_unique_code,"quiz_responses",$oldid, + backup_putid($restore->backup_unique_code,"quiz_states",$oldid, $newid); } else { $status = false; diff --git a/mod/quiz/review.php b/mod/quiz/review.php index 65f8d624964..e54648acef3 100644 --- a/mod/quiz/review.php +++ b/mod/quiz/review.php @@ -1,56 +1,64 @@ course)) { - error("Course is misconfigured"); - } - - if (! $quiz = get_record("quiz", "id", $cm->instance)) { - error("Course module is incorrect"); - } - - } else { - if (! $quiz = get_record("quiz", "id", $q)) { - error("Course module is incorrect"); - } - if (! $course = get_record("course", "id", $quiz->course)) { - error("Course is misconfigured"); - } - if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) { - error("Course Module ID was incorrect"); - } - } + $attempt = required_param('attempt', PARAM_INT); // A particular attempt ID for review + $page = optional_param('page', 0, PARAM_INT); // The required page + $showall = optional_param('showall', 0, PARAM_BOOL); if (! $attempt = get_record("quiz_attempts", "id", $attempt)) { error("No such attempt ID exists"); } + if (! $quiz = get_record("quiz", "id", $attempt->quiz)) { + error("Course module is incorrect"); + } + if (! $course = get_record("course", "id", $quiz->course)) { + error("Course is misconfigured"); + } + if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) { + error("Course Module ID was incorrect"); + } + if (!count_records('quiz_newest_states', 'attemptid', $attempt->id)) { + // this question has not yet been upgraded to the new model + quiz_upgrade_states($attempt); + } require_login($course->id, false, $cm); + $isteacher = isteacher($course->id); + $popup = false; // controls whether the protected window is used - if (!isteacher($course->id)) { - if (!$quiz->review) { + if (!$isteacher) { + if (!$attempt->timefinish) { + redirect('attempt.php?q='.$quiz->id); + } + // If not even responses are to be shown in review then we + // don't allow any review + if (!($quiz->review & QUIZ_REVIEW_RESPONSES)) { error(get_string("noreview", "quiz")); } - if (time() < $quiz->timeclose and $quiz->review == QUIZ_REVIEW_AFTER) { - error(get_string("noreviewuntil", "quiz", userdate($quiz->timeclose))); - } - if (time() > $quiz->timeclose and $quiz->review == QUIZ_REVIEW_BEFORE) { - error(get_string("noreview", "quiz")); + if ((time() - $attempt->timefinish) > 120) { // always allow review right after attempt + if (time() < $quiz->timeclose and !($quiz->review & QUIZ_REVIEW_OPEN)) { + error(get_string("noreviewuntil", "quiz", userdate($quiz->timeclose))); + } + if (time() >= $quiz->timeclose and !($quiz->review & QUIZ_REVIEW_CLOSED)) { + error(get_string("noreview", "quiz")); + } + } else { + // immediately after the attempt the review should be in the protected window if required + $popup = $quiz->popup; } if ($attempt->userid != $USER->id) { error("This is not your attempt!"); @@ -59,57 +67,82 @@ add_to_log($course->id, "quiz", "review", "review.php?id=$cm->id&attempt=$attempt->id", "$quiz->id", "$cm->id"); - -// Print the page header +/// Print the page header $strquizzes = get_string("modulenameplural", "quiz"); - $strquiz = get_string("modulename", "quiz"); - $strreport = get_string("report", "quiz"); $strreview = get_string("review", "quiz"); - $strname = get_string("name"); - $strattempts = get_string("attempts", "quiz"); $strscore = get_string("score", "quiz"); $strgrade = get_string("grade"); $strbestgrade = get_string("bestgrade", "quiz"); $strtimetaken = get_string("timetaken", "quiz"); - $strtimecompleted = get_string("timecompleted", "quiz"); + $strtimecompleted = get_string("completedon", "quiz"); $stroverdue = get_string("overdue", "quiz"); - print_header_simple(format_string($quiz->name), "", + if (!empty($popup)) { + define('MESSAGE_WINDOW', true); // This prevents the message window coming up + print_header($course->shortname.': '.format_string($quiz->name), '', '', '', '', false, '', '', false, ''); + /// Include Javascript protection for this page + include('protect_js.php'); + } else { + $strupdatemodule = isteacheredit($course->id) + ? update_module_button($cm->id, $course->id, get_string('modulename', 'quiz')) + : ""; + print_header_simple(format_string($quiz->name), "", "id\">$strquizzes -> id\">".format_string($quiz->name,true)." -> $strreview", - "", "", true); - + "", "", true, $strupdatemodule); + } echo ''; // for overlib - print_heading(format_string($quiz->name)); - - -/// Include Javascript protection for this page if required - - if (!empty($quiz->popup)) { - include("protect_js.php"); +/// Print heading and tabs if this is part of a preview + if ($isteacher) { + $currenttab = ($attempt->userid == $USER->id) ? 'preview' : ''; + include('tabs.php'); + } else { + print_heading(format_string($quiz->name)); } - if (!($questions = quiz_get_attempt_questions($quiz, $attempt))) { - error("Unable to get questions from database for quiz $quiz->id attempt $attempt->id number $attempt->attempt"); +/// Load all the questions and states needed by this script + + // load the questions needed by page + $pagelist = $showall ? quiz_questions_in_quiz($attempt->layout) : quiz_questions_on_page($attempt->layout, $page); + $sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance". + " FROM {$CFG->prefix}quiz_questions q,". + " {$CFG->prefix}quiz_question_instances i". + " WHERE i.quiz = '$quiz->id' AND q.id = i.question". + " AND q.id IN ($pagelist)"; + if (!$questions = get_records_sql($sql)) { + error('No questions found'); } - if (!$result = quiz_grade_responses($quiz, $questions)) { - error("Could not re-grade this quiz attempt!"); + // Load the question type specific information + if (!quiz_get_question_options($questions)) { + error('Could not load question options'); } + // Restore the question sessions to their most recent states + // creating new sessions where required + if (!$states = quiz_restore_question_sessions($questions, $quiz, $attempt)) { + error('Could not restore question sessions'); + } + +/// Print infobox + $timelimit = (int)$quiz->timelimit * 60; $overtime = 0; - if ($timetaken = ($attempt->timefinish - $attempt->timestart)) { - if($timelimit && $timetaken > ($timelimit + 60)) { - $overtime = $timetaken - $timelimit; - $overtime = format_time($overtime); + if ($attempt->timefinish) { + if ($timetaken = ($attempt->timefinish - $attempt->timestart)) { + if($timelimit && $timetaken > ($timelimit + 60)) { + $overtime = $timetaken - $timelimit; + $overtime = format_time($overtime); + } + $timetaken = format_time($timetaken); + } else { + $timetaken = "-"; } - $timetaken = format_time($timetaken); } else { - $timetaken = "-"; + $timetaken = get_string('unfinished', 'quiz'); } $table->align = array("right", "left"); @@ -118,41 +151,84 @@ $picture = print_user_picture($student->id, $course->id, $student->picture, false, true); $table->data[] = array($picture, fullname($student, true)); } - $table->data[] = array("$strtimetaken:", $timetaken); - $table->data[] = array("$strtimecompleted:", userdate($attempt->timefinish)); + if ($isteacher and count($attempts = get_records_select('quiz_attempts', "quiz = '$quiz->id' AND userid = '$attempt->userid'", 'attempt ASC')) > 1) { + // print list of attempts + $attemptlist = ''; + foreach ($attempts as $at) { + $attemptlist .= ($at->id == $attempt->id) + ? ''.$at->attempt.', ' + : ''.$at->attempt.', '; + } + $table->data[] = array(get_string('attempts', 'quiz').':', trim($attemptlist, ' ,')); + } + + $table->data[] = array(get_string('startedon', 'quiz').':', userdate($attempt->timestart)); + if ($attempt->timefinish) { + $table->data[] = array("$strtimecompleted:", userdate($attempt->timefinish)); + $table->data[] = array("$strtimetaken:", $timetaken); + } if (!empty($overtime)) { $table->data[] = array("$stroverdue:", $overtime); } if ($quiz->grade) { if($overtime) { $result->sumgrades = "0"; - $result->percentage = "0"; $result->grade = "0.0"; } - $table->data[] = array("$strscore:", "$result->sumgrades/$quiz->sumgrades ($result->percentage %)"); - $table->data[] = array("$strgrade:", "$result->grade/$quiz->grade"); + $percentage = format_float(($attempt->sumgrades/$quiz->sumgrades)*100, 0); + $grade = format_float(($attempt->sumgrades/$quiz->sumgrades)*$quiz->grade, $CFG->quiz_decimalpoints); + $table->data[] = array("$strscore:", "$attempt->sumgrades/$quiz->sumgrades ($percentage %)"); + $table->data[] = array("$strgrade:", $grade.get_string('outof', 'quiz').$quiz->grade); + } + if ($isteacher and $attempt->userid == $USER->id) { + // the teacher is at the end of a preview. Print button to start new preview + unset($buttonoptions); + $buttonoptions['q'] = $quiz->id; + $buttonoptions['forcenew'] = true; + echo '
'; + print_single_button($CFG->wwwroot.'/mod/quiz/attempt.php', $buttonoptions, get_string('startagain', 'quiz')); + echo '
'; + } else { // print number of the attempt + print_heading(get_string('reviewofattempt', 'quiz', $attempt->attempt)); } - print_table($table); - if (isteacher($course->id)) { - print_continue("report.php?q=$quiz->id"); - } else { - print_continue("view.php?q=$quiz->id"); +/// Print the navigation panel if required + $numpages = quiz_number_of_pages($attempt->layout); + if ($numpages > 1 and !$showall) { + print_paging_bar($numpages, $page, 1, 'review.php?attempt='.$attempt->id.'&'); + echo '
'; + print_string('showall', 'quiz'); + echo '
'; } - $quiz->feedback = true; - $quiz->correctanswers = true; - $quiz->shuffleanswers = false; - $quiz->shufflequestions = false; - quiz_print_quiz_questions($quiz, $questions, $result); +/// Print all the questions - if (isteacher($course->id)) { - print_continue("report.php?q=$quiz->id"); - } else { - print_continue("view.php?q=$quiz->id"); + $pagequestions = explode(',', $pagelist); + $number = quiz_first_questionnumber($attempt->layout, $pagelist); + foreach ($pagequestions as $i) { + $options = quiz_get_reviewoptions($quiz, $attempt, $isteacher); + $options->validation = QUIZ_EVENTVALIDATE === $states[$i]->event; + $options->history = ($isteacher and !$attempt->preview) ? 'all' : 'graded'; + // Print the question + if ($i > 0) { + echo "
\n"; + } + quiz_print_quiz_question($questions[$i], $states[$i], $number, $quiz, $options); + $number += $questions[$i]->length; } - print_footer($course); + // Print the navigation panel if required + if ($numpages > 1 and !$showall) { + print_paging_bar($numpages, $page, 1, 'review.php?attempt='.$attempt->id.'&'); + } + // print javascript button to close the window, if necessary + if (!$isteacher) { + include('attempt_close_js.php'); + } + + if (empty($popup)) { + print_footer($course); + } ?> diff --git a/mod/quiz/reviewoptions.html b/mod/quiz/reviewoptions.html new file mode 100644 index 00000000000..e9c5a5643a3 --- /dev/null +++ b/mod/quiz/reviewoptions.html @@ -0,0 +1,86 @@ + + + + + + + + + + +
:
:
:
:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ review & QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY) ? 'checked="checked"' : '' ?> /> + + review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY) ? 'checked="checked"' : '' ?> /> + + review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY) ? 'checked="checked"' : '' ?> /> + + review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY) ? 'checked="checked"' : '' ?> /> +
+ review & QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN) ? 'checked="checked"' : '' ?> /> + + review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN) ? 'checked="checked"' : '' ?> /> + + review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN) ? 'checked="checked"' : '' ?> /> + + review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN) ? 'checked="checked"' : '' ?> /> +
+ review & QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED) ? 'checked="checked"' : '' ?> /> + + review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED) ? 'checked="checked"' : '' ?> /> + + review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED) ? 'checked="checked"' : '' ?> /> + + review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED) ? 'checked="checked"' : '' ?> /> +
+ + + + diff --git a/mod/quiz/reviewquestion.php b/mod/quiz/reviewquestion.php new file mode 100644 index 00000000000..be75dfdce7f --- /dev/null +++ b/mod/quiz/reviewquestion.php @@ -0,0 +1,117 @@ +attempt)) { + error('No such attempt ID exists'); + } + } elseif ($attemptid) { + if (! $attempt = get_record('quiz_attempts', 'id', $attemptid)) { + error('No such attempt ID exists'); + } + if (! $neweststate = get_field('quiz_newest_states', 'new', 'attemptid', $attemptid, 'questionid', $questionid)) { + // newest_state not set, probably because this is an old attempt from the old quiz module code + if (! $state = get_record('quiz_states', 'question', $questionid, 'attempt', $attemptid)) { + error('Invalid question id'); + } + } else { + if (! $state = get_record('quiz_states', 'id', $neweststate->new)) { + error('Invalid state id'); + } + } + } else { + error('Parameter missing'); + } + if (! $question = get_record('quiz_questions', 'id', $state->question)) { + error('Question for this state is missing'); + } + if (! $quiz = get_record('quiz', 'id', $attempt->quiz)) { + error('Course module is incorrect'); + } + if (! $course = get_record('course', 'id', $quiz->course)) { + error('Course is misconfigured'); + } + if (! $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id)) { + error('Course Module ID was incorrect'); + } + + require_login($course->id, false, $cm); + $isteacher = isteacher($course->id); + + if (!$isteacher) { + if (!$attempt->timefinish) { + redirect('attempt.php?q='.$quiz->id); + } + // If not even responses are to be shown in review then we + // don't allow any review + if (!($quiz->review & QUIZ_REVIEW_RESPONSES)) { + error(get_string("noreview", "quiz")); + } + if ((time() - $attempt->timefinish) > 120) { // always allow review right after attempt + if (time() < $quiz->timeclose and !($quiz->review & QUIZ_REVIEW_OPEN)) { + error(get_string("noreviewuntil", "quiz", userdate($quiz->timeclose))); + } + if (time() >= $quiz->timeclose and !($quiz->review & QUIZ_REVIEW_CLOSED)) { + error(get_string("noreview", "quiz")); + } + } + if ($attempt->userid != $USER->id) { + error('This is not your attempt!'); + } + } + + //add_to_log($course->id, 'quiz', 'review', "review.php?id=$cm->id&attempt=$attempt->id", "$quiz->id", "$cm->id"); + +/// Print the page header + + $strquizzes = get_string('modulenameplural', 'quiz'); + $strreviewquestion = get_string('reviewquestion', 'quiz'); + + print_header(); + + echo ''; // for overlib + +/// Print heading + print_heading(format_string($quiz->name)); + + $instance = get_record('quiz_question_instances', 'quiz', $quiz->id, 'question', $question->id); + $question->instance = $instance->id; + $question->maxgrade = $instance->grade; + $question->name_prefix = 'r'; + $QUIZ_QTYPES[$question->qtype]->get_question_options($question); + + quiz_restore_state($question, $state); + $state->last_graded = $state; + + $options = quiz_get_reviewoptions($quiz, $attempt, $isteacher); + $options->validation = ($state->event == QUIZ_EVENTVALIDATE); + $options->history = 'all'; + + quiz_print_quiz_question($question, $state, $number, $quiz, $options); + + print_footer(); + +?> diff --git a/mod/quiz/styles.php b/mod/quiz/styles.php new file mode 100644 index 00000000000..7bcb92256eb --- /dev/null +++ b/mod/quiz/styles.php @@ -0,0 +1,61 @@ + +.feedbacktext { + display:block; +} + +body#mod-quiz-attempts table#attempts, +body#mod-quiz-attempts table#commands +{ + width: 80%; + margin: auto; +} +body#mod-quiz-attempts table#attempts { + margin: 20px auto; +} +body#mod-quiz-attempts table#attempts .header, +body#mod-quiz-attempts table#attempts .cell +{ + padding: 4px; +} +body#mod-quiz-attempts table#attempts .header .commands { + display: inline; +} +body#mod-quiz-attempts table#attempts .picture { + width: 40px; +} +body#mod-quiz-attempts table#attempts td { + border-left-width: 1px; + border-right-width: 1px; + border-left-style: solid; + border-right-style: solid; + vertical-align: bottom; +} +.feedbacktext { + text-align:right; +} +body#mod-quiz-attempts table#attempts .header { + text-align: left; +} +body#mod-quiz-attempts table#attempts .picture { + text-align: center !important; +} +body#mod-quiz-attempts .controls { + text-align: center; +} + + + + +body#mod-quiz-attempts table#attempts td { + border-color: #dddddd; +} +body#mod-quiz-attempts table#attempts .r1 { + background-color: #eeeeee; +} + + + + +.editorhelptext { + font-size:0.7em; +} diff --git a/mod/quiz/tabs.php b/mod/quiz/tabs.php new file mode 100644 index 00000000000..285f55e055a --- /dev/null +++ b/mod/quiz/tabs.php @@ -0,0 +1,59 @@ +id); + } + if (!isset($course)) { + $course = get_record('course', 'id', $quiz->course); + } + + //print_heading(format_string($quiz->name)); + + $tabs = array(); + $row = array(); + $inactive = array(); + + $row[] = new tabobject('info', "view.php?q=$quiz->id", get_string('info', 'quiz')); + $row[] = new tabobject('attempts', "attempts.php?q=$quiz->id", get_string('attempts', 'quiz')); + $row[] = new tabobject('preview', "attempt.php?q=$quiz->id", get_string('preview', 'quiz')); + if (isteacheredit($course->id)) { + $row[] = new tabobject('edit', "edit.php?quizid=$quiz->id", get_string('editquiz', 'quiz')); + //$row[] = new tabobject('update', "$CFG->wwwroot/course/mod.php?update=$cm->id&sesskey=$USER->sesskey", get_string('updatethis', '', get_string('modulename', 'quiz'))); + } + $row[] = new tabobject('reports', "report.php?q=$quiz->id", get_string('reports', 'quiz')); + + $tabs[] = $row; + + if ($currenttab == 'reports' and isset($mode)) { + $inactive[] = 'reports'; + $allreports = get_list_of_plugins("mod/quiz/report"); + $reportlist = array ('simplestat', 'fullstat'); // Standard reports we want to show first + + foreach ($allreports as $report) { + if (!in_array($report, $reportlist)) { + $reportlist[] = $report; + } + } + + $row = array(); + $currenttab = ''; + foreach ($reportlist as $report) { + $row[] = new tabobject($report, "report.php?q=$quiz->id&mode=$report", + get_string("report$report", "quiz")); + if ($report == $mode) { + $currenttab = $report; + } + } + $tabs[] = $row; + } + + print_tabs($tabs, $currenttab, $inactive); + +?> diff --git a/mod/quiz/timer.js b/mod/quiz/timer.js index f3ddba35b21..b02823286ce 100644 --- a/mod/quiz/timer.js +++ b/mod/quiz/timer.js @@ -10,7 +10,7 @@ function countdown_clock() { if(quizTimerValue == 0) { clearTimeout(timeout_id); - //alert(timesup); + document.forms[0].timeup.value = 1; document.forms[0].submit(); } diff --git a/mod/quiz/version.php b/mod/quiz/version.php index acb747c541d..7f3988ba209 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -5,7 +5,7 @@ // This fragment is called by moodle_needs_upgrading() and /admin/index.php //////////////////////////////////////////////////////////////////////////////// -$module->version = 2005042002; // The (date) version of this module +$module->version = 2005050405; // The (date) version of this module $module->requires = 2005021600; // Requires this Moodle version $module->cron = 0; // How often should cron check this module (seconds)? diff --git a/mod/quiz/view.php b/mod/quiz/view.php index d602eb4c051..6372ee6133a 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -11,15 +11,16 @@ $q = optional_param('q', 0, PARAM_INT); // quiz ID $edit = optional_param('edit', ''); + if ($id) { if (! $cm = get_record("course_modules", "id", $id)) { error("Course Module ID was incorrect"); } - + if (! $course = get_record("course", "id", $cm->course)) { error("Course is misconfigured"); } - + if (! $quiz = get_record("quiz", "id", $cm->instance)) { error("Course module is incorrect"); } @@ -37,7 +38,8 @@ } require_login($course->id, false, $cm); - + $isteacher = isteacher($course->id); + // if no questions have been set up yet redirect to edit.php if (!$quiz->questions and isteacheredit($course->id)) { redirect('edit.php?quizid='.$quiz->id); @@ -75,30 +77,54 @@ echo ''; - if (isteacher($course->id)) { - $attemptcount = count_records_select("quiz_attempts", "quiz = '$quiz->id' AND timefinish > 0"); - $a->count = count_records("quiz_grades", "quiz", "$quiz->id"); - $a->items = get_string('users'); - $strusers = get_string('counteditems', '', $a); - $strviewallanswers = get_string("viewallanswers","quiz",$attemptcount); - echo ""; - } else if (!$cm->visible) { - notice(get_string("activityiscurrentlyhidden")); - } - - $available = ($quiz->timeopen < $timenow and $timenow < $quiz->timeclose) || isteacher($course->id); + $available = ($quiz->timeopen < $timenow and $timenow < $quiz->timeclose) || $isteacher; // Print the main part of the page + // Print heading and tabs for teacher + if ($isteacher) { + $currenttab = 'info'; + include('tabs.php'); + } print_heading(format_string($quiz->name)); if (trim(strip_tags($quiz->intro))) { - print_simple_box(format_text($quiz->intro), 'center', '70%', '', 5, 'generalbox', 'intro'); + print_simple_box(format_text($quiz->intro), "center"); + } + + if ($quiz->attempts > 1) { + echo "

".get_string("attemptsallowed", "quiz").": $quiz->attempts

"; + echo "

".get_string("grademethod", "quiz").": ".$QUIZ_GRADE_METHOD[$quiz->grademethod]."

"; + } else { + echo "
"; + } + if ($available) { + if ($quiz->timelimit) { + echo "

".get_string("quiztimelimit","quiz", format_time($quiz->timelimit * 60))."

"; + } + echo "

".get_string("quizavailable", "quiz", userdate($quiz->timeclose)); + } else if ($timenow < $quiz->timeopen) { + echo "

".get_string("quiznotavailable", "quiz", userdate($quiz->timeopen)); + } else { + echo "

".get_string("quizclosed", "quiz", userdate($quiz->timeclose)); } + // This is all the teacher will get + if ($isteacher) { + print_footer($course); + exit; + } + if (isguest()) { - print_heading(get_string("guestsno", "quiz")); + + $wwwroot = $CFG->wwwroot.'/login/index.php'; + if (!empty($CFG->loginhttps)) { + $wwwroot = str_replace('http','https', $wwwroot); + } + + notice_yesno(get_string('guestno', 'quiz').'

'.get_string('liketologin'), + $wwwroot, $_SERVER['HTTP_REFERER']); print_footer($course); exit; } @@ -109,11 +135,10 @@ $numattempts = 0; } - if ($quiz->attempts > 1) { - echo "

".get_string("attemptsallowed", "quiz").": $quiz->attempts

"; - echo "

".get_string("grademethod", "quiz").": ".$QUIZ_GRADE_METHOD[$quiz->grademethod]."

"; - } else { - echo "
"; + $unfinished = false; + if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) { + $attempts[] = $unfinishedattempt; + $unfinished = true; } $strattempt = get_string("attempt", "quiz"); @@ -125,7 +150,7 @@ $mygrade = quiz_get_best_grade($quiz, $USER->id); - if ($numattempts) { + if ($numattempts) { if ($quiz->grade and $quiz->sumgrades) { if ($quiz->grade <> $quiz->sumgrades) { $table->head = array($strattempt, $strtimetaken, $strtimecompleted, "$strmarks / $quiz->sumgrades", "$strgrade / $quiz->grade"); @@ -142,61 +167,88 @@ $table->size = array("", "", ""); } foreach ($attempts as $attempt) { - if ($timetaken = ($attempt->timefinish - $attempt->timestart)) { - $timetaken = format_time($timetaken); + $timetaken = ''; + $datecompleted = ''; + if ($attempt->timefinish > 0) { + $timetaken = format_time($attempt->timefinish - $attempt->timestart); + $datecompleted = userdate($attempt->timefinish); + } else if ($available) { + $timetaken = format_time(time() - $attempt->timestart); + $strconfirmstartattempt = addslashes(get_string("confirmstartattempt","quiz")); + $datecompleted = "\n".''; + $datecompleted .= ''; } else { - $timetaken = "-"; + $timetaken = format_time($quiz->timeclose - $attempt->timestart); + $datecompleted = userdate($quiz->timeclose); } + if ($quiz->grade and $quiz->sumgrades) { - $attemptmark = ''; + $attemptmark = format_float($attempt->sumgrades,$quiz->decimalpoints); $attemptgrade = format_float(($attempt->sumgrades/$quiz->sumgrades)*$quiz->grade,$quiz->decimalpoints); if ($attemptgrade == $mygrade) { $attemptgrade = "$attemptgrade"; } - if (quiz_review_allowed($quiz)) { - $attemptmark = "id&attempt=$attempt->id\">$attempt->sumgrades"; + + // if attempt is closed and review is allowed then make attemptnumber and + // mark and grade into links to review page + if (quiz_review_allowed($quiz) and $attempt->timefinish > 0) { + $attemptmark = "id&attempt=$attempt->id\">".format_float($attempt->sumgrades,$quiz->decimalpoints).''; $attemptgrade = "id&attempt=$attempt->id\">$attemptgrade"; $attempt->attempt = "id&attempt=$attempt->id\">#$attempt->attempt"; } + if ($quiz->grade <> $quiz->sumgrades) { - $table->data[] = array( $attempt->attempt, - format_time($attempt->timefinish - $attempt->timestart), - userdate($attempt->timefinish), + $table->data[] = array( $attempt->attempt, + $timetaken, + $datecompleted, $attemptmark, $attemptgrade); } else { - $table->data[] = array( $attempt->attempt, - format_time($attempt->timefinish - $attempt->timestart), - userdate($attempt->timefinish), + $table->data[] = array( $attempt->attempt, + $timetaken, + $datecompleted, $attemptgrade); } } else { // No grades are being used if (quiz_review_allowed($quiz)) { - $attempt->attempt = "id&attempt=$attempt->id\">#$attempt->attempt"; + if($attempt->timefinish > 0) { + $attempt->attempt = "id&attempt=$attempt->id\">#$attempt->attempt"; + } else { + $attempt->attempt = "#$attempt->attempt"; + } } - $table->data[] = array( $attempt->attempt, - format_time($attempt->timefinish - $attempt->timestart), - userdate($attempt->timefinish)); + $table->data[] = array( $attempt->attempt, + $timetaken, + $datecompleted); } } print_table($table); } - if ($available) { - if ($quiz->timelimit) { - echo "

".get_string("quiztimelimit","quiz", format_time($quiz->timelimit * 60))."

"; - } - echo "

".get_string("quizavailable", "quiz", userdate($quiz->timeclose)); - } else if ($timenow < $quiz->timeopen) { - echo "

".get_string("quiznotavailable", "quiz", userdate($quiz->timeopen)); - } else { - echo "

".get_string("quizclosed", "quiz", userdate($quiz->timeclose)); - } - - if (!$quiz->questions) { print_heading(get_string("noquestions", "quiz")); } else { - if ($numattempts < $quiz->attempts or !$quiz->attempts) { + if ($numattempts < $quiz->attempts or !$quiz->attempts) { if ($available) { $options["id"] = $cm->id; if ($numattempts and $quiz->grade) { @@ -216,22 +268,24 @@ if ($quiz->grade) { print_heading(get_string("yourfinalgradeis", "quiz", "$mygrade / $quiz->grade")); } + print_continue('../../course/view.php?id='.$course->id); } } - // Finish the page echo ''; print_footer($course); - + function quiz_review_allowed($quiz) { - if (!$quiz->review) { + // If not even responses are to be shown in review then we + // don't allow any review + if (!($quiz->review & QUIZ_REVIEW_RESPONSES)) { return false; } - if ((time() < $quiz->timeclose) and ($quiz->review == QUIZ_REVIEW_AFTER)) { + if ((time() < $quiz->timeclose) and !($quiz->review & QUIZ_REVIEW_OPEN)) { return false; } - if ((time() > $quiz->timeclose) and ($quiz->review == QUIZ_REVIEW_BEFORE)) { + if ((time() > $quiz->timeclose) and !($quiz->review & QUIZ_REVIEW_CLOSED)) { return false; } return true; diff --git a/mod/quiz/view_js.php b/mod/quiz/view_js.php index 765e2baa002..b184137a9b6 100644 --- a/mod/quiz/view_js.php +++ b/mod/quiz/view_js.php @@ -4,13 +4,13 @@ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); $window = (!empty($quiz->popup)) ? 'quizpopup' : '_self'; $windowoptions = ($window == '_self') ? '' : "left=0, top=0, height='+window.screen.height+', width='+window.screen.width+', channelmode=yes, fullscreen=yes, scrollbars=yes, resizeable=no, directories=no, toolbar=no, titlebar=no, location=no, status=no, menubar=no"; $buttontext = ($numattempts) ? get_string('reattemptquiz', 'quiz') : get_string('attemptquiznow', 'quiz'); - +$buttontext = ($unfinished) ? get_string('continueattemptquiz', 'quiz') : $buttontext; ?> diff --git a/version.php b/version.php index 871ed5ae523..23b829bf992 100644 --- a/version.php +++ b/version.php @@ -6,7 +6,7 @@ // This is compared against the values stored in the database to determine // whether upgrades should be performed (see lib/db/*.php) - $version = 2005043000; // YYYYMMDD = date + $version = 2005050600; // YYYYMMDD = date // XY = increments within a single day $release = '1.5 ALPHA'; // Human-friendly version name