. /** * Web CT question importer. * * @package qformat_webct * @copyright 2004 ASP Consulting http://www.asp-consulting.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Manipulate HTML editites in a string. Used by WebCT import. * @param string $string * @return string */ function unhtmlentities($string) { $search = array ("']*?>.*?'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); } /** * Helper function for WebCT import. * @param unknown_type $formula */ 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 (preg_match('~[0-9.](e|E|\\*10\\*\\*)\\([+-]?[0-9]+\\)~', $formula, $regs)) { $formula = str_replace( $regs[0], preg_replace('/[)(]/', '', $regs[0]), $formula); } // Replace *10** with e where possible. while (preg_match('~(^[+-]?|[^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 (preg_match('~(^|[^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 (preg_match('~^(.*[^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 (preg_match('~\\)$~', $splits[0])) { // Find the start of this parenthesis. $deep = 1; for ($i = 1; $deep; ++$i) { if (!preg_match('~^(.*[^[:alnum:]_])?([[:alnum:]_]*([)(])([^)(]*[)(]){'.$i.'})$~', $splits[0], $regs)) { print_error("parenthesisinproperstart", 'question', '', $splits[0]); } if ('(' == $regs[3]) { --$deep; } else if (')' == $regs[3]) { ++$deep; } else { print_error('impossiblechar', 'question', '', $regs[3]); } } $base = $regs[2]; $splits[0] = $regs[1]; } else { print_error('badbase', 'question', '', $splits[0]); } // Find $exp (similar to above but a little easier). if (preg_match('~^([+-]?(\\{[^}]\\}|([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?))(.*)~', $splits[1], $regs)) { // The simple case. $exp = $regs[1]; $splits[1] = $regs[6]; } else if (preg_match('~^[+-]?[[:alnum:]_]*\\(~', $splits[1])) { // Find the end of the parenthesis. $deep = 1; for ($i = 1; $deep; ++$i) { if (!preg_match('~^([+-]?[[:alnum:]_]*([)(][^)(]*){'.$i.'}([)(]))(.*)~', $splits[1], $regs)) { print_error("parenthesisinproperclose", 'question', '', $splits[1]); } if (')' == $regs[3]) { --$deep; } else if ('(' == $regs[3]) { ++$deep; } else { print_error("impossiblechar", 'question'); } } $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; } /** * Web CT question importer. * * @copyright 2004 ASP Consulting http://www.asp-consulting.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class qformat_webct extends qformat_default { public function provide_import() { return true; } protected function readquestions($lines) { $webctnumberregex = '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?'; $questions = array(); $errors = array(); $warnings = array(); $webctoptions = array(); $ignorerestofquestion = false; $nlinecounter = 0; $nquestionstartline = 0; $bishtmltext = false; $lines[] = ":EOF:"; // For an easiest processing of the last line. // We don't call defaultquestion() here, it will be called later. foreach ($lines as $line) { $nlinecounter++; $line = iconv("Windows-1252", "UTF-8", $line); // Processing multiples lines strings. if (isset($questiontext) and is_string($questiontext)) { if (preg_match("~^:~", $line)) { $question->questiontext = trim($questiontext); unset($questiontext); } else { $questiontext .= str_replace('\:', ':', $line); continue; } } if (isset($answertext) and is_string($answertext)) { if (preg_match("~^:~", $line)) { $answertext = 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 (preg_match("~^:~", $line)) { $question->subquestions[$currentchoice] = trim($responsetext); unset($responsetext); } else { $responsetext .= str_replace('\:', ':', $line); continue; } } if (isset($feedbacktext) and is_string($feedbacktext)) { if (preg_match("~^:~", $line)) { $question->feedback[$currentchoice] = trim($feedbacktext); unset($feedbacktext); } else { $feedbacktext .= str_replace('\:', ':', $line); continue; } } if (isset($generalfeedbacktext) and is_string($generalfeedbacktext)) { if (preg_match("~^:~", $line)) { $question->tempgeneralfeedback= trim($generalfeedbacktext); unset($generalfeedbacktext); } else { $generalfeedbacktext .= str_replace('\:', ':', $line); continue; } } $line = trim($line); if (preg_match("~^:(TYPE|EOF):~i", $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 = $this->create_default_question_name( $question->questiontext, get_string('questionname', 'question')); } if (!isset($question->defaultmark)) { $question->defaultmark = 1; } if (!isset($question->image)) { $question->image = ""; } // Perform sanity checks. $questionok = true; if (strlen($question->questiontext) == 0) { $warnings[] = get_string('missingquestion', 'qformat_webct', $nquestionstartline); $questionok = false; } if (count($question->answer) < 1) { // A question must have at least 1 answer. $errors[] = get_string('missinganswer', 'qformat_webct', $nquestionstartline); $questionok = false; } else { // Create empty feedback array. foreach ($question->answer as $key => $dataanswer) { if (!isset($question->feedback[$key])) { $question->feedback[$key] = ''; } } // This tempgeneralfeedback allows the code to work with versions from 1.6 to 1.9. // When question->generalfeedback is undefined, the webct feedback is added to each answer feedback. if (isset($question->tempgeneralfeedback)) { if (isset($question->generalfeedback)) { $question->generalfeedback = $question->tempgeneralfeedback; } else { foreach ($question->answer as $key => $dataanswer) { if ($question->tempgeneralfeedback !='') { $question->feedback[$key] = $question->tempgeneralfeedback .'
'.$question->feedback[$key]; } } } unset($question->tempgeneralfeedback); } $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', 'qformat_webct', $nlinecounter) .' '.get_string('fractionsnomax', 'question', $maxfraction); $questionok = false; } break; case 'multichoice': if ($question->single) { if ($maxfraction != 1) { $maxfraction = $maxfraction * 100; $errors[] = "'$question->name': ".get_string('wronggrade', 'qformat_webct', $nlinecounter) .' '.get_string('fractionsnomax', 'question', $maxfraction); $questionok = false; } } else { $totalfraction = round($totalfraction, 2); if ($totalfraction != 1) { $totalfraction = $totalfraction * 100; $errors[] = "'$question->name': ".get_string('wronggrade', 'qformat_webct', $nlinecounter) .' '.get_string('fractionsaddwrong', 'question', $totalfraction); $questionok = false; } } break; case 'calculated': foreach ($question->answers as $answer) { if ($formulaerror = qtype_calculated_find_formula_errors($answer)) { $warnings[] = "'$question->name': ". $formulaerror; $questionok = false; } } foreach ($question->dataset as $dataset) { $dataset->itemcount=count($dataset->datasetitem); } $question->import_process = true; unset($question->answer); // Not used in calculated question. break; case 'match': // MDL-10680: // Switch subquestions and subanswers. foreach ($question->subquestions as $id => $subquestion) { $temp = $question->subquestions[$id]; $question->subquestions[$id] = $question->subanswers[$id]; $question->subanswers[$id] = $temp; } if (count($question->answer) < 3) { // Add a dummy missing question. $question->name = 'Dummy question added '.$question->name; $question->answer[] = 'dummy'; $question->subanswers[] = 'dummy'; $question->subquestions[] = 'dummy'; $question->fraction[] = '0.0'; $question->feedback[] = ''; } break; default: // No problemo. } } if ($questionok) { $questions[] = $question; // Store it. unset($question); // And prepare a new one. $question = $this->defaultquestion(); } } $nquestionstartline = $nlinecounter; } // Processing Question Header. if (preg_match("~^:TYPE:MC:1(.*)~i", $line, $webctoptions)) { // Multiple Choice Question with only one good answer. $question = $this->defaultquestion(); $question->feedback = array(); $question->qtype = 'multichoice'; $question->single = 1; // Only one answer is allowed. $ignorerestofquestion = false; continue; } if (preg_match("~^:TYPE:MC:N(.*)~i", $line, $webctoptions)) { // Multiple Choice Question with several good answers. $question = $this->defaultquestion(); $question->feedback = array(); $question->qtype = 'multichoice'; $question->single = 0; // Many answers allowed. $ignorerestofquestion = false; continue; } if (preg_match("~^:TYPE:S~i", $line)) { // Short Answer Question. $question = $this->defaultquestion(); $question->feedback = array(); $question->qtype = 'shortanswer'; $question->usecase = 0; // Ignore case. $ignorerestofquestion = false; continue; } if (preg_match("~^:TYPE:C~i", $line)) { // Calculated Question. $question = $this->defaultquestion(); $question->qtype = 'calculated'; $question->answers = array(); // No problem as they go as :FORMULA: from webct. $question->units = array(); $question->dataset = array(); // To make us pass the end-of-question sanity checks. $question->answer = array('dummy'); $question->fraction = array('1.0'); $question->feedback = array(); $currentchoice = -1; $ignorerestofquestion = false; continue; } if (preg_match("~^:TYPE:M~i", $line)) { // Match Question. $question = $this->defaultquestion(); $question->qtype = 'match'; $question->feedback = array(); $ignorerestofquestion = false; // Match question processing is not debugged. continue; } if (preg_match("~^:TYPE:P~i", $line)) { // Paragraph question (essay). $warnings[] = get_string("paragraphquestion", "qformat_webct", $nlinecounter); unset($question); $ignorerestofquestion = true; // Question Type not handled by Moodle. continue; } if (preg_match("~^:TYPE:~i", $line)) { // Unknow question type. $warnings[] = get_string('unknowntype', 'qformat_webct', $nlinecounter); unset($question); $ignorerestofquestion = true; // Question Type not handled by Moodle. continue; } if ($ignorerestofquestion) { continue; } if (preg_match("~^:TITLE:(.*)~i", $line, $webctoptions)) { $name = trim($webctoptions[1]); $question->name = $this->clean_question_name($name); continue; } if (preg_match("~^:IMAGE:(.*)~i", $line, $webctoptions)) { $filename = trim($webctoptions[1]); if (preg_match("~^http://~i", $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 (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match( "~^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?($webctnumberregex)~", $line, $webctoptions)) { $datasetname = preg_replace('/^::/', '', $webctoptions[1]); $datasetvalue = qformat_webct_convert_formula($webctoptions[4]); switch ($webctoptions[2]) { case 'MIN': $question->dataset[$datasetname]->min = $datasetvalue; break; case 'MAX': $question->dataset[$datasetname]->max = $datasetvalue; break; case 'DEC': $datasetvalue = floor($datasetvalue); // Int only! $question->dataset[$datasetname]->length = max(0, $datasetvalue); break; default: // The VAL case. $question->dataset[$datasetname]->datasetitem[$webctoptions[3]] = new stdClass(); $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->itemnumber = $webctoptions[3]; $question->dataset[$datasetname]->datasetitem[$webctoptions[3]]->value = $datasetvalue; break; } continue; } $bishtmltext = preg_match("~:H$~i", $line); // True if next lines are coded in HTML. if (preg_match("~^:QUESTION~i", $line)) { $questiontext=''; // Start gathering next lines. continue; } if (preg_match("~^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)~i", $line, $webctoptions)) { // Shortanswer. $currentchoice=$webctoptions[1]; $answertext=$webctoptions[2]; // Start gathering next lines. $question->fraction[$currentchoice]=($webctoptions[3]/100); continue; } if (preg_match("~^:ANSWER([0-9]+):([0-9\.\-]+)~i", $line, $webctoptions)) { $answertext=''; // Start gathering next lines. $currentchoice=$webctoptions[1]; $question->fraction[$currentchoice]=($webctoptions[2]/100); continue; } if (preg_match('~^:FORMULA:(.*)~i', $line, $webctoptions)) { // Answer for a calculated question. ++$currentchoice; $question->answers[$currentchoice] = qformat_webct_convert_formula($webctoptions[1]); // Default settings. $question->fraction[$currentchoice] = 1.0; $question->tolerance[$currentchoice] = 0.0; $question->tolerancetype[$currentchoice] = 2; // Nominal (units in webct). $question->feedback[$currentchoice] = ''; $question->correctanswerlength[$currentchoice] = 4; $datasetnames = question_bank::get_qtype('calculated')->find_dataset_names($webctoptions[1]); foreach ($datasetnames as $datasetname) { $question->dataset[$datasetname] = new stdClass(); $question->dataset[$datasetname]->datasetitem = array(); $question->dataset[$datasetname]->name = $datasetname; $question->dataset[$datasetname]->distribution = 'uniform'; $question->dataset[$datasetname]->status = 'private'; } continue; } if (preg_match("~^:L([0-9]+)~i", $line, $webctoptions)) { $answertext=''; // Start gathering next lines. $currentchoice=$webctoptions[1]; $question->fraction[$currentchoice]=1; continue; } if (preg_match("~^:R([0-9]+)~i", $line, $webctoptions)) { $responsetext=''; // Start gathering next lines. $currentchoice=$webctoptions[1]; continue; } if (preg_match("~^:REASON([0-9]+):?~i", $line, $webctoptions)) { $feedbacktext=''; // Start gathering next lines. $currentchoice=$webctoptions[1]; continue; } if (preg_match("~^:FEEDBACK([0-9]+):?~i", $line, $webctoptions)) { $generalfeedbacktext=''; // Start gathering next lines. $currentchoice=$webctoptions[1]; continue; } if (preg_match('~^:FEEDBACK:(.*)~i', $line, $webctoptions)) { $generalfeedbacktext=''; // Start gathering next lines. continue; } if (preg_match('~^:LAYOUT:(.*)~i', $line, $webctoptions)) { // Ignore since layout in question_multichoice is no more used in Moodle. // $webctoptions[1] contains either vertical or horizontal. continue; } if (isset($question->qtype ) && 'calculated' == $question->qtype && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webctoptions)) { // We can but hope that this always appear before the ANSTYPE property. $question->correctanswerlength[$currentchoice] = $webctoptions[1]; continue; } if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match("~^:TOL:($webctnumberregex)~i", $line, $webctoptions)) { // We can but hope that this always appear before the TOL property. $question->tolerance[$currentchoice] = qformat_webct_convert_formula($webctoptions[1]); continue; } if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:TOLTYPE:percent~i', $line)) { // Percentage case is handled as relative in Moodle. $question->tolerance[$currentchoice] /= 100; $question->tolerancetype[$currentchoice] = 1; // Relative. continue; } if (preg_match('~^:UNITS:(.+)~i', $line, $webctoptions) and $webctunits = trim($webctoptions[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) && preg_match('~^:UNITREQ:(.*)~i', $line, $webctoptions) && !$webctoptions[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) && preg_match('~^:UNITCASE:~i', $line)) { // This could be important but I was not able to figure out how // it works so I ignore it for now. continue; } if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:dec~i', $line)) { $question->correctanswerformat[$currentchoice]='1'; continue; } if (isset($question->qtype )&& 'calculated' == $question->qtype && preg_match('~^:ANSTYPE:sig~i', $line)) { $question->correctanswerformat[$currentchoice]='2'; continue; } } if (count($errors) > 0) { echo '

'.get_string('errorsdetected', 'qformat_webct', count($errors)).'

'; unset($questions); // No questions imported. } if (count($warnings) > 0) { echo '

'.get_string('warningsdetected', 'qformat_webct', count($warnings)).'

'; } return $questions; } }