PageRenderTime 26ms CodeModel.GetById 2ms app.highlight 19ms RepoModel.GetById 0ms app.codeStats 1ms

/question/format/hotpot/format.php

https://github.com/borrown/moodle
PHP | 612 lines | 471 code | 62 blank | 79 comment | 103 complexity | be6e51e1438508d80212aa3dcea533cc MD5 | raw file
  1<?PHP // $Id$
  2////////////////////////////////////////////////////////////////////////////
  3/// Hotpotatoes 5.0 and 6.0 Format
  4///
  5/// This Moodle class provides all functions necessary to import
  6///                                      (export is not implemented ... yet)
  7///
  8////////////////////////////////////////////////////////////////////////////
  9
 10// Based on default.php, included by ../import.php
 11/**
 12 * @package questionbank
 13 * @subpackage importexport
 14 */
 15require_once($CFG->dirroot . '/mod/hotpot/lib.php');
 16
 17class qformat_hotpot extends qformat_default {
 18
 19    function provide_import() {
 20        return true;
 21    }
 22
 23    function readquestions ($lines) {
 24        /// Parses an array of lines into an array of questions,
 25        /// where each item is a question object as defined by
 26        /// readquestion().
 27
 28        // set courseid and baseurl
 29        global $CFG, $COURSE, $course;
 30        switch (true) {
 31            case isset($this->course->id):
 32                // import to quiz module
 33                $courseid = $this->course->id;
 34                break;
 35            case isset($course->id):
 36                // import to lesson module
 37                $courseid = $course->id;
 38                break;
 39            case isset($COURSE->id):
 40                // last resort
 41                $courseid = $COURSE->id;
 42                break;
 43            default:
 44                // shouldn't happen !!
 45                $courseid = 0; 
 46        }
 47        require_once($CFG->libdir.'/filelib.php');
 48        $baseurl = get_file_url($courseid).'/';
 49
 50        // get import file name
 51        global $params;
 52        if (! empty($this->realfilename)) {
 53            $filename = $this->realfilename;
 54        } else if (isset($params) && !empty($params->choosefile)) {
 55            // course file (Moodle >=1.6+)
 56            $filename = $params->choosefile;
 57        } else {
 58            // uploaded file (all Moodles)
 59            $filename = basename($_FILES['newfile']['tmp_name']);
 60        }
 61
 62        // get hotpot file source
 63        $source = implode($lines, " ");
 64        $source = hotpot_convert_relative_urls($source, $baseurl, $filename);
 65
 66        // create xml tree for this hotpot
 67        $xml = new hotpot_xml_tree($source);
 68
 69        // determine the quiz type
 70        $xml->quiztype = '';
 71        $keys = array_keys($xml->xml);
 72        foreach ($keys as $key) {
 73            if (preg_match('/^(hotpot|textoys)-(\w+)-file$/i', $key, $matches)) {
 74                $xml->quiztype = strtolower($matches[2]);
 75                $xml->xml_root = "['$key']['#']";
 76                break;
 77            }
 78        }
 79
 80        // convert xml to questions array
 81        $questions = array();
 82        switch ($xml->quiztype) {
 83            case 'jcloze':
 84                $this->process_jcloze($xml, $questions);
 85                break;
 86            case 'jcross':
 87                $this->process_jcross($xml, $questions);
 88                break;
 89            case 'jmatch':
 90                $this->process_jmatch($xml, $questions);
 91                break;
 92            case 'jmix':
 93                $this->process_jmix($xml, $questions);
 94                break;
 95            case 'jbc':
 96            case 'jquiz':
 97                $this->process_jquiz($xml, $questions);
 98                break;
 99            default:
100                if (empty($xml->quiztype)) {
101                    notice("Input file not recognized as a Hot Potatoes XML file");
102                } else {
103                    notice("Unknown quiz type '$xml->quiztype'");
104                }
105        } // end switch
106
107        if (count($questions)) {
108            return $questions;
109        } else {
110            if (method_exists($this, 'error')) { // Moodle >= 1.8
111                $this->error(get_string('giftnovalidquestion', 'quiz'));
112            }
113            return false;
114        }
115    }
116
117    function process_jcloze(&$xml, &$questions) {
118        // define default grade (per cloze gap)
119        $defaultgrade = 1;
120        $gap_count = 0;
121
122        // detect old Moodles (1.4 and earlier)
123        global $CFG, $db;
124        $moodle_14 = false;
125        if ($columns = $db->MetaColumns("{$CFG->prefix}question_multianswer")) {
126            foreach ($columns as $column) {
127                if ($column->name=='answers' || $column->name=='positionkey' || $column->name=='answertype' || $column->name=='norm') {
128                    $moodle_14 = true;
129                }
130            }
131        }
132
133        // xml tags for the start of the gap-fill exercise
134        $tags = 'data,gap-fill';
135
136        $x = 0;
137        while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
138            // there is usually only one exercise in a file
139
140            if (method_exists($this, 'defaultquestion')) {
141                $question = $this->defaultquestion();
142            } else {
143                $question = new stdClass();
144                $question->usecase = 0; // Ignore case
145                $question->image = "";  // No images with this format
146            }
147            $question->qtype = MULTIANSWER;
148
149            $question->name = $this->hotpot_get_title($xml, $x);
150            $question->questiontext = $this->hotpot_get_reading($xml);
151
152            // setup answer arrays
153            if ($moodle_14) {
154                $question->answers = array();
155            } else {
156                global $COURSE; // initialized in questions/import.php
157                $question->course = $COURSE->id;
158                $question->options = new stdClass();
159                $question->options->questions = array(); // one for each gap
160            }
161
162            $q = 0;
163            while ($text = $xml->xml_value($tags, $exercise."[$q]")) {
164                // add next bit of text
165                $question->questiontext .= $this->hotpot_prepare_str($text);
166
167                // check for a gap
168                $question_record = $exercise."['question-record'][$q]['#']";
169                if ($xml->xml_value($tags, $question_record)) {
170
171                    // add gap
172                    $gap_count ++;
173                    $positionkey = $q+1;
174                    $question->questiontext .= '{#'.$positionkey.'}';
175        
176                    // initialize answer settings
177                    if ($moodle_14) {
178                        $question->answers[$q]->positionkey = $positionkey;
179                        $question->answers[$q]->answertype = SHORTANSWER;
180                        $question->answers[$q]->norm = $defaultgrade;
181                        $question->answers[$q]->alternatives = array();
182                    } else {
183                        $wrapped = new stdClass();
184                        $wrapped->qtype = SHORTANSWER;
185                        $wrapped->usecase = 0;
186                        $wrapped->defaultgrade = $defaultgrade;
187                        $wrapped->questiontextformat = 0;
188                        $wrapped->answer = array();
189                        $wrapped->fraction = array();
190                        $wrapped->feedback = array();
191                        $answers = array();
192                    }
193        
194                    // add answers
195                    $a = 0;
196                    while (($answer=$question_record."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
197                        $text = $this->hotpot_prepare_str($xml->xml_value($tags,  $answer."['text'][0]['#']"));
198                        $correct = $xml->xml_value($tags,  $answer."['correct'][0]['#']");
199                        $feedback = $this->hotpot_prepare_str($xml->xml_value($tags,  $answer."['feedback'][0]['#']"));
200                        if (strlen($text)) {
201                            // set score (0=0%, 1=100%)
202                            $fraction = empty($correct) ? 0 : 1;
203                            // store answer
204                            if ($moodle_14) {
205                                $question->answers[$q]->alternatives[$a] = new stdClass();
206                                $question->answers[$q]->alternatives[$a]->answer = $text;
207                                $question->answers[$q]->alternatives[$a]->fraction = $fraction;
208                                $question->answers[$q]->alternatives[$a]->feedback = $feedback;
209                            } else {
210                                $wrapped->answer[] = $text;
211                                $wrapped->fraction[] = $fraction;
212                                $wrapped->feedback[] = $feedback;
213                                $answers[] = (empty($fraction) ? '' : '=').$text.(empty($feedback) ? '' : ('#'.$feedback));
214                            }
215                        }
216                        $a++;
217                    }
218                    // compile answers into question text, if necessary
219                    if ($moodle_14) {
220                        // do nothing
221                    } else {
222                        $wrapped->questiontext = '{'.$defaultgrade.':SHORTANSWER:'.implode('~', $answers).'}';
223                        $question->options->questions[] = $wrapped;
224                    }
225                } // end if gap
226                $q++;
227            } // end while $text
228
229            if ($q) {
230                // define total grade for this exercise
231                $question->defaultgrade = $gap_count * $defaultgrade;
232
233                // add this cloze as a single question object
234                $questions[] = $question;
235            } else {
236                // no gaps found in this text so don't add this question
237                // import will fail and error message will be displayed:
238            }
239
240            $x++;
241        } // end while $exercise
242    }
243
244    function process_jcross(&$xml, &$questions) {
245        // xml tags to the start of the crossword exercise clue items
246        $tags = 'data,crossword,clues,item';
247
248        $x = 0;
249        while (($item = "[$x]['#']") && $xml->xml_value($tags, $item)) {
250
251            $text = $xml->xml_value($tags, $item."['def'][0]['#']");
252            $answer = $xml->xml_value($tags, $item."['word'][0]['#']");
253
254            if ($text && $answer) {
255                if (method_exists($this, 'defaultquestion')) {
256                    $question = $this->defaultquestion();
257                } else {
258                    $question = new stdClass();
259                    $question->usecase = 0; // Ignore case
260                    $question->image = "";  // No images with this format
261                }
262                $question->qtype = SHORTANSWER;
263                $question->name = $this->hotpot_get_title($xml, $x, true);
264
265                $question->questiontext = $this->hotpot_prepare_str($text);
266                $question->answer = array($this->hotpot_prepare_str($answer));
267                $question->fraction = array(1);
268                $question->feedback = array('');
269
270                $questions[] = $question;
271            }
272            $x++;
273        }
274    }
275
276    function process_jmatch(&$xml, &$questions) {
277        // define default grade (per matched pair)
278        $defaultgrade = 1;
279        $match_count = 0;
280
281        // xml tags to the start of the matching exercise
282        $tags = 'data,matching-exercise';
283
284        $x = 0;
285        while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
286            // there is usually only one exercise in a file
287
288            if (method_exists($this, 'defaultquestion')) {
289                $question = $this->defaultquestion();
290            } else {
291                $question = new stdClass();
292                $question->usecase = 0; // Ignore case
293                $question->image = "";  // No images with this format
294            }
295            $question->qtype = MATCH;
296            $question->name = $this->hotpot_get_title($xml, $x);
297
298            $question->questiontext = $this->hotpot_get_reading($xml);
299            $question->questiontext .= $this->hotpot_get_instructions($xml);
300
301            $question->subquestions = array();
302            $question->subanswers = array();
303            $p = 0;
304            while (($pair = $exercise."['pair'][$p]['#']") && $xml->xml_value($tags, $pair)) {
305                $left = $xml->xml_value($tags, $pair."['left-item'][0]['#']['text'][0]['#']");
306                $right = $xml->xml_value($tags, $pair."['right-item'][0]['#']['text'][0]['#']");
307                if ($left && $right) {
308                    $match_count++;
309                    $question->subquestions[$p] = $this->hotpot_prepare_str($left);
310                    $question->subanswers[$p] = $this->hotpot_prepare_str($right);
311                }
312                $p++;
313            }
314            $question->defaultgrade = $match_count * $defaultgrade;
315            $questions[] = $question;
316            $x++;
317        }
318    }
319
320    function process_jmix(&$xml, &$questions) {
321        // define default grade (per segment)
322        $defaultgrade = 1;
323        $segment_count = 0;
324
325        // xml tags to the start of the jumbled order exercise
326        $tags = 'data,jumbled-order-exercise';
327
328        $x = 0;
329        while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
330            // there is usually only one exercise in a file
331
332            if (method_exists($this, 'defaultquestion')) {
333                $question = $this->defaultquestion();
334            } else {
335                $question = new stdClass();
336                $question->usecase = 0; // Ignore case
337                $question->image = "";  // No images with this format
338            }
339            $question->qtype = SHORTANSWER;
340            $question->name = $this->hotpot_get_title($xml, $x);
341
342            $question->answer = array();
343            $question->fraction = array();
344            $question->feedback = array();
345
346            $i = 0;
347            $segments = array();
348            while ($segment = $xml->xml_value($tags, $exercise."['main-order'][0]['#']['segment'][$i]['#']")) {
349                $segments[] = $this->hotpot_prepare_str($segment);
350                $segment_count++;
351                $i++;
352            }
353            $answer = implode(' ', $segments);
354
355            $this->hotpot_seed_RNG();
356            shuffle($segments);
357
358            $question->questiontext = $this->hotpot_get_reading($xml);
359            $question->questiontext .= $this->hotpot_get_instructions($xml);
360            $question->questiontext .= ' &nbsp; <NOBR><B>[ &nbsp; '.implode(' &nbsp; ', $segments).' &nbsp; ]</B></NOBR>';
361
362            $a = 0;
363            while (!empty($answer)) {
364                $question->answer[$a] = $answer;
365                $question->fraction[$a] = 1;
366                $question->feedback[$a] = '';
367                $answer = $this->hotpot_prepare_str($xml->xml_value($tags, $exercise."['alternate'][$a]['#']"));
368                $a++;
369            }
370            $question->defaultgrade = $segment_count * $defaultgrade;
371            $questions[] = $question;
372            $x++;
373        }
374    }
375    function process_jquiz(&$xml, &$questions) {
376        // define default grade (per question)
377        $defaultgrade = 1;
378
379        // xml tags to the start of the questions
380        $tags = 'data,questions';
381
382        $x = 0;
383        while (($exercise = "[$x]['#']") && $xml->xml_value($tags, $exercise)) {
384            // there is usually only one 'questions' object in a single exercise
385
386            $q = 0;
387            while (($question_record = $exercise."['question-record'][$q]['#']") && $xml->xml_value($tags, $question_record)) {
388
389                if (method_exists($this, 'defaultquestion')) {
390                    $question = $this->defaultquestion();
391                } else {
392                    $question = new stdClass();
393                    $question->usecase = 0; // Ignore case
394                    $question->image = "";  // No images with this format
395                }
396                $question->defaultgrade = $defaultgrade;
397                $question->name = $this->hotpot_get_title($xml, $q, true);
398
399                $text = $xml->xml_value($tags, $question_record."['question'][0]['#']");
400                $question->questiontext = $this->hotpot_prepare_str($text);
401
402                if ($xml->xml_value($tags, $question_record."['answers']")) {
403                    // HP6 JQuiz
404                    $answers = $question_record."['answers'][0]['#']";
405                } else {
406                    // HP5 JBC or JQuiz
407                    $answers = $question_record;
408                }
409                if($xml->xml_value($tags, $question_record."['question-type']")) {
410                    // HP6 JQuiz
411                    $type = $xml->xml_value($tags, $question_record."['question-type'][0]['#']");
412                    //  1 : multiple choice
413                    //  2 : short-answer
414                    //  3 : hybrid
415                    //  4 : multiple select
416                } else {
417                    // HP5
418                    switch ($xml->quiztype) {
419                        case 'jbc':
420                            $must_select_all = $xml->xml_value($tags, $question_record."['must-select-all'][0]['#']");
421                            if (empty($must_select_all)) {
422                                $type = 1; // multichoice
423                            } else {
424                                $type = 4; // multiselect
425                            }
426                            break;
427                        case 'jquiz':
428                            $type = 2; // shortanswer
429                            break;
430                        default:
431                            $type = 0; // unknown
432                    }
433                }
434                $question->qtype = ($type==2 ? SHORTANSWER : MULTICHOICE);
435                $question->single = ($type==4 ? 0 : 1);
436
437                // workaround required to calculate scores for multiple select answers
438                $no_of_correct_answers = 0;
439                if ($type==4) {
440                    $a = 0;
441                    while (($answer = $answers."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
442                        $correct = $xml->xml_value($tags, $answer."['correct'][0]['#']");
443                        if (empty($correct)) {
444                            // do nothing
445                        } else {
446                            $no_of_correct_answers++;
447                        }
448                        $a++;
449                    }
450                }
451                $a = 0;
452                $question->answer = array();
453                $question->fraction = array();
454                $question->feedback = array();
455                $aa = 0;
456                $correct_answers = array();
457                $correct_answers_all_zero = true;
458                while (($answer = $answers."['answer'][$a]['#']") && $xml->xml_value($tags, $answer)) {
459                    $correct = $xml->xml_value($tags, $answer."['correct'][0]['#']");
460                    if (empty($correct)) {
461                        $fraction = 0;
462                    } else if ($type==4) { // multiple select
463                        // strange behavior if the $fraction isn't exact to 5 decimal places
464                        $fraction = round(1/$no_of_correct_answers, 5);
465                    } else {
466                        if ($xml->xml_value($tags, $answer."['percent-correct']")) {
467                            // HP6 JQuiz
468                            $percent = $xml->xml_value($tags, $answer."['percent-correct'][0]['#']");
469                            $fraction = $percent/100;
470                        } else {
471                            // HP5 JBC or JQuiz
472                            $fraction = 1;
473                        }
474                    }
475                    $answertext = $this->hotpot_prepare_str($xml->xml_value($tags, $answer."['text'][0]['#']"));
476                    if ($answertext!='') {
477                        $question->answer[$aa] = $answertext;
478                        $question->fraction[$aa] = $fraction;
479                        $question->feedback[$aa] = $this->hotpot_prepare_str($xml->xml_value($tags, $answer."['feedback'][0]['#']"));
480                        if ($correct) {
481                            if ($fraction) {
482                                $correct_answers_all_zero = false;
483                            }
484                            $correct_answers[] = $aa;
485                        }
486                        $aa++;
487                    }
488                    $a++;
489                }
490                if ($correct_answers_all_zero) {
491                    // correct answers all have score of 0%, 
492                    // so reset score for correct answers 100%
493                    foreach ($correct_answers as $aa) {
494                        $question->fraction[$aa] = 1;
495                    }
496                }
497                // add a sanity check for empty questions, see MDL-17779
498                if (!empty($question->questiontext)) {
499                    $questions[] = $question;
500                }
501                $q++;
502            }
503            $x++;
504        }
505    }
506
507    function hotpot_seed_RNG() {
508        // seed the random number generator
509        static $HOTPOT_SEEDED_RNG = FALSE;
510        if (!$HOTPOT_SEEDED_RNG) {
511            srand((double) microtime() * 1000000);
512            $HOTPOT_SEEDED_RNG = TRUE;
513        }
514    }
515    function hotpot_get_title(&$xml, $x, $flag=false) {
516        $title = $xml->xml_value('data,title');
517        if ($x || $flag) {
518            $title .= ' ('.($x+1).')';
519        }
520        return $this->hotpot_prepare_str($title);
521    }
522    function hotpot_get_instructions(&$xml) {
523        $text = $xml->xml_value('hotpot-config-file,'.$xml->quiztype.',instructions');
524        if (empty($text)) {
525            $text = "Hot Potatoes $xml->quiztype";
526        }
527        return $this->hotpot_prepare_str($text);
528    }
529    function hotpot_get_reading(&$xml) {
530        $str = '';
531        $tags = 'data,reading';
532        if ($xml->xml_value("$tags,include-reading")) {
533            if ($title = $xml->xml_value("$tags,reading-title")) {
534                $str .= "<H3>$title</H3>";
535            }
536            if ($text = $xml->xml_value("$tags,reading-text")) {
537                $str .= "<P>$text</P>";
538            }
539        }
540        return $this->hotpot_prepare_str($str);
541    }
542    function hotpot_prepare_str($str) {
543        // convert html entities to unicode and add slashes
544        $str = preg_replace_callback('/&#([0-9]+);/', array(&$this, 'hotpot_prepare_str_dec'), $str);
545        $str = preg_replace_callback('/&#x([0-9a-f]+);/i', array(&$this, 'hotpot_prepare_str_hexdec'), $str);
546        return addslashes($str);
547    }
548    function hotpot_prepare_str_dec(&$matches) {
549        return hotpot_charcode_to_utf8($matches[1]);
550    }
551    function hotpot_prepare_str_hexdec(&$matches) {
552        return hotpot_charcode_to_utf8(hexdec($matches[1]));
553    }
554} // end class
555
556function hotpot_charcode_to_utf8($charcode) {
557    // thanks to Miguel Perez: http://jp2.php.net/chr (19-Sep-2007)
558    if ($charcode <= 0x7F) {
559        // ascii char (roman alphabet + punctuation)
560        return chr($charcode);
561    }
562    if ($charcode <= 0x7FF) {
563        // 2-byte char
564        return chr(($charcode >> 0x06) + 0xC0).chr(($charcode & 0x3F) + 0x80);
565    }
566    if ($charcode <= 0xFFFF) {
567        // 3-byte char
568        return chr(($charcode >> 0x0C) + 0xE0).chr((($charcode >> 0x06) & 0x3F) + 0x80).chr(($charcode & 0x3F) + 0x80);
569    }
570    if ($charcode <= 0x1FFFFF) {
571        // 4-byte char
572        return chr(($charcode >> 0x12) + 0xF0).chr((($charcode >> 0x0C) & 0x3F) + 0x80).chr((($charcode >> 0x06) & 0x3F) + 0x80).chr(($charcode & 0x3F) + 0x80);
573    }
574    // unidentified char code !!
575    return ' '; 
576}
577
578function hotpot_convert_relative_urls($str, $baseurl, $filename) {
579    $tagopen = '(?:(<)|(&lt;)|(&amp;#x003C;))'; // left angle bracket
580    $tagclose = '(?(2)>|(?(3)&gt;|(?(4)&amp;#x003E;)))'; //  right angle bracket (to match left angle bracket)
581
582    $space = '\s+'; // at least one space
583    $anychar = '(?:[^>]*?)'; // any character
584
585    $quoteopen = '("|&quot;|&amp;quot;)'; // open quote
586    $quoteclose = '\\5'; //  close quote (to match open quote)
587
588    $replace = "hotpot_convert_relative_url('".$baseurl."', '".$filename."', '\\1', '\\6', '\\7')";
589
590    $tags = array('script'=>'src', 'link'=>'href', 'a'=>'href','img'=>'src','param'=>'value', 'object'=>'data', 'embed'=>'src');
591    foreach ($tags as $tag=>$attribute) {
592        if ($tag=='param') {
593            $url = '\S+?\.\S+?'; // must include a filename and have no spaces
594        } else {
595            $url = '.*?';
596        }
597        $search = "/($tagopen$tag$space$anychar$attribute=$quoteopen)($url)($quoteclose$anychar$tagclose)/is";
598        if (preg_match_all($search, $str, $matches, PREG_OFFSET_CAPTURE)) {
599            $i_max = count($matches[0]) - 1;
600            for ($i=$i_max; $i>=0; $i--) {
601                $match = $matches[0][$i][0];
602                $start = $matches[0][$i][1];
603                $replace = hotpot_convert_relative_url(
604                    $baseurl, $filename, $matches[1][$i][0], $matches[6][$i][0], $matches[7][$i][0], false
605                );
606                $str = substr_replace($str, $replace, $start, strlen($match));
607            }
608        }
609    }
610
611    return $str;
612}