. /** * Web CT question importer. * * @package qformat * @subpackage 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 { 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(); $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++; $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 (sizeof($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) { // echo "
"; print_r ($question);
                        $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,$webct_options)) {
                // 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
                $ignore_rest_of_question = FALSE;
                continue;
            }

            if (preg_match("~^:TYPE:MC:N(.*)~i",$line,$webct_options)) {
                // Multiple Choice Question with several good answers
                $question = $this->defaultquestion();
                $question->feedback = array();
                $question->qtype = MULTICHOICE;
                $question->single = 0;        // Many answers allowed
                $ignore_rest_of_question = 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
                $ignore_rest_of_question = 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;
                $ignore_rest_of_question = FALSE;
                continue;
            }

            if (preg_match("~^:TYPE:M~i",$line)) {
                // Match Question
                $question = $this->defaultquestion();
                $question->qtype = MATCH;
                $question->feedback = array();
                $ignore_rest_of_question = FALSE;         // match question processing is not debugged
                continue;
            }

            if (preg_match("~^:TYPE:P~i",$line)) {
                // Paragraph Question
                $warnings[] = get_string("paragraphquestion", "qformat_webct", $nLineCounter);
                unset($question);
                $ignore_rest_of_question = TRUE;         // Question Type not handled by Moodle
                continue;
            }

            if (preg_match("~^:TYPE:~i",$line)) {
                // Unknow Question
                $warnings[] = get_string("unknowntype", "qformat_webct", $nLineCounter);
                unset($question);
                $ignore_rest_of_question = TRUE;         // Question Type not handled by Moodle
                continue;
            }

            if ($ignore_rest_of_question) {
                continue;
            }

            if (preg_match("~^:TITLE:(.*)~i",$line,$webct_options)) {
                $name = trim($webct_options[1]);
                $question->name = $this->clean_question_name($name);
                continue;
            }

            if (preg_match("~^:IMAGE:(.*)~i",$line,$webct_options)) {
                $filename = trim($webct_options[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, $webct_options)) {
                $datasetname = preg_replace('/^::/', '', $webct_options[1]);
                $datasetvalue = qformat_webct_convert_formula($webct_options[4]);
                switch ($webct_options[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[$webct_options[3]] = new stdClass();
                        $question->dataset[$datasetname]->datasetitem[$webct_options[3]]->itemnumber = $webct_options[3];
                        $question->dataset[$datasetname]->datasetitem[$webct_options[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,$webct_options)) {      /// SHORTANSWER
                $currentchoice=$webct_options[1];
                $answertext=$webct_options[2];            // Start gathering next lines
                $question->fraction[$currentchoice]=($webct_options[3]/100);
                continue;
            }

            if (preg_match("~^:ANSWER([0-9]+):([0-9\.\-]+)~i",$line,$webct_options)) {
                $answertext="";                 // Start gathering next lines
                $currentchoice=$webct_options[1];
                $question->fraction[$currentchoice]=($webct_options[2]/100);
                continue;
            }

            if (preg_match('~^:FORMULA:(.*)~i', $line, $webct_options)) {
                // Answer for a CALCULATED question
                ++$currentchoice;
                $question->answers[$currentchoice] =
                        qformat_webct_convert_formula($webct_options[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($webct_options[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,$webct_options)) {
                $answertext="";                 // Start gathering next lines
                $currentchoice=$webct_options[1];
                $question->fraction[$currentchoice]=1;
                continue;
            }

            if (preg_match("~^:R([0-9]+)~i",$line,$webct_options)) {
                $responsetext="";                // Start gathering next lines
                $currentchoice=$webct_options[1];
                continue;
            }

            if (preg_match("~^:REASON([0-9]+):?~i",$line,$webct_options)) {
                $feedbacktext="";               // Start gathering next lines
                $currentchoice=$webct_options[1];
                continue;
            }
            if (preg_match("~^:FEEDBACK([0-9]+):?~i",$line,$webct_options)) {
                $generalfeedbacktext="";               // Start gathering next lines
                $currentchoice=$webct_options[1];
                continue;
            }
            if (preg_match('~^:FEEDBACK:(.*)~i',$line,$webct_options)) {
                $generalfeedbacktext="";               // Start gathering next lines
                continue;
            }
            if (preg_match('~^:LAYOUT:(.*)~i',$line,$webct_options)) {
            //    ignore  since layout in question_multichoice  is no more used in moodle
            //    $webct_options[1] contains either vertical or horizontal ;
                continue;
            }

            if (isset($question->qtype ) && CALCULATED == $question->qtype && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webct_options)) {
                // We can but hope that this always appear before the ANSTYPE property
                $question->correctanswerlength[$currentchoice] = $webct_options[1];
                continue;
            }

            if (isset($question->qtype )&& CALCULATED == $question->qtype && preg_match("~^:TOL:($webctnumberregex)~i", $line, $webct_options)) {
                // We can but hope that this always appear before the TOL property
                $question->tolerance[$currentchoice] =
                        qformat_webct_convert_formula($webct_options[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, $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) && preg_match('~^:UNITREQ:(.*)~i', $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) && 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 (sizeof($errors) > 0) {
            echo "

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

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

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

"; } return $questions; } } ?>