2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Web CT question importer.
22 * @copyright 2004 ASP Consulting http://www.asp-consulting.net
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
30 * Manipulate HTML editites in a string. Used by WebCT import.
31 * @param string $string
34 function unhtmlentities($string){
35 $search = array ("'<script[?>]*?>.*?</script>'si", // remove javascript
36 "'<[\/\!]*?[^<?>]*?>'si", // remove HTML tags
37 "'([\r\n])[\s]+'", // remove spaces
38 "'&(quot|#34);'i", // remove HTML entites
47 "'&#(\d+);'e"); // Evaluate like PHP
61 return preg_replace ($search, $replace, $string);
65 * Helper function for WebCT import.
66 * @param unknown_type $formula
68 function qformat_webct_convert_formula($formula) {
70 // Remove empty space, as it would cause problems otherwise:
71 $formula = str_replace(' ', '', $formula);
73 // Remove paranthesis after e,E and *10**:
74 while (preg_match('~[0-9.](e|E|\\*10\\*\\*)\\([+-]?[0-9]+\\)~', $formula, $regs)) {
75 $formula = str_replace(
76 $regs[0], preg_replace('/[)(]/', '', $regs[0]), $formula);
79 // Replace *10** with e where possible
80 while (preg_match('~(^[+-]?|[^eE][+-]|[^0-9eE+-])[0-9.]+\\*10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)~',
82 $formula = str_replace(
83 $regs[0], str_replace('*10**', 'e', $regs[0]), $formula);
86 // Replace other 10** with 1e where possible
87 while (preg_match('~(^|[^0-9.eE])10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)~', $formula, $regs)) {
88 $formula = str_replace(
89 $regs[0], str_replace('10**', '1e', $regs[0]), $formula);
92 // Replace all other base**exp with the PHP equivalent function pow(base,exp)
93 // (Pretty tricky to exchange an operator with a function)
94 while (2 == count($splits = explode('**', $formula, 2))) {
97 if (preg_match('~^(.*[^0-9.eE])?(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?|\\{[^}]*\\})$~',
101 $splits[0] = $regs[1];
103 } else if (preg_match('~\\)$~', $splits[0])) {
104 // Find the start of this parenthesis
106 for ($i = 1 ; $deep ; ++$i) {
107 if (!preg_match('~^(.*[^[:alnum:]_])?([[:alnum:]_]*([)(])([^)(]*[)(]){'.$i.'})$~',
108 $splits[0], $regs)) {
109 print_error("parenthesisinproperstart", 'question', '', $splits[0]);
111 if ('(' == $regs[3]) {
113 } else if (')' == $regs[3]) {
116 print_error('impossiblechar', 'question', '', $regs[3]);
120 $splits[0] = $regs[1];
123 print_error('badbase', 'question', '', $splits[0]);
126 // Find $exp (similar to above but a little easier)
127 if (preg_match('~^([+-]?(\\{[^}]\\}|([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?))(.*)~',
128 $splits[1], $regs)) {
131 $splits[1] = $regs[6];
133 } else if (preg_match('~^[+-]?[[:alnum:]_]*\\(~', $splits[1])) {
134 // Find the end of the parenthesis
136 for ($i = 1 ; $deep ; ++$i) {
137 if (!preg_match('~^([+-]?[[:alnum:]_]*([)(][^)(]*){'.$i.'}([)(]))(.*)~',
138 $splits[1], $regs)) {
139 print_error("parenthesisinproperclose", 'question', '', $splits[1]);
141 if (')' == $regs[3]) {
143 } else if ('(' == $regs[3]) {
146 print_error("impossiblechar", 'question');
150 $splits[1] = $regs[4];
154 $formula = "$splits[0]pow($base,$exp)$splits[1]";
157 // Nothing more is known to need to be converted
164 * Web CT question importer.
166 * @copyright 2004 ASP Consulting http://www.asp-consulting.net
167 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
169 class qformat_webct extends qformat_default {
171 function provide_import() {
175 protected function readquestions($lines) {
177 '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?';
179 $questions = array();
182 $webct_options = array();
184 $ignore_rest_of_question = FALSE;
187 $nQuestionStartLine = 0;
188 $bIsHTMLText = FALSE;
189 $lines[] = ":EOF:"; // for an easiest processing of the last line
190 // $question = $this->defaultquestion();
192 foreach ($lines as $line) {
194 $line = iconv("Windows-1252","UTF-8",$line);
195 // Processing multiples lines strings
197 if (isset($questiontext) and is_string($questiontext)) {
198 if (preg_match("~^:~",$line)) {
199 $question->questiontext = trim($questiontext);
200 unset($questiontext);
203 $questiontext .= str_replace('\:', ':', $line);
208 if (isset($answertext) and is_string($answertext)) {
209 if (preg_match("~^:~",$line)) {
210 $answertext = trim($answertext);
211 $question->answer[$currentchoice] = $answertext;
212 $question->subanswers[$currentchoice] = $answertext;
216 $answertext .= str_replace('\:', ':', $line);
221 if (isset($responsetext) and is_string($responsetext)) {
222 if (preg_match("~^:~",$line)) {
223 $question->subquestions[$currentchoice] = trim($responsetext);
224 unset($responsetext);
227 $responsetext .= str_replace('\:', ':', $line);
232 if (isset($feedbacktext) and is_string($feedbacktext)) {
233 if (preg_match("~^:~",$line)) {
234 $question->feedback[$currentchoice] = trim($feedbacktext);
235 unset($feedbacktext);
238 $feedbacktext .= str_replace('\:', ':', $line);
243 if (isset($generalfeedbacktext) and is_string($generalfeedbacktext)) {
244 if (preg_match("~^:~",$line)) {
245 $question->tempgeneralfeedback= trim($generalfeedbacktext);
246 unset($generalfeedbacktext);
249 $generalfeedbacktext .= str_replace('\:', ':', $line);
256 if (preg_match("~^:(TYPE|EOF):~i",$line)) {
257 // New Question or End of File
258 if (isset($question)) { // if previous question exists, complete, check and save it
260 // Setup default value of missing fields
261 if (!isset($question->name)) {
262 $question->name = $question->questiontext;
264 if (strlen($question->name) > 255) {
265 $question->name = substr($question->name,0,250)."...";
266 $warnings[] = get_string("questionnametoolong", "qformat_webct", $nQuestionStartLine);
268 if (!isset($question->defaultmark)) {
269 $question->defaultmark = 1;
271 if (!isset($question->image)) {
272 $question->image = "";
275 // Perform sanity checks
277 if (strlen($question->questiontext) == 0) {
278 $warnings[] = get_string("missingquestion", "qformat_webct", $nQuestionStartLine);
281 if (sizeof($question->answer) < 1) { // a question must have at least 1 answer
282 $errors[] = get_string("missinganswer", "qformat_webct", $nQuestionStartLine);
286 // Create empty feedback array
287 foreach ($question->answer as $key => $dataanswer) {
288 if(!isset( $question->feedback[$key])){
289 $question->feedback[$key] = '';
292 // this tempgeneralfeedback allows the code to work with versions from 1.6 to 1.9
293 // when question->generalfeedback is undefined, the webct feedback is added to each answer feedback
294 if (isset($question->tempgeneralfeedback)){
295 if (isset($question->generalfeedback)) {
296 $question->generalfeedback = $question->tempgeneralfeedback;
298 foreach ($question->answer as $key => $dataanswer) {
299 if ($question->tempgeneralfeedback !=''){
300 $question->feedback[$key] = $question->tempgeneralfeedback.'<br/>'.$question->feedback[$key];
304 unset($question->tempgeneralfeedback);
308 foreach($question->fraction as $fraction) {
310 $totalfraction += $fraction;
312 if ($fraction > $maxfraction) {
313 $maxfraction = $fraction;
316 switch ($question->qtype) {
318 if ($maxfraction != 1) {
319 $maxfraction = $maxfraction * 100;
320 $errors[] = "'$question->name': ".get_string("wronggrade", "qformat_webct", $nLineCounter).' '.get_string("fractionsnomax", "question", $maxfraction);
326 if ($question->single) {
327 if ($maxfraction != 1) {
328 $maxfraction = $maxfraction * 100;
329 $errors[] = "'$question->name': ".get_string("wronggrade", "qformat_webct", $nLineCounter).' '.get_string("fractionsnomax", "question", $maxfraction);
333 $totalfraction = round($totalfraction,2);
334 if ($totalfraction != 1) {
335 $totalfraction = $totalfraction * 100;
336 $errors[] = "'$question->name': ".get_string("wronggrade", "qformat_webct", $nLineCounter).' '.get_string("fractionsaddwrong", "question", $totalfraction);
343 foreach ($question->answers as $answer) {
344 if ($formulaerror = qtype_calculated_find_formula_errors($answer)) {
345 $warnings[] = "'$question->name': ". $formulaerror;
349 foreach ($question->dataset as $dataset) {
350 $dataset->itemcount=count($dataset->datasetitem);
352 $question->import_process=TRUE ;
353 unset($question->answer); //not used in calculated question
357 // switch subquestions and subanswers
358 foreach ($question->subquestions as $id=>$subquestion) {
359 $temp = $question->subquestions[$id];
360 $question->subquestions[$id] = $question->subanswers[$id];
361 $question->subanswers[$id] = $temp;
363 if (count($question->answer) < 3){
364 // add a dummy missing question
365 $question->name = 'Dummy question added '.$question->name ;
366 $question->answer[] = 'dummy';
367 $question->subanswers[] = 'dummy';
368 $question->subquestions[] = 'dummy';
369 $question->fraction[] = '0.0';
370 $question->feedback[] = '';
379 // echo "<pre>"; print_r ($question);
380 $questions[] = $question; // store it
381 unset($question); // and prepare a new one
382 $question = $this->defaultquestion();
385 $nQuestionStartLine = $nLineCounter;
388 // Processing Question Header
390 if (preg_match("~^:TYPE:MC:1(.*)~i",$line,$webct_options)) {
391 // Multiple Choice Question with only one good answer
392 $question = $this->defaultquestion();
393 $question->feedback = array();
394 $question->qtype = MULTICHOICE;
395 $question->single = 1; // Only one answer is allowed
396 $ignore_rest_of_question = FALSE;
400 if (preg_match("~^:TYPE:MC:N(.*)~i",$line,$webct_options)) {
401 // Multiple Choice Question with several good answers
402 $question = $this->defaultquestion();
403 $question->feedback = array();
404 $question->qtype = MULTICHOICE;
405 $question->single = 0; // Many answers allowed
406 $ignore_rest_of_question = FALSE;
410 if (preg_match("~^:TYPE:S~i",$line)) {
411 // Short Answer Question
412 $question = $this->defaultquestion();
413 $question->feedback = array();
414 $question->qtype = SHORTANSWER;
415 $question->usecase = 0; // Ignore case
416 $ignore_rest_of_question = FALSE;
420 if (preg_match("~^:TYPE:C~i",$line)) {
421 // Calculated Question
422 $question = $this->defaultquestion();
423 $question->qtype = CALCULATED;
424 $question->answers = array(); // No problem as they go as :FORMULA: from webct
425 $question->units = array();
426 $question->dataset = array();
428 // To make us pass the end-of-question sanity checks
429 $question->answer = array('dummy');
430 $question->fraction = array('1.0');
431 $question->feedback = array();
434 $ignore_rest_of_question = FALSE;
438 if (preg_match("~^:TYPE:M~i",$line)) {
440 $question = $this->defaultquestion();
441 $question->qtype = MATCH;
442 $question->feedback = array();
443 $ignore_rest_of_question = FALSE; // match question processing is not debugged
447 if (preg_match("~^:TYPE:P~i",$line)) {
448 // Paragraph Question
449 $warnings[] = get_string("paragraphquestion", "qformat_webct", $nLineCounter);
451 $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle
455 if (preg_match("~^:TYPE:~i",$line)) {
457 $warnings[] = get_string("unknowntype", "qformat_webct", $nLineCounter);
459 $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle
463 if ($ignore_rest_of_question) {
467 if (preg_match("~^:TITLE:(.*)~i",$line,$webct_options)) {
468 $name = trim($webct_options[1]);
469 if (strlen($name) > 255) {
470 $name = substr($name,0,250)."...";
471 $warnings[] = get_string("questionnametoolong", "qformat_webct", $nLineCounter);
473 $question->name = $name;
477 if (preg_match("~^:IMAGE:(.*)~i",$line,$webct_options)) {
478 $filename = trim($webct_options[1]);
479 if (preg_match("~^http://~i",$filename)) {
480 $question->image = $filename;
485 // Need to put the parsing of calculated items here to avoid ambitiuosness:
486 // if question isn't defined yet there is nothing to do here (avoid notices)
487 if (!isset($question)) {
490 if (isset($question->qtype ) && CALCULATED == $question->qtype && preg_match(
491 "~^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?($webctnumberregex)~", $line, $webct_options)) {
492 $datasetname = preg_replace('/^::/', '', $webct_options[1]);
493 $datasetvalue = qformat_webct_convert_formula($webct_options[4]);
494 switch ($webct_options[2]) {
496 $question->dataset[$datasetname]->min = $datasetvalue;
499 $question->dataset[$datasetname]->max = $datasetvalue;
502 $datasetvalue = floor($datasetvalue); // int only!
503 $question->dataset[$datasetname]->length = max(0, $datasetvalue);
507 $question->dataset[$datasetname]->datasetitem[$webct_options[3]] = new stdClass();
508 $question->dataset[$datasetname]->datasetitem[$webct_options[3]]->itemnumber = $webct_options[3];
509 $question->dataset[$datasetname]->datasetitem[$webct_options[3]]->value = $datasetvalue;
516 $bIsHTMLText = preg_match("~:H$~i",$line); // True if next lines are coded in HTML
517 if (preg_match("~^:QUESTION~i",$line)) {
518 $questiontext=""; // Start gathering next lines
522 if (preg_match("~^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)~i",$line,$webct_options)) { /// SHORTANSWER
523 $currentchoice=$webct_options[1];
524 $answertext=$webct_options[2]; // Start gathering next lines
525 $question->fraction[$currentchoice]=($webct_options[3]/100);
529 if (preg_match("~^:ANSWER([0-9]+):([0-9\.\-]+)~i",$line,$webct_options)) {
530 $answertext=""; // Start gathering next lines
531 $currentchoice=$webct_options[1];
532 $question->fraction[$currentchoice]=($webct_options[2]/100);
536 if (preg_match('~^:FORMULA:(.*)~i', $line, $webct_options)) {
537 // Answer for a CALCULATED question
539 $question->answers[$currentchoice] =
540 qformat_webct_convert_formula($webct_options[1]);
543 $question->fraction[$currentchoice] = 1.0;
544 $question->tolerance[$currentchoice] = 0.0;
545 $question->tolerancetype[$currentchoice] = 2; // nominal (units in webct)
546 $question->feedback[$currentchoice] = '';
547 $question->correctanswerlength[$currentchoice] = 4;
549 $datasetnames = question_bank::get_qtype('calculated')->
550 find_dataset_names($webct_options[1]);
551 foreach ($datasetnames as $datasetname) {
552 $question->dataset[$datasetname] = new stdClass();
553 $question->dataset[$datasetname]->datasetitem = array();
554 $question->dataset[$datasetname]->name = $datasetname ;
555 $question->dataset[$datasetname]->distribution = 'uniform';
556 $question->dataset[$datasetname]->status ='private';
561 if (preg_match("~^:L([0-9]+)~i",$line,$webct_options)) {
562 $answertext=""; // Start gathering next lines
563 $currentchoice=$webct_options[1];
564 $question->fraction[$currentchoice]=1;
568 if (preg_match("~^:R([0-9]+)~i",$line,$webct_options)) {
569 $responsetext=""; // Start gathering next lines
570 $currentchoice=$webct_options[1];
574 if (preg_match("~^:REASON([0-9]+):?~i",$line,$webct_options)) {
575 $feedbacktext=""; // Start gathering next lines
576 $currentchoice=$webct_options[1];
579 if (preg_match("~^:FEEDBACK([0-9]+):?~i",$line,$webct_options)) {
580 $generalfeedbacktext=""; // Start gathering next lines
581 $currentchoice=$webct_options[1];
584 if (preg_match('~^:FEEDBACK:(.*)~i',$line,$webct_options)) {
585 $generalfeedbacktext=""; // Start gathering next lines
588 if (preg_match('~^:LAYOUT:(.*)~i',$line,$webct_options)) {
589 // ignore since layout in question_multichoice is no more used in moodle
590 // $webct_options[1] contains either vertical or horizontal ;
594 if (isset($question->qtype ) && CALCULATED == $question->qtype && preg_match('~^:ANS-DEC:([1-9][0-9]*)~i', $line, $webct_options)) {
595 // We can but hope that this always appear before the ANSTYPE property
596 $question->correctanswerlength[$currentchoice] = $webct_options[1];
600 if (isset($question->qtype )&& CALCULATED == $question->qtype && preg_match("~^:TOL:($webctnumberregex)~i", $line, $webct_options)) {
601 // We can but hope that this always appear before the TOL property
602 $question->tolerance[$currentchoice] =
603 qformat_webct_convert_formula($webct_options[1]);
607 if (isset($question->qtype )&& CALCULATED == $question->qtype && preg_match('~^:TOLTYPE:percent~i', $line)) {
608 // Percentage case is handled as relative in Moodle:
609 $question->tolerance[$currentchoice] /= 100;
610 $question->tolerancetype[$currentchoice] = 1; // Relative
614 if (preg_match('~^:UNITS:(.+)~i', $line, $webct_options)
615 and $webctunits = trim($webct_options[1])) {
616 // This is a guess - I really do not know how different webct units are separated...
617 $webctunits = explode(':', $webctunits);
618 $unitrec->multiplier = 1.0; // Webct does not seem to support this
619 foreach ($webctunits as $webctunit) {
620 $unitrec->unit = trim($webctunit);
621 $question->units[] = $unitrec;
626 if (!empty($question->units) && preg_match('~^:UNITREQ:(.*)~i', $line, $webct_options)
627 && !$webct_options[1]) {
628 // There are units but units are not required so add the no unit alternative
629 // We can but hope that the UNITS property always appear before this property
631 $unitrec->multiplier = 1.0;
632 $question->units[] = $unitrec;
636 if (!empty($question->units) && preg_match('~^:UNITCASE:~i', $line)) {
637 // This could be important but I was not able to figure out how
638 // it works so I ignore it for now
642 if (isset($question->qtype )&& CALCULATED == $question->qtype && preg_match('~^:ANSTYPE:dec~i', $line)) {
643 $question->correctanswerformat[$currentchoice]='1';
646 if (isset($question->qtype )&& CALCULATED == $question->qtype && preg_match('~^:ANSTYPE:sig~i', $line)) {
647 $question->correctanswerformat[$currentchoice]='2';
652 if (sizeof($errors) > 0) {
653 echo "<p>".get_string("errorsdetected", "qformat_webct", sizeof($errors))."</p><ul>";
654 foreach($errors as $error) {
655 echo "<li>$error</li>";
658 unset($questions); // no questions imported
661 if (sizeof($warnings) > 0) {
662 echo "<p>".get_string("warningsdetected", "qformat_webct", sizeof($warnings))."</p><ul>";
663 foreach($warnings as $warning) {
664 echo "<li>$warning</li>";