]*?>.*?'si", // remove javascript "'<[\/\!]*?[^]*?>'si", // remove HTML tags "'([\r\n])[\s]+'", // remove spaces "'&(quot|#34);'i", // remove HTML entites "'&(amp|#38);'i", "'&(lt|#60);'i", "'&(gt|#62);'i", "'&(nbsp|#160);'i", "'&(iexcl|#161);'i", "'&(cent|#162);'i", "'&(pound|#163);'i", "'&(copy|#169);'i", "'&#(\d+);'e"); // Evaluate like PHP $replace = array ("", "", "\\1", "\"", "&", "<", "?>", " ", chr(161), chr(162), chr(163), chr(169), "chr(\\1)"); return preg_replace ($search, $replace, $string); } class qformat_webct_modified_calculated_qtype extends question_calculated_qtype { // We need to make a modification of this qtype so that // it will be able to save webct style calculated questions // The difference between webct and Moodle is that webct // pass the individual data items as part of the question // while Moodle core treat them separately function save_question_options($question, $options = false) { if (false !== $options) { // function is called from save_question... return parent::save_question_options($question, $options); } // function is called from format.php... $datasetdatas = $question->datasets; // Set dataset $form->dataset = array(); foreach ($datasetdatas as $datasetname => $notimportant) { // Literal - question local - name $form->dataset[] = "1-0-$datasetname"; } $subtypeoptions->answers = $question->answers; $subtypeoptions->units = $question->units; unset($question->datasets); unset($question->answers); unset($question->units); $this->save_question($question, $form, 'not used', $subtypeoptions); // Save dataset options and items... // Get datasetdefinitions global $CFG; $datasetdefs = get_records_sql( "SELECT a.* FROM {$CFG->prefix}question_dataset_definitions a, {$CFG->prefix}question_datasets b WHERE a.id = b.datasetdefinition AND b.question = '$question->id' "); foreach ($datasetdefs as $datasetdef) { $datasetdata = $datasetdatas[$datasetdef->name]; // Set items and retrieve ->itemcout $item->definition = $datasetdef->id; for ($item->itemnumber=1 ; isset($datasetdata->items["$item->itemnumber"]) ; ++$item->itemnumber) { $item->value = $datasetdata->items["$item->itemnumber"]; if (!insert_record('question_dataset_items', $item)) { error("Unable to insert dataset item $item->itemnumber with $item->value for $datasetdef->name"); } } $datasetdef->itemcount = $item->itemnumber - 1; // Retrieve ->options if (is_numeric($datasetdata->min) && is_numeric($datasetdata->max) && $datasetdata->min <= $datasetdata->max) { if (is_numeric($datasetdata->dec)) { $dec = max(0, ceil($datasetdata->dec)); } else { $dec = 1; // A try } $datasetdef->options = "uniform:$datasetdata->min:$datasetdata->max:$dec"; } else { $datasetdef->options = ''; } // Save definition if ($datasetdef->itemcount || $datasetdef->options) { if (!update_record('question_dataset_definitions', $datasetdef)) { error("Unable to update dataset definition $datasetdef->name on question $question->id"); } } } // Done return true; } } $QTYPES[CALCULATED] = new qformat_webct_modified_calculated_qtype(); function qformat_webct_convert_formula($formula) { // Remove empty space, as it would cause problems otherwise: $formula = str_replace(' ', '', $formula); // Remove paranthesis after e,E and *10**: while (ereg('[0-9.](e|E|\\*10\\*\\*)\\([+-]?[0-9]+\\)', $formula, $regs)) { $formula = str_replace( $regs[0], ereg_replace('[)(]', '', $regs[0]), $formula); } // Replace *10** with e where possible while (ereg( '(^[+-]?|[^eE][+-]|[^0-9eE+-])[0-9.]+\\*10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)', $formula, $regs)) { $formula = str_replace( $regs[0], str_replace('*10**', 'e', $regs[0]), $formula); } // Replace other 10** with 1e where possible while (ereg('(^|[^0-9.eE])10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)', $formula, $regs)) { $formula = str_replace( $regs[0], str_replace('10**', '1e', $regs[0]), $formula); } // Replace all other base**exp with the PHP equivalent function pow(base,exp) // (Pretty tricky to exchange an operator with a function) while (2 == count($splits = explode('**', $formula, 2))) { // Find $base if (ereg('^(.*[^0-9.eE])?(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?|\\{[^}]*\\})$', $splits[0], $regs)) { // The simple cases $base = $regs[2]; $splits[0] = $regs[1]; } else if (ereg('\\)$', $splits[0])) { // Find the start of this parenthesis $deep = 1; for ($i = 1 ; $deep ; ++$i) { if (!ereg('^(.*[^[:alnum:]_])?([[:alnum:]_]*([)(])([^)(]*[)(]){'.$i.'})$', $splits[0], $regs)) { error("Parenthesis before ** is not properly started in $splits[0]**"); } if ('(' == $regs[3]) { --$deep; } else if (')' == $regs[3]) { ++$deep; } else { error("Impossible character $regs[3] detected as parenthesis character"); } } $base = $regs[2]; $splits[0] = $regs[1]; } else { error("Bad base before **: $splits[0]**"); } // Find $exp (similar to above but a little easier) if (ereg('^([+-]?(\\{[^}]\\}|([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?))(.*)', $splits[1], $regs)) { // The simple case $exp = $regs[1]; $splits[1] = $regs[6]; } else if (ereg('^[+-]?[[:alnum:]_]*\\(', $splits[1])) { // Find the end of the parenthesis $deep = 1; for ($i = 1 ; $deep ; ++$i) { if (!ereg('^([+-]?[[:alnum:]_]*([)(][^)(]*){'.$i.'}([)(]))(.*)', $splits[1], $regs)) { error("Parenthesis after ** is not properly closed in **$splits[1]"); } if (')' == $regs[3]) { --$deep; } else if ('(' == $regs[3]) { ++$deep; } else { error("Impossible character $regs[3] detected as parenthesis character"); } } $exp = $regs[1]; $splits[1] = $regs[4]; } // Replace it! $formula = "$splits[0]pow($base,$exp)$splits[1]"; } // Nothing more is known to need to be converted return $formula; } class qformat_webct extends qformat_default { function provide_import() { return true; } function readquestions ($lines) { $qtypecalculated = new qformat_webct_modified_calculated_qtype(); $webctnumberregex = '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?'; $questions = array(); $errors = array(); $warnings = array(); $webct_options = array(); $ignore_rest_of_question = FALSE; $nLineCounter = 0; $nQuestionStartLine = 0; $bIsHTMLText = FALSE; $lines[] = ":EOF:"; // for an easiest processing of the last line $question = $this->defaultquestion(); foreach ($lines as $line) { $nLineCounter++; // Processing multiples lines strings if (isset($questiontext) and is_string($questiontext)) { if (ereg("^:",$line)) { $question->questiontext = addslashes(trim($questiontext)); unset($questiontext); } else { $questiontext .= str_replace('\:', ':', $line); continue; } } if (isset($answertext) and is_string($answertext)) { if (ereg("^:",$line)) { $answertext = addslashes(trim($answertext)); $question->answer[$currentchoice] = $answertext; $question->subanswers[$currentchoice] = $answertext; unset($answertext); } else { $answertext .= str_replace('\:', ':', $line); continue; } } if (isset($responsetext) and is_string($responsetext)) { if (ereg("^:",$line)) { $question->subquestions[$currentchoice] = addslashes(trim($responsetext)); unset($responsetext); } else { $responsetext .= str_replace('\:', ':', $line); continue; } } if (isset($feedbacktext) and is_string($feedbacktext)) { if (ereg("^:",$line)) { $question->feedback[$currentchoice] = addslashes(trim($feedbacktext)); unset($feedbacktext); } else { $feedbacktext .= str_replace('\:', ':', $line); continue; } } $line = trim($line); if (eregi("^:(TYPE|EOF):",$line)) { // New Question or End of File if (isset($question)) { // if previous question exists, complete, check and save it // Setup default value of missing fields if (!isset($question->name)) { $question->name = $question->questiontext; } if (strlen($question->name) > 255) { $question->name = substr($question->name,0,250)."..."; $warnings[] = get_string("questionnametoolong", "quiz", $nQuestionStartLine); } if (!isset($question->defaultgrade)) { $question->defaultgrade = 1; } if (!isset($question->image)) { $question->image = ""; } // Perform sanity checks $QuestionOK = TRUE; if (strlen($question->questiontext) == 0) { $warnings[] = get_string("missingquestion", "quiz", $nQuestionStartLine); $QuestionOK = FALSE; } if (sizeof($question->answer) < 1) { // a question must have at least 1 answer $errors[] = get_string("missinganswer", "quiz", $nQuestionStartLine); $QuestionOK = FALSE; } else { // Create empty feedback array $question->feedback = array(); foreach ($question->answer as $key => $dataanswer) { $question->feedback[$key] = ''; } $maxfraction = -1; $totalfraction = 0; foreach($question->fraction as $fraction) { if ($fraction > 0) { $totalfraction += $fraction; } if ($fraction > $maxfraction) { $maxfraction = $fraction; } } switch ($question->qtype) { case SHORTANSWER: if ($maxfraction != 1) { $maxfraction = $maxfraction * 100; $errors[] = "'$question->name': ".get_string("wronggrade", "quiz", $nLineCounter).get_string("fractionsnomax", "quiz", $maxfraction); $QuestionOK = FALSE; } break; case MULTICHOICE: if ($question->single) { if ($maxfraction != 1) { $maxfraction = $maxfraction * 100; $errors[] = "'$question->name': ".get_string("wronggrade", "quiz", $nLineCounter).get_string("fractionsnomax", "quiz", $maxfraction); $QuestionOK = FALSE; } } else { $totalfraction = round($totalfraction,2); if ($totalfraction != 1) { echo "

$totalfraction

"; $totalfraction = $totalfraction * 100; $errors[] = "'$question->name': ".get_string("wronggrade", "quiz", $nLineCounter).get_string("fractionsaddwrong", "quiz", $totalfraction); $QuestionOK = FALSE; } } break; case CALCULATED: foreach ($question->answers as $answer) { if ($formulaerror = quiz_qtype_calculated_find_formula_errors($answer->answer)) { $warnings[] = "'$question->name': ". $formulaerror; $QuestionOK = FALSE; } } break; default: // No problemo } } if ($QuestionOK) { // $question->feedback = array(); $questions[] = $question; // store it unset($question); // and prepare a new one $question = $this->defaultquestion(); } } $nQuestionStartLine = $nLineCounter; } // Processing Question Header if (eregi("^:TYPE:MC:1(.*)",$line,$webct_options)) { // Multiple Choice Question with only one good answer $question->qtype = MULTICHOICE; $question->single = 1; // Only one answer is allowed $ignore_rest_of_question = FALSE; continue; } if (eregi("^:TYPE:MC:N(.*)",$line,$webct_options)) { // Multiple Choice Question with several good answers $question->qtype = MULTICHOICE; $question->single = 0; // Many answers allowed $ignore_rest_of_question = FALSE; continue; } if (eregi("^:TYPE:S",$line)) { // Short Answer Question $question->qtype = SHORTANSWER; $question->usecase = 0; // Ignore case $ignore_rest_of_question = FALSE; continue; } if (eregi("^:TYPE:C",$line)) { // Calculated Question $question->qtype = CALCULATED; $question->answers = array(); // No problem as they go as :FORMULA: from webct $question->units = array(); $question->datasets = array(); // To make us pass the end-of-question sanity checks $question->answer = array('dummy'); $question->fraction = array('1.0'); $currentchoice = -1; $ignore_rest_of_question = FALSE; continue; } if (eregi("^:TYPE:M",$line)) { // Match Question $question->qtype = MATCH; $ignore_rest_of_question = FALSE; // match question processing is not debugged continue; } if (eregi("^:TYPE:P",$line)) { // Paragraph Question $warnings[] = get_string("paragraphquestion", "quiz", $nLineCounter); $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle continue; } if (eregi("^:TYPE:",$line)) { // Unknow Question $warnings[] = get_string("unknowntype", "quiz", $nLineCounter); $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle continue; } if ($ignore_rest_of_question) { continue; } if (eregi("^:TITLE:(.*)",$line,$webct_options)) { $name = trim($webct_options[1]); if (strlen($name) > 255) { $name = substr($name,0,250)."..."; $warnings[] = get_string("questionnametoolong", "quiz", $nLineCounter); } $question->name = addslashes($name); continue; } if (eregi("^:IMAGE:(.*)",$line,$webct_options)) { $filename = trim($webct_options[1]); if (eregi("^http://",$filename)) { $question->image = $filename; } continue; } // Need to put the parsing of calculated items here to avoid ambitiuosness: // if question isn't defined yet there is nothing to do here (avoid notices) if (!isset($question)) { continue; } if (CALCULATED == $question->qtype && ereg( "^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?($webctnumberregex)", $line, $webct_options)) { $datasetname = ereg_replace('^::', '', $webct_options[1]); $datasetvalue = qformat_webct_convert_formula($webct_options[4]); switch ($webct_options[2]) { case 'MIN': $question->datasets[$datasetname]->min = $datasetvalue; break; case 'MAX': $question->datasets[$datasetname]->max = $datasetvalue; break; case 'DEC': $datasetvalue = floor($datasetvalue); // int only! $question->datasets[$datasetname]->dec = max(0, $datasetvalue); break; default: // The VAL case: $question->datasets[$datasetname]->items[$webct_options[3]] = $datasetvalue; break; } continue; } $bIsHTMLText = eregi(":H$",$line); // True if next lines are coded in HTML if (eregi("^:QUESTION",$line)) { $questiontext=""; // Start gathering next lines continue; } if (eregi("^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)",$line,$webct_options)) { /// SHORTANSWER $currentchoice=$webct_options[1]; $answertext=$webct_options[2]; // Start gathering next lines $question->fraction[$currentchoice]=($webct_options[3]/100); continue; } if (eregi("^:ANSWER([0-9]+):([0-9\.\-]+)",$line,$webct_options)) { $answertext=""; // Start gathering next lines $currentchoice=$webct_options[1]; $question->fraction[$currentchoice]=($webct_options[2]/100); continue; } if (eregi('^:FORMULA:(.*)', $line, $webct_options)) { // Answer for a CALCULATED question ++$currentchoice; $question->answers[$currentchoice]->answer = qformat_webct_convert_formula($webct_options[1]); // Default settings: $question->answers[$currentchoice]->fraction = 1.0; $question->answers[$currentchoice]->tolerance = 0.0; $question->answers[$currentchoice]->tolerancetype = 2; // nominal (units in webct) $question->answers[$currentchoice]->feedback = ''; $question->answers[$currentchoice]->correctanswerlength = 4; $datasetnames = $qtypecalculated->find_dataset_names($webct_options[1]); foreach ($datasetnames as $datasetname) { $question->datasets[$datasetname]->items = array(); } continue; } if (eregi("^:L([0-9]+)",$line,$webct_options)) { $answertext=""; // Start gathering next lines $currentchoice=$webct_options[1]; $question->fraction[$currentchoice]=1; continue; } if (eregi("^:R([0-9]+)",$line,$webct_options)) { $responsetext=""; // Start gathering next lines $currentchoice=$webct_options[1]; continue; } if (eregi("^:REASON([0-9]+):?",$line,$webct_options)) { $feedbacktext=""; // Start gathering next lines $currentchoice=$webct_options[1]; continue; } if (CALCULATED == $question->qtype && eregi('^:ANS-DEC:([1-9][0-9]*)', $line, $webct_options)) { // We can but hope that this always appear before the ANSTYPE property $question->answers[$currentchoice]->correctanswerlength = $webct_options[1]; continue; } if (CALCULATED == $question->qtype && eregi("^:TOL:($webctnumberregex)", $line, $webct_options)) { // We can but hope that this always appear before the TOL property $question->answers[$currentchoice]->tolerance = qformat_webct_convert_formula($webct_options[1]); continue; } if (CALCULATED == $question->qtype && eregi('^:TOLTYPE:percent', $line)) { // Percentage case is handled as relative in Moodle: $question->answers[$currentchoice]->tolerance /= 100; $question->answers[$currentchoice]->tolerancetype = 1; // Relative continue; } if (eregi('^:UNITS:(.+)', $line, $webct_options) and $webctunits = trim($webct_options[1])) { // This is a guess - I really do not know how different webct units are separated... $webctunits = explode(':', $webctunits); $unitrec->multiplier = 1.0; // Webct does not seem to support this foreach ($webctunits as $webctunit) { $unitrec->unit = trim($webctunit); $question->units[] = $unitrec; } continue; } if (!empty($question->units) && eregi('^:UNITREQ:(.*)', $line, $webct_options) && !$webct_options[1]) { // There are units but units are not required so add the no unit alternative // We can but hope that the UNITS property always appear before this property $unitrec->unit = ''; $unitrec->multiplier = 1.0; $question->units[] = $unitrec; continue; } if (!empty($question->units) && eregi('^:UNITCASE:', $line)) { // This could be important but I was not able to figure out how // it works so I ignore it for now continue; } if (CALCULATED == $question->qtype && eregi('^:ANSTYPE:dec', $line)) { // Houston - we have a problem // Moodle does not support this - we try something defensively by // setting the correct answer length to 5, it shoud be enough for // most cases $question->answers[$currentchoice]->correctanswerlength = 5; continue; } } if (sizeof($errors) > 0) { echo "

".get_string("errorsdetected", "quiz", sizeof($errors))."

"; unset($questions); // no questions imported } if (sizeof($warnings) > 0) { echo "

".get_string("warningsdetected", "quiz", sizeof($warnings))."

"; } return $questions; } } ?>