2 ///////////////////////////////////////////////////////////////////////////
6 ///////////////////////////////////////////////////////////////////////////
8 // NOTICE OF COPYRIGHT //
10 // Part of Moodle - Modular Object-Oriented Dynamic Learning Environment //
11 // http://moodle.com //
13 // Copyright (C) 2004 ASP Consulting http://www.asp-consulting.net //
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. //
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: //
25 // http://www.gnu.org/copyleft/gpl.html //
27 ///////////////////////////////////////////////////////////////////////////
29 // Based on format.php, included by ../../import.php
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
48 "'&#(\d+);'e"); // Evaluate like PHP
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
80 '(^[+-]?|[^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 (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))) {
97 if (ereg('^(.*[^0-9.eE])?(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([eE][+-]?[0-9]+)?|\\{[^}]*\\})$',
101 $splits[0] = $regs[1];
103 } else if (ereg('\\)$', $splits[0])) {
104 // Find the start of this parenthesis
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]) {
113 } else if (')' == $regs[3]) {
116 error("Impossible character $regs[3] detected as parenthesis character");
120 $splits[0] = $regs[1];
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)) {
131 $splits[1] = $regs[6];
133 } else if (ereg('^[+-]?[[:alnum:]_]*\\(', $splits[1])) {
134 // Find the end of the parenthesis
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]) {
143 } else if ('(' == $regs[3]) {
146 error("Impossible character $regs[3] detected as parenthesis character");
150 $splits[1] = $regs[4];
154 $formula = "$splits[0]pow($base,$exp)$splits[1]";
157 // Nothing more is known to need to be converted
162 class qformat_webct
extends qformat_default
{
164 function provide_import() {
168 function readquestions ($lines) {
170 // $qtypecalculated = new qformat_webct_modified_calculated_qtype();
172 '[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)((e|E|\\*10\\*\\*)([+-]?[0-9]+|\\([+-]?[0-9]+\\)))?';
174 $questions = array();
177 $webct_options = array();
179 $ignore_rest_of_question = FALSE;
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) {
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);
198 $questiontext .= str_replace('\:', ':', $line);
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;
211 $answertext .= str_replace('\:', ':', $line);
216 if (isset($responsetext) and is_string($responsetext)) {
217 if (ereg("^:",$line)) {
218 $question->subquestions
[$currentchoice] = addslashes(trim($responsetext));
219 unset($responsetext);
222 $responsetext .= str_replace('\:', ':', $line);
227 if (isset($feedbacktext) and is_string($feedbacktext)) {
228 if (ereg("^:",$line)) {
229 $question->feedback
[$currentchoice] = addslashes(trim($feedbacktext));
230 unset($feedbacktext);
233 $feedbacktext .= str_replace('\:', ':', $line);
238 if (isset($generalfeedbacktext) and is_string($generalfeedbacktext)) {
239 if (ereg("^:",$line)) {
240 $question->tempgeneralfeedback
= addslashes(trim($generalfeedbacktext));
241 unset($generalfeedbacktext);
244 $generalfeedbacktext .= str_replace('\:', ':', $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
272 if (strlen($question->questiontext
) == 0) {
273 $warnings[] = get_string("missingquestion", "quiz", $nQuestionStartLine);
276 if (sizeof($question->answer
) < 1) { // a question must have at least 1 answer
277 $errors[] = get_string("missinganswer", "quiz", $nQuestionStartLine);
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
;
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
);
303 foreach($question->fraction
as $fraction) {
305 $totalfraction +
= $fraction;
307 if ($fraction > $maxfraction) {
308 $maxfraction = $fraction;
311 switch ($question->qtype
) {
313 if ($maxfraction != 1) {
314 $maxfraction = $maxfraction * 100;
315 $errors[] = "'$question->name': ".get_string("wronggrade", "quiz", $nLineCounter).' '.get_string("fractionsnomax", "quiz", $maxfraction);
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);
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);
338 foreach ($question->answers
as $answer) {
339 if ($formulaerror =qtype_calculated_find_formula_errors($answer)) { //$QTYPES['calculated']->
340 $warnings[] = "'$question->name': ". $formulaerror;
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
351 if (count($question->answer
) < 3){
352 // add a dummy missing question
353 $question->name
= 'Dummy question added '.$question->name
;
354 $question->answer
[] = 'dummy';
355 $question->subanswers
[] = 'dummy';
356 $question->subquestions
[] = 'dummy';
357 $question->fraction
[] = '0.0';
358 $question->feedback
[] = '';
367 // echo "<pre>"; print_r ($question);
368 $questions[] = $question; // store it
369 unset($question); // and prepare a new one
370 $question = $this->defaultquestion();
373 $nQuestionStartLine = $nLineCounter;
376 // Processing Question Header
378 if (eregi("^:TYPE:MC:1(.*)",$line,$webct_options)) {
379 // Multiple Choice Question with only one good answer
380 $question = $this->defaultquestion();
381 $question->feedback
= array();
382 $question->qtype
= MULTICHOICE
;
383 $question->single
= 1; // Only one answer is allowed
384 $ignore_rest_of_question = FALSE;
388 if (eregi("^:TYPE:MC:N(.*)",$line,$webct_options)) {
389 // Multiple Choice Question with several good answers
390 $question = $this->defaultquestion();
391 $question->feedback
= array();
392 $question->qtype
= MULTICHOICE
;
393 $question->single
= 0; // Many answers allowed
394 $ignore_rest_of_question = FALSE;
398 if (eregi("^:TYPE:S",$line)) {
399 // Short Answer Question
400 $question = $this->defaultquestion();
401 $question->feedback
= array();
402 $question->qtype
= SHORTANSWER
;
403 $question->usecase
= 0; // Ignore case
404 $ignore_rest_of_question = FALSE;
408 if (eregi("^:TYPE:C",$line)) {
409 // Calculated Question
410 /* $warnings[] = get_string("calculatedquestion", "quiz", $nLineCounter);
412 $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle
414 $question = $this->defaultquestion();
415 $question->qtype
= CALCULATED
;
416 $question->answers
= array(); // No problem as they go as :FORMULA: from webct
417 $question->units
= array();
418 $question->dataset
= array();
420 // To make us pass the end-of-question sanity checks
421 $question->answer
= array('dummy');
422 $question->fraction
= array('1.0');
423 $question->feedback
= array();
426 $ignore_rest_of_question = FALSE;
430 if (eregi("^:TYPE:M",$line)) {
432 $question = $this->defaultquestion();
433 $question->qtype
= MATCH
;
434 $question->feedback
= array();
435 $ignore_rest_of_question = FALSE; // match question processing is not debugged
439 if (eregi("^:TYPE:P",$line)) {
440 // Paragraph Question
441 $warnings[] = get_string("paragraphquestion", "quiz", $nLineCounter);
443 $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle
447 if (eregi("^:TYPE:",$line)) {
449 $warnings[] = get_string("unknowntype", "quiz", $nLineCounter);
451 $ignore_rest_of_question = TRUE; // Question Type not handled by Moodle
455 if ($ignore_rest_of_question) {
459 if (eregi("^:TITLE:(.*)",$line,$webct_options)) {
460 $name = trim($webct_options[1]);
461 if (strlen($name) > 255) {
462 $name = substr($name,0,250)."...";
463 $warnings[] = get_string("questionnametoolong", "quiz", $nLineCounter);
465 $question->name
= addslashes($name);
469 if (eregi("^:IMAGE:(.*)",$line,$webct_options)) {
470 $filename = trim($webct_options[1]);
471 if (eregi("^http://",$filename)) {
472 $question->image
= $filename;
477 // Need to put the parsing of calculated items here to avoid ambitiuosness:
478 // if question isn't defined yet there is nothing to do here (avoid notices)
479 if (!isset($question)) {
482 if (isset($question->qtype
) && CALCULATED
== $question->qtype
&& ereg(
483 "^:([[:lower:]].*|::.*)-(MIN|MAX|DEC|VAL([0-9]+))::?:?($webctnumberregex)", $line, $webct_options)) {
484 $datasetname = ereg_replace('^::', '', $webct_options[1]);
485 $datasetvalue = qformat_webct_convert_formula($webct_options[4]);
486 switch ($webct_options[2]) {
488 $question->dataset
[$datasetname]->min
= $datasetvalue;
491 $question->dataset
[$datasetname]->max
= $datasetvalue;
494 $datasetvalue = floor($datasetvalue); // int only!
495 $question->dataset
[$datasetname]->length
= max(0, $datasetvalue);
499 $question->dataset
[$datasetname]->datasetitem
[$webct_options[3]] = new stdClass();
500 $question->dataset
[$datasetname]->datasetitem
[$webct_options[3]]->itemnumber
= $webct_options[3];
501 $question->dataset
[$datasetname]->datasetitem
[$webct_options[3]]->value
= $datasetvalue;
508 $bIsHTMLText = eregi(":H$",$line); // True if next lines are coded in HTML
509 if (eregi("^:QUESTION",$line)) {
510 $questiontext=""; // Start gathering next lines
514 if (eregi("^:ANSWER([0-9]+):([^:]+):([0-9\.\-]+):(.*)",$line,$webct_options)) { /// SHORTANSWER
515 $currentchoice=$webct_options[1];
516 $answertext=$webct_options[2]; // Start gathering next lines
517 $question->fraction
[$currentchoice]=($webct_options[3]/100);
521 if (eregi("^:ANSWER([0-9]+):([0-9\.\-]+)",$line,$webct_options)) {
522 $answertext=""; // Start gathering next lines
523 $currentchoice=$webct_options[1];
524 $question->fraction
[$currentchoice]=($webct_options[2]/100);
528 if (eregi('^:FORMULA:(.*)', $line, $webct_options)) {
529 // Answer for a CALCULATED question
531 $question->answers
[$currentchoice] =
532 qformat_webct_convert_formula($webct_options[1]);
535 $question->fraction
[$currentchoice] = 1.0;
536 $question->tolerance
[$currentchoice] = 0.0;
537 $question->tolerancetype
[$currentchoice] = 2; // nominal (units in webct)
538 $question->feedback
[$currentchoice] = '';
539 $question->correctanswerlength
[$currentchoice] = 4;
541 $datasetnames = $QTYPES[CALCULATED
]->find_dataset_names($webct_options[1]);
542 foreach ($datasetnames as $datasetname) {
543 $question->dataset
[$datasetname] = new stdClass();
544 $question->dataset
[$datasetname]->datesetitem
= array();
545 $question->dataset
[$datasetname]->name
= $datasetname ;
546 $question->dataset
[$datasetname]->distribution
= 'uniform';
547 $question->dataset
[$datasetname]->status
='private';
552 if (eregi("^:L([0-9]+)",$line,$webct_options)) {
553 $answertext=""; // Start gathering next lines
554 $currentchoice=$webct_options[1];
555 $question->fraction
[$currentchoice]=1;
559 if (eregi("^:R([0-9]+)",$line,$webct_options)) {
560 $responsetext=""; // Start gathering next lines
561 $currentchoice=$webct_options[1];
565 if (eregi("^:REASON([0-9]+):?",$line,$webct_options)) {
566 $feedbacktext=""; // Start gathering next lines
567 $currentchoice=$webct_options[1];
570 if (eregi("^:FEEDBACK([0-9]+):?",$line,$webct_options)) {
571 $generalfeedbacktext=""; // Start gathering next lines
572 $currentchoice=$webct_options[1];
575 if (eregi('^:FEEDBACK:(.*)',$line,$webct_options)) {
576 $generalfeedbacktext=""; // Start gathering next lines
579 if (eregi('^:LAYOUT:(.*)',$line,$webct_options)) {
580 // ignore since layout in question_multichoice is no more used in moodle
581 // $webct_options[1] contains either vertical or horizontal ;
585 if (isset($question->qtype
) && CALCULATED
== $question->qtype
&& eregi('^:ANS-DEC:([1-9][0-9]*)', $line, $webct_options)) {
586 // We can but hope that this always appear before the ANSTYPE property
587 $question->correctanswerlength
[$currentchoice] = $webct_options[1];
591 if (isset($question->qtype
)&& CALCULATED
== $question->qtype
&& eregi("^:TOL:($webctnumberregex)", $line, $webct_options)) {
592 // We can but hope that this always appear before the TOL property
593 $question->tolerance
[$currentchoice] =
594 qformat_webct_convert_formula($webct_options[1]);
598 if (isset($question->qtype
)&& CALCULATED
== $question->qtype
&& eregi('^:TOLTYPE:percent', $line)) {
599 // Percentage case is handled as relative in Moodle:
600 $question->tolerance
[$currentchoice] /= 100;
601 $question->tolerancetype
[$currentchoice] = 1; // Relative
605 if (eregi('^:UNITS:(.+)', $line, $webct_options)
606 and $webctunits = trim($webct_options[1])) {
607 // This is a guess - I really do not know how different webct units are separated...
608 $webctunits = explode(':', $webctunits);
609 $unitrec->multiplier
= 1.0; // Webct does not seem to support this
610 foreach ($webctunits as $webctunit) {
611 $unitrec->unit
= trim($webctunit);
612 $question->units
[] = $unitrec;
617 if (!empty($question->units
) && eregi('^:UNITREQ:(.*)', $line, $webct_options)
618 && !$webct_options[1]) {
619 // There are units but units are not required so add the no unit alternative
620 // We can but hope that the UNITS property always appear before this property
622 $unitrec->multiplier
= 1.0;
623 $question->units
[] = $unitrec;
627 if (!empty($question->units
) && eregi('^:UNITCASE:', $line)) {
628 // This could be important but I was not able to figure out how
629 // it works so I ignore it for now
633 if (isset($question->qtype
)&& CALCULATED
== $question->qtype
&& eregi('^:ANSTYPE:dec', $line)) {
634 $question->correctanswerformat
[$currentchoice]='1';
637 if (isset($question->qtype
)&& CALCULATED
== $question->qtype
&& eregi('^:ANSTYPE:sig', $line)) {
638 $question->correctanswerformat
[$currentchoice]='2';
643 if (sizeof($errors) > 0) {
644 echo "<p>".get_string("errorsdetected", "quiz", sizeof($errors))."</p><ul>";
645 foreach($errors as $error) {
646 echo "<li>$error</li>";
649 unset($questions); // no questions imported
652 if (sizeof($warnings) > 0) {
653 echo "<p>".get_string("warningsdetected", "quiz", sizeof($warnings))."</p><ul>";
654 foreach($warnings as $warning) {
655 echo "<li>$warning</li>";