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 "
';
+ // 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 = '-+/*%>:^~=&|!';
+ $safeoperatorchar = '-+/*%>:^~=&|!'; /* */
$operatorornumber = "[$safeoperatorchar.0-9eE]";
@@ -580,7 +561,7 @@ function quiz_qtype_calculated_find_formula_errors($formula) {
return get_string('illegalformulasyntax', 'quiz', $regs[0]);
}
break;
-
+
// Zero argument functions
case 'pi':
if ($regs[3]) {
@@ -607,7 +588,7 @@ function quiz_qtype_calculated_find_formula_errors($formula) {
return get_string('functiontakesoneortwoargs','quiz',$regs[2]);
}
break;
-
+
// Functions that must have two arguments
case 'atan2': case 'fmod': case 'pow':
if ($regs[5] || empty($regs[4])) {
@@ -636,7 +617,7 @@ function quiz_qtype_calculated_find_formula_errors($formula) {
$formula = ereg_replace("^$regs[2]\\([^)]*\\)", '1', $formula);
}
}
-
+
if (ereg("[^$safeoperatorchar.0-9eE]+", $formula, $regs)) {
return get_string('illegalformulasyntax', 'quiz', $regs[0]);
} else {
diff --git a/mod/quiz/questiontypes/datasetdependent/abstractqtype.php b/mod/quiz/questiontypes/datasetdependent/abstractqtype.php
index e731f7678a4..20e97c531e7 100644
--- a/mod/quiz/questiontypes/datasetdependent/abstractqtype.php
+++ b/mod/quiz/questiontypes/datasetdependent/abstractqtype.php
@@ -12,16 +12,115 @@ define("LINK", "3");
class quiz_dataset_dependent_questiontype extends quiz_default_questiontype {
- var $virtualqtype= false;
-
- // Contains picked dataset numbers by category. The idea is to
- // reuse dataset number for each category within a quiz.
- var $datasetnumbers = array();
+ var $virtualqtype = false;
function name() {
return 'datasetdependent';
}
+ function save_question_options($question, $options) {
+ // Default does nothing...
+ return true;
+ }
+
+ function create_session_and_responses(&$question, &$state, $quiz, $attempt) {
+ // Find out how many datasets are available
+ global $CFG;
+ if(!$maxnumber = (int)get_field_sql(
+ "SELECT MAX(a.itemcount)
+ FROM {$CFG->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 '
-
-
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 '
';
+ 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 .= 'getLocalPart($partType) . '" />';
+ }
+ }
+ $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$name>";
+ } else {
+ $xml = "<$name$elementNS>$value$name>";
+ }
+ } else {
+ $xml = "<$name$elementNS xsi:type=\"" . $this->getPrefixFromNamespace($ns) . ":$uqType\"$encodingStyle>$value$name>";
+ }
+ $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$name>";
+ } else {
+ $xml = "<$name>$contents$name>";
+ }
+ } else {
+ $xml = "<$name xsi:type=\"" . $this->getPrefixFromNamespace('http://xml.apache.org/xml-soap') . ":$uqType\"$encodingStyle>$contents$name>";
+ }
+ $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 .= "$elementName>";
+ } 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
+ ."$name>";
+ } 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
+ ."$name>";
+ }
+ } 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$name>";
+ } else {
+ $xml = "<$name$elementNS>$value$name>";
+ }
+ } else {
+ $xml = "<$name$elementNS xsi:type=\"" . $this->getPrefixFromNamespace($ns) . ":$uqType\"$encodingStyle>$value$name>";
+ }
+ }
+ $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 .= "" . (isset($prefix) ? $prefix . ':' : '') . "$name>";
+ }
+ } elseif($pos >= $this->root_struct){
+ $this->document .= "" . (isset($prefix) ? $prefix . ':' : '') . "$name>";
+ }
+ // 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 . "$operation>";
+ } else {
+ $payload = "<$operation>" . $payload . "$operation>";
+ }
+ } else {
+ $this->debug("wrapping RPC request with encoded method element");
+ if ($namespace) {
+ $payload = "<$nsPrefix:$operation$methodEncodingStyle xmlns:$nsPrefix=\"$namespace\">" .
+ $payload .
+ "$nsPrefix:$operation>";
+ } else {
+ $payload = "<$operation$methodEncodingStyle>" .
+ $payload .
+ "$operation>";
+ }
+ }
+ }
+ // 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 '
';
+ }
+ }
+ // 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 '
+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
+?>
+
+
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 = '';
+ $table->data[] = array($link, $servers, $capabilities, $actions);
+ }
+ }
+
+ // Print the table
+ print_heading($stritemtypes);
+ print_table($table);
+
+ // Print add link
+ echo '
';
+ echo 'answer . '" alt="';
+ echo s($answers['false']->answer) . '" />';
+ 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 '
';
+
+ 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("
");
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("
".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