"MDL-12304, fix double text"
[moodle-linuxchix.git] / question / format / webct / format.php
blob6557c9688f86250e1a81201e5cd3b904a8e6a342
1 <?php // $Id$
2 ///////////////////////////////////////////////////////////////////////////
3 // //
4 // WebCT FORMAT //
5 // //
6 ///////////////////////////////////////////////////////////////////////////
7 // //
8 // NOTICE OF COPYRIGHT //
9 // //
10 // Part of Moodle - Modular Object-Oriented Dynamic Learning Environment //
11 // http://moodle.com //
12 // //
13 // Copyright (C) 2004 ASP Consulting http://www.asp-consulting.net //
14 // //
15 // This program is free software; you can redistribute it and/or modify //
16 // it under the terms of the GNU General Public License as published by //
17 // the Free Software Foundation; either version 2 of the License, or //
18 // (at your option) any later version. //
19 // //
20 // This program is distributed in the hope that it will be useful, //
21 // but WITHOUT ANY WARRANTY; without even the implied warranty of //
22 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
23 // GNU General Public License for more details: //
24 // //
25 // http://www.gnu.org/copyleft/gpl.html //
26 // //
27 ///////////////////////////////////////////////////////////////////////////
29 // Based on format.php, included by ../../import.php
30 /**
31 * @package questionbank
32 * @subpackage importexport
35 function unhtmlentities($string){
36 $search = array ("'<script[?>]*?>.*?</script>'si", // remove javascript
37 "'<[\/\!]*?[^<?>]*?>'si", // remove HTML tags
38 "'([\r\n])[\s]+'", // remove spaces
39 "'&(quot|#34);'i", // remove HTML entites
40 "'&(amp|#38);'i",
41 "'&(lt|#60);'i",
42 "'&(gt|#62);'i",
43 "'&(nbsp|#160);'i",
44 "'&(iexcl|#161);'i",
45 "'&(cent|#162);'i",
46 "'&(pound|#163);'i",
47 "'&(copy|#169);'i",
48 "'&#(\d+);'e"); // Evaluate like PHP
49 $replace = array ("",
50 "",
51 "\\1",
52 "\"",
53 "&",
54 "<",
55 "?>",
56 " ",
57 chr(161),
58 chr(162),
59 chr(163),
60 chr(169),
61 "chr(\\1)");
62 return preg_replace ($search, $replace, $string);
67 function qformat_webct_convert_formula($formula) {
69 // Remove empty space, as it would cause problems otherwise:
70 $formula = str_replace(' ', '', $formula);
72 // Remove paranthesis after e,E and *10**:
73 while (ereg('[0-9.](e|E|\\*10\\*\\*)\\([+-]?[0-9]+\\)', $formula, $regs)) {
74 $formula = str_replace(
75 $regs[0], ereg_replace('[)(]', '', $regs[0]), $formula);
78 // Replace *10** with e where possible
79 while (ereg(
80 '(^[+-]?|[^eE][+-]|[^0-9eE+-])[0-9.]+\\*10\\*\\*[+-]?[0-9]+([^0-9.eE]|$)',
81 $formula, $regs)) {
82 $formula = str_replace(
83 $regs[0], str_replace('*10**', 'e', $regs[0]), $formula);
86 // Replace other 10** with 1e where possible
87 while (ereg('(^|[^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))) {
96 // Find $base
97 if (ereg('^(.*[^0-9.eE])?(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?|\\{[^}]*\\})$',
98 $splits[0], $regs)) {
99 // The simple cases
100 $base = $regs[2];
101 $splits[0] = $regs[1];
103 } else if (ereg('\\)$', $splits[0])) {
104 // Find the start of this parenthesis
105 $deep = 1;
106 for ($i = 1 ; $deep ; ++$i) {
107 if (!ereg('^(.*[^[:alnum:]_])?([[:alnum:]_]*([)(])([^)(]*[)(]){'.$i.'})$',
108 $splits[0], $regs)) {
109 error("Parenthesis before ** is not properly started in $splits[0]**");
111 if ('(' == $regs[3]) {
112 --$deep;
113 } else if (')' == $regs[3]) {
114 ++$deep;
115 } else {
116 error("Impossible character $regs[3] detected as parenthesis character");
119 $base = $regs[2];
120 $splits[0] = $regs[1];
122 } else {
123 error("Bad base before **: $splits[0]**");
126 // Find $exp (similar to above but a little easier)
127 if (ereg('^([+-]?(\\{[^}]\\}|([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?))(.*)',
128 $splits[1], $regs)) {
129 // The simple case
130 $exp = $regs[1];
131 $splits[1] = $regs[6];
133 } else if (ereg('^[+-]?[[:alnum:]_]*\\(', $splits[1])) {
134 // Find the end of the parenthesis
135 $deep = 1;
136 for ($i = 1 ; $deep ; ++$i) {
137 if (!ereg('^([+-]?[[:alnum:]_]*([)(][^)(]*){'.$i.'}([)(]))(.*)',
138 $splits[1], $regs)) {
139 error("Parenthesis after ** is not properly closed in **$splits[1]");
141 if (')' == $regs[3]) {
142 --$deep;
143 } else if ('(' == $regs[3]) {
144 ++$deep;
145 } else {
146 error("Impossible character $regs[3] detected as parenthesis character");
149 $exp = $regs[1];
150 $splits[1] = $regs[4];
153 // Replace it!
154 $formula = "$splits[0]pow($base,$exp)$splits[1]";
157 // Nothing more is known to need to be converted
159 return $formula;
162 class qformat_webct extends qformat_default {
164 function provide_import() {
165 return true;
168 function readquestions ($lines) {
169 global $QTYPES ;
170 // $qtypecalculated = new qformat_webct_modified_calculated_qtype();
171 $webctnumberregex =
172 '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?';
174 $questions = array();
175 $errors = array();
176 $warnings = array();
177 $webct_options = array();
179 $ignore_rest_of_question = FALSE;
181 $nLineCounter = 0;
182 $nQuestionStartLine = 0;
183 $bIsHTMLText = FALSE;
184 $lines[] = ":EOF:"; // for an easiest processing of the last line
185 // $question = $this->defaultquestion();
187 foreach ($lines as $line) {
188 $nLineCounter++;
189 $line = iconv("Windows-1252","UTF-8",$line);
190 // Processing multiples lines strings
192 if (isset($questiontext) and is_string($questiontext)) {
193 if (ereg("^:",$line)) {
194 $question->questiontext = addslashes(trim($questiontext));
195 unset($questiontext);
197 else {
198 $questiontext .= str_replace('\:', ':', $line);
199 continue;
203 if (isset($answertext) and is_string($answertext)) {
204 if (ereg("^:",$line)) {
205 $answertext = addslashes(trim($answertext));
206 $question->answer[$currentchoice] = $answertext;
207 $question->subanswers[$currentchoice] = $answertext;
208 unset($answertext);
210 else {
211 $answertext .= str_replace('\:', ':', $line);
212 continue;
216 if (isset($responsetext) and is_string($responsetext)) {
217 if (ereg("^:",$line)) {
218 $question->subquestions[$currentchoice] = addslashes(trim($responsetext));
219 unset($responsetext);
221 else {
222 $responsetext .= str_replace('\:', ':', $line);
223 continue;
227 if (isset($feedbacktext) and is_string($feedbacktext)) {
228 if (ereg("^:",$line)) {
229 $question->feedback[$currentchoice] = addslashes(trim($feedbacktext));
230 unset($feedbacktext);
232 else {
233 $feedbacktext .= str_replace('\:', ':', $line);
234 continue;
238 if (isset($generalfeedbacktext) and is_string($generalfeedbacktext)) {
239 if (ereg("^:",$line)) {
240 $question->tempgeneralfeedback= addslashes(trim($generalfeedbacktext));
241 unset($generalfeedbacktext);
243 else {
244 $generalfeedbacktext .= str_replace('\:', ':', $line);
245 continue;
249 $line = trim($line);
251 if (eregi("^:(TYPE|EOF):",$line)) {
252 // New Question or End of File
253 if (isset($question)) { // if previous question exists, complete, check and save it
255 // Setup default value of missing fields
256 if (!isset($question->name)) {
257 $question->name = $question->questiontext;
259 if (strlen($question->name) > 255) {
260 $question->name = substr($question->name,0,250)."...";
261 $warnings[] = get_string("questionnametoolong", "quiz", $nQuestionStartLine);
263 if (!isset($question->defaultgrade)) {
264 $question->defaultgrade = 1;
266 if (!isset($question->image)) {
267 $question->image = "";
270 // Perform sanity checks
271 $QuestionOK = TRUE;
272 if (strlen($question->questiontext) == 0) {
273 $warnings[] = get_string("missingquestion", "quiz", $nQuestionStartLine);
274 $QuestionOK = FALSE;
276 if (sizeof($question->answer) < 1) { // a question must have at least 1 answer
277 $errors[] = get_string("missinganswer", "quiz", $nQuestionStartLine);
278 $QuestionOK = FALSE;
280 else {
281 // Create empty feedback array
282 foreach ($question->answer as $key => $dataanswer) {
283 if(!isset( $question->feedback[$key])){
284 $question->feedback[$key] = '';
287 // this tempgeneralfeedback allows the code to work with versions from 1.6 to 1.9
288 // when question->generalfeedback is undefined, the webct feedback is added to each answer feedback
289 if (isset($question->tempgeneralfeedback)){
290 if (isset($question->generalfeedback)) {
291 $question->generalfeedback = $question->tempgeneralfeedback;
292 } else {
293 foreach ($question->answer as $key => $dataanswer) {
294 if ($question->tempgeneralfeedback !=''){
295 $question->feedback[$key] = $question->tempgeneralfeedback.'<br/>'.$question->feedback[$key];
299 unset($question->tempgeneralfeedback);
301 $maxfraction = -1;
302 $totalfraction = 0;
303 foreach($question->fraction as $fraction) {
304 if ($fraction > 0) {
305 $totalfraction += $fraction;
307 if ($fraction > $maxfraction) {
308 $maxfraction = $fraction;
311 switch ($question->qtype) {
312 case SHORTANSWER:
313 if ($maxfraction != 1) {
314 $maxfraction = $maxfraction * 100;
315 $errors[] = "'$question->name': ".get_string("wronggrade", "quiz", $nLineCounter).' '.get_string("fractionsnomax", "quiz", $maxfraction);
316 $QuestionOK = FALSE;
318 break;
320 case MULTICHOICE:
321 if ($question->single) {
322 if ($maxfraction != 1) {
323 $maxfraction = $maxfraction * 100;
324 $errors[] = "'$question->name': ".get_string("wronggrade", "quiz", $nLineCounter).' '.get_string("fractionsnomax", "quiz", $maxfraction);
325 $QuestionOK = FALSE;
327 } else {
328 $totalfraction = round($totalfraction,2);
329 if ($totalfraction != 1) {
330 $totalfraction = $totalfraction * 100;
331 $errors[] = "'$question->name': ".get_string("wronggrade", "quiz", $nLineCounter).' '.get_string("fractionsaddwrong", "quiz", $totalfraction);
332 $QuestionOK = FALSE;
335 break;
337 case CALCULATED:
338 foreach ($question->answers as $answer) {
339 if ($formulaerror =qtype_calculated_find_formula_errors($answer)) { //$QTYPES['calculated']->
340 $warnings[] = "'$question->name': ". $formulaerror;
341 $QuestionOK = FALSE;
344 foreach ($question->dataset as $dataset) {
345 $dataset->itemcount=count($dataset->datasetitem);
347 $question->import_process=TRUE ;
348 unset($question->answer); //not used in calculated question
349 break;
350 case MATCH:
351 // MDL-10680:
352 // switch subquestions and subanswers
353 foreach ($question->subquestions as $id=>$subquestion) {
354 $temp = $question->subquestions[$id];
355 $question->subquestions[$id] = $question->subanswers[$id];
356 $question->subanswers[$id] = $temp;
358 if (count($question->answer) < 3){
359 // add a dummy missing question
360 $question->name = 'Dummy question added '.$question->name ;
361 $question->answer[] = 'dummy';
362 $question->subanswers[] = 'dummy';
363 $question->subquestions[] = 'dummy';
364 $question->fraction[] = '0.0';
365 $question->feedback[] = '';
367 break;
368 default:
369 // No problemo
373 if ($QuestionOK) {
374 // echo "<pre>"; print_r ($question);
375 $questions[] = $question; // store it
376 unset($question); // and prepare a new one
377 $question = $this->defaultquestion();
380 $nQuestionStartLine = $nLineCounter;
383 // Processing Question Header
385 if (eregi("^:TYPE:MC:1(.*)",$line,$webct_options)) {
386 // Multiple Choice Question with only one good answer
387 $question = $this->defaultquestion();
388 $question->feedback = array();
389 $question->qtype = MULTICHOICE;
390 $question->single = 1; // Only one answer is allowed
391 $ignore_rest_of_question = FALSE;
392 continue;
395 if (eregi("^:TYPE:MC:N(.*)",$line,$webct_options)) {
396 // Multiple Choice Question with several good answers
397 $question = $this->defaultquestion();
398 $question->feedback = array();
399 $question->qtype = MULTICHOICE;
400 $question->single = 0; // Many answers allowed
401 $ignore_rest_of_question = FALSE;
402 continue;
405 if (eregi("^:TYPE:S",$line)) {
406 // Short Answer Question
407 $question = $this->defaultquestion();
408 $question->feedback = array();
409 $question->qtype = SHORTANSWER;
410 $question->usecase = 0; // Ignore case
411 $ignore_rest_of_question = FALSE;
412 continue;
415 if (eregi("^:TYPE:C",$line)) {
416 // Calculated Question
417 /* $warnings[] = get_string("calculatedquestion", "quiz", $nLineCounter);
418 unset($question);
419 $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle
421 $question = $this->defaultquestion();
422 $question->qtype = CALCULATED;
423 $question->answers = array(); // No problem as they go as :FORMULA: from webct
424 $question->units = array();
425 $question->dataset = array();
427 // To make us pass the end-of-question sanity checks
428 $question->answer = array('dummy');
429 $question->fraction = array('1.0');
430 $question->feedback = array();
432 $currentchoice = -1;
433 $ignore_rest_of_question = FALSE;
434 continue;
437 if (eregi("^:TYPE:M",$line)) {
438 // Match Question
439 $question = $this->defaultquestion();
440 $question->qtype = MATCH;
441 $question->feedback = array();
442 $ignore_rest_of_question = FALSE; // match question processing is not debugged
443 continue;
446 if (eregi("^:TYPE:P",$line)) {
447 // Paragraph Question
448 $warnings[] = get_string("paragraphquestion", "quiz", $nLineCounter);
449 unset($question);
450 $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle
451 continue;
454 if (eregi("^:TYPE:",$line)) {
455 // Unknow Question
456 $warnings[] = get_string("unknowntype", "quiz", $nLineCounter);
457 unset($question);
458 $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle
459 continue;
462 if ($ignore_rest_of_question) {
463 continue;
466 if (eregi("^:TITLE:(.*)",$line,$webct_options)) {
467 $name = trim($webct_options[1]);
468 if (strlen($name) > 255) {
469 $name = substr($name,0,250)."...";
470 $warnings[] = get_string("questionnametoolong", "quiz", $nLineCounter);
472 $question->name = addslashes($name);
473 continue;
476 if (eregi("^:IMAGE:(.*)",$line,$webct_options)) {
477 $filename = trim($webct_options[1]);
478 if (eregi("^http://",$filename)) {
479 $question->image = $filename;
481 continue;
484 // Need to put the parsing of calculated items here to avoid ambitiuosness:
485 // if question isn't defined yet there is nothing to do here (avoid notices)
486 if (!isset($question)) {
487 continue;
489 if (isset($question->qtype ) && CALCULATED == $question->qtype && ereg(
490 "^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?($webctnumberregex)", $line, $webct_options)) {
491 $datasetname = ereg_replace('^::', '', $webct_options[1]);
492 $datasetvalue = qformat_webct_convert_formula($webct_options[4]);
493 switch ($webct_options[2]) {
494 case 'MIN':
495 $question->dataset[$datasetname]->min = $datasetvalue;
496 break;
497 case 'MAX':
498 $question->dataset[$datasetname]->max = $datasetvalue;
499 break;
500 case 'DEC':
501 $datasetvalue = floor($datasetvalue); // int only!
502 $question->dataset[$datasetname]->length = max(0, $datasetvalue);
503 break;
504 default:
505 // The VAL case:
506 $question->dataset[$datasetname]->datasetitem[$webct_options[3]] = new stdClass();
507 $question->dataset[$datasetname]->datasetitem[$webct_options[3]]->itemnumber = $webct_options[3];
508 $question->dataset[$datasetname]->datasetitem[$webct_options[3]]->value = $datasetvalue;
509 break;
511 continue;
515 $bIsHTMLText = eregi(":H$",$line); // True if next lines are coded in HTML
516 if (eregi("^:QUESTION",$line)) {
517 $questiontext=""; // Start gathering next lines
518 continue;
521 if (eregi("^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)",$line,$webct_options)) { /// SHORTANSWER
522 $currentchoice=$webct_options[1];
523 $answertext=$webct_options[2]; // Start gathering next lines
524 $question->fraction[$currentchoice]=($webct_options[3]/100);
525 continue;
528 if (eregi("^:ANSWER([0-9]+):([0-9\.\-]+)",$line,$webct_options)) {
529 $answertext=""; // Start gathering next lines
530 $currentchoice=$webct_options[1];
531 $question->fraction[$currentchoice]=($webct_options[2]/100);
532 continue;
535 if (eregi('^:FORMULA:(.*)', $line, $webct_options)) {
536 // Answer for a CALCULATED question
537 ++$currentchoice;
538 $question->answers[$currentchoice] =
539 qformat_webct_convert_formula($webct_options[1]);
541 // Default settings:
542 $question->fraction[$currentchoice] = 1.0;
543 $question->tolerance[$currentchoice] = 0.0;
544 $question->tolerancetype[$currentchoice] = 2; // nominal (units in webct)
545 $question->feedback[$currentchoice] = '';
546 $question->correctanswerlength[$currentchoice] = 4;
548 $datasetnames = $QTYPES[CALCULATED]->find_dataset_names($webct_options[1]);
549 foreach ($datasetnames as $datasetname) {
550 $question->dataset[$datasetname] = new stdClass();
551 $question->dataset[$datasetname]->datasetitem = array();
552 $question->dataset[$datasetname]->name = $datasetname ;
553 $question->dataset[$datasetname]->distribution = 'uniform';
554 $question->dataset[$datasetname]->status ='private';
556 continue;
559 if (eregi("^:L([0-9]+)",$line,$webct_options)) {
560 $answertext=""; // Start gathering next lines
561 $currentchoice=$webct_options[1];
562 $question->fraction[$currentchoice]=1;
563 continue;
566 if (eregi("^:R([0-9]+)",$line,$webct_options)) {
567 $responsetext=""; // Start gathering next lines
568 $currentchoice=$webct_options[1];
569 continue;
572 if (eregi("^:REASON([0-9]+):?",$line,$webct_options)) {
573 $feedbacktext=""; // Start gathering next lines
574 $currentchoice=$webct_options[1];
575 continue;
577 if (eregi("^:FEEDBACK([0-9]+):?",$line,$webct_options)) {
578 $generalfeedbacktext=""; // Start gathering next lines
579 $currentchoice=$webct_options[1];
580 continue;
582 if (eregi('^:FEEDBACK:(.*)',$line,$webct_options)) {
583 $generalfeedbacktext=""; // Start gathering next lines
584 continue;
586 if (eregi('^:LAYOUT:(.*)',$line,$webct_options)) {
587 // ignore since layout in question_multichoice is no more used in moodle
588 // $webct_options[1] contains either vertical or horizontal ;
589 continue;
592 if (isset($question->qtype ) && CALCULATED == $question->qtype && eregi('^:ANS-DEC:([1-9][0-9]*)', $line, $webct_options)) {
593 // We can but hope that this always appear before the ANSTYPE property
594 $question->correctanswerlength[$currentchoice] = $webct_options[1];
595 continue;
598 if (isset($question->qtype )&& CALCULATED == $question->qtype && eregi("^:TOL:($webctnumberregex)", $line, $webct_options)) {
599 // We can but hope that this always appear before the TOL property
600 $question->tolerance[$currentchoice] =
601 qformat_webct_convert_formula($webct_options[1]);
602 continue;
605 if (isset($question->qtype )&& CALCULATED == $question->qtype && eregi('^:TOLTYPE:percent', $line)) {
606 // Percentage case is handled as relative in Moodle:
607 $question->tolerance[$currentchoice] /= 100;
608 $question->tolerancetype[$currentchoice] = 1; // Relative
609 continue;
612 if (eregi('^:UNITS:(.+)', $line, $webct_options)
613 and $webctunits = trim($webct_options[1])) {
614 // This is a guess - I really do not know how different webct units are separated...
615 $webctunits = explode(':', $webctunits);
616 $unitrec->multiplier = 1.0; // Webct does not seem to support this
617 foreach ($webctunits as $webctunit) {
618 $unitrec->unit = trim($webctunit);
619 $question->units[] = $unitrec;
621 continue;
624 if (!empty($question->units) && eregi('^:UNITREQ:(.*)', $line, $webct_options)
625 && !$webct_options[1]) {
626 // There are units but units are not required so add the no unit alternative
627 // We can but hope that the UNITS property always appear before this property
628 $unitrec->unit = '';
629 $unitrec->multiplier = 1.0;
630 $question->units[] = $unitrec;
631 continue;
634 if (!empty($question->units) && eregi('^:UNITCASE:', $line)) {
635 // This could be important but I was not able to figure out how
636 // it works so I ignore it for now
637 continue;
640 if (isset($question->qtype )&& CALCULATED == $question->qtype && eregi('^:ANSTYPE:dec', $line)) {
641 $question->correctanswerformat[$currentchoice]='1';
642 continue;
644 if (isset($question->qtype )&& CALCULATED == $question->qtype && eregi('^:ANSTYPE:sig', $line)) {
645 $question->correctanswerformat[$currentchoice]='2';
646 continue;
650 if (sizeof($errors) > 0) {
651 echo "<p>".get_string("errorsdetected", "quiz", sizeof($errors))."</p><ul>";
652 foreach($errors as $error) {
653 echo "<li>$error</li>";
655 echo "</ul>";
656 unset($questions); // no questions imported
659 if (sizeof($warnings) > 0) {
660 echo "<p>".get_string("warningsdetected", "quiz", sizeof($warnings))."</p><ul>";
661 foreach($warnings as $warning) {
662 echo "<li>$warning</li>";
664 echo "</ul>";
666 return $questions;