You are here

class qformat_hotpot in Quiz 6.5

Same name and namespace in other branches
  1. 6.6 includes/moodle/question/format/hotpot/format.php \qformat_hotpot

@package questionbank @subpackage importexport

Hierarchy

Expanded class hierarchy of qformat_hotpot

File

includes/moodle/question/format/hotpot/format.php, line 16

View source
class qformat_hotpot extends qformat_default {
  function provide_import() {
    return true;
  }
  function readquestions($lines) {

    /// Parses an array of lines into an array of questions,

    /// where each item is a question object as defined by

    /// readquestion().

    // set courseid and baseurl
    global $CFG, $COURSE, $course;
    switch (true) {
      case isset($this->course->id):

        // import to quiz module
        $courseid = $this->course->id;
        break;
      case isset($course->id):

        // import to lesson module
        $courseid = $course->id;
        break;
      case isset($COURSE->id):

        // last resort
        $courseid = $COURSE->id;
        break;
      default:

        // shouldn't happen !!
        $courseid = 0;
    }
    require_once $CFG->libdir . '/filelib.php';
    $baseurl = get_file_url($courseid) . '/';

    // get import file name
    global $params;
    if (isset($params) && !empty($params->choosefile)) {

      // course file (Moodle >=1.6+)
      $filename = $params->choosefile;
    }
    else {

      // uploaded file (all Moodles)
      $filename = basename($_FILES['newfile']['tmp_name']);
    }

    // get hotpot file source
    $source = implode($lines, " ");
    $source = hotpot_convert_relative_urls($source, $baseurl, $filename);

    // create xml tree for this hotpot
    $xml = new hotpot_xml_tree($source);

    // determine the quiz type
    $xml->quiztype = '';
    $keys = array_keys($xml->xml);
    foreach ($keys as $key) {
      if (preg_match('/^(hotpot|textoys)-(\\w+)-file$/i', $key, $matches)) {
        $xml->quiztype = strtolower($matches[2]);
        $xml->xml_root = "['{$key}']['#']";
        break;
      }
    }

    // convert xml to questions array
    $questions = array();
    switch ($xml->quiztype) {
      case 'jcloze':
        $this
          ->process_jcloze($xml, $questions);
        break;
      case 'jcross':
        $this
          ->process_jcross($xml, $questions);
        break;
      case 'jmatch':
        $this
          ->process_jmatch($xml, $questions);
        break;
      case 'jmix':
        $this
          ->process_jmix($xml, $questions);
        break;
      case 'jbc':
      case 'jquiz':
        $this
          ->process_jquiz($xml, $questions);
        break;
      default:
        if (empty($xml->quiztype)) {
          notice("Input file not recognized as a Hot Potatoes XML file");
        }
        else {
          notice("Unknown quiz type '{$xml->quiztype}'");
        }
    }

    // end switch
    return $questions;
  }
  function process_jcloze(&$xml, &$questions) {

    // define default grade (per cloze gap)
    $defaultgrade = 1;
    $gap_count = 0;

    // detect old Moodles (1.4 and earlier)
    global $CFG, $db;
    $moodle_14 = false;
    if ($columns = $db
      ->MetaColumns("{$CFG->prefix}question_multianswer")) {
      foreach ($columns as $column) {
        if ($column->name == 'answers' || $column->name == 'positionkey' || $column->name == 'answertype' || $column->name == 'norm') {
          $moodle_14 = true;
        }
      }
    }

    // xml tags for the start of the gap-fill exercise
    $tags = 'data,gap-fill';
    $x = 0;
    while (($exercise = "[{$x}]['#']") && $xml
      ->xml_value($tags, $exercise)) {

      // there is usually only one exercise in a file
      if (method_exists($this, 'defaultquestion')) {
        $question = $this
          ->defaultquestion();
      }
      else {
        $question = new stdClass();
        $question->usecase = 0;

        // Ignore case
        $question->image = "";

        // No images with this format
      }
      $question->qtype = MULTIANSWER;
      $question->name = $this
        ->hotpot_get_title($xml, $x);
      $question->questiontext = $this
        ->hotpot_get_reading($xml);

      // setup answer arrays
      if ($moodle_14) {
        $question->answers = array();
      }
      else {
        global $COURSE;

        // initialized in questions/import.php
        $question->course = $COURSE->id;
        $question->options = new stdClass();
        $question->options->questions = array();

        // one for each gap
      }
      $q = 0;
      while ($text = $xml
        ->xml_value($tags, $exercise . "[{$q}]")) {

        // add next bit of text
        $question->questiontext .= $this
          ->hotpot_prepare_str($text);

        // check for a gap
        $question_record = $exercise . "['question-record'][{$q}]['#']";
        if ($xml
          ->xml_value($tags, $question_record)) {

          // add gap
          $gap_count++;
          $positionkey = $q + 1;
          $question->questiontext .= '{#' . $positionkey . '}';

          // initialize answer settings
          if ($moodle_14) {
            $question->answers[$q]->positionkey = $positionkey;
            $question->answers[$q]->answertype = SHORTANSWER;
            $question->answers[$q]->norm = $defaultgrade;
            $question->answers[$q]->alternatives = array();
          }
          else {
            $wrapped = new stdClass();
            $wrapped->qtype = SHORTANSWER;
            $wrapped->usecase = 0;
            $wrapped->defaultgrade = $defaultgrade;
            $wrapped->questiontextformat = 0;
            $wrapped->answer = array();
            $wrapped->fraction = array();
            $wrapped->feedback = array();
            $answers = array();
          }

          // add answers
          $a = 0;
          while (($answer = $question_record . "['answer'][{$a}]['#']") && $xml
            ->xml_value($tags, $answer)) {
            $text = $this
              ->hotpot_prepare_str($xml
              ->xml_value($tags, $answer . "['text'][0]['#']"));
            $correct = $xml
              ->xml_value($tags, $answer . "['correct'][0]['#']");
            $feedback = $this
              ->hotpot_prepare_str($xml
              ->xml_value($tags, $answer . "['feedback'][0]['#']"));
            if ($text) {

              // set score (0=0%, 1=100%)
              $fraction = empty($correct) ? 0 : 1;

              // store answer
              if ($moodle_14) {
                $question->answers[$q]->alternatives[$a] = new stdClass();
                $question->answers[$q]->alternatives[$a]->answer = $text;
                $question->answers[$q]->alternatives[$a]->fraction = $fraction;
                $question->answers[$q]->alternatives[$a]->feedback = $feedback;
              }
              else {
                $wrapped->answer[] = $text;
                $wrapped->fraction[] = $fraction;
                $wrapped->feedback[] = $feedback;
                $answers[] = (empty($fraction) ? '' : '=') . $text . (empty($feedback) ? '' : '#' . $feedback);
              }
            }
            $a++;
          }

          // compile answers into question text, if necessary
          if ($moodle_14) {

            // do nothing
          }
          else {
            $wrapped->questiontext = '{' . $defaultgrade . ':SHORTANSWER:' . implode('~', $answers) . '}';
            $question->options->questions[] = $wrapped;
          }
        }

        // end if gap
        $q++;
      }

      // end while $text
      // define total grade for this exercise
      $question->defaultgrade = $gap_count * $defaultgrade;
      $questions[] = $question;
      $x++;
    }

    // end while $exercise
  }
  function process_jcross(&$xml, &$questions) {

    // xml tags to the start of the crossword exercise clue items
    $tags = 'data,crossword,clues,item';
    $x = 0;
    while (($item = "[{$x}]['#']") && $xml
      ->xml_value($tags, $item)) {
      $text = $xml
        ->xml_value($tags, $item . "['def'][0]['#']");
      $answer = $xml
        ->xml_value($tags, $item . "['word'][0]['#']");
      if ($text && $answer) {
        if (method_exists($this, 'defaultquestion')) {
          $question = $this
            ->defaultquestion();
        }
        else {
          $question = new stdClass();
          $question->usecase = 0;

          // Ignore case
          $question->image = "";

          // No images with this format
        }
        $question->qtype = SHORTANSWER;
        $question->name = $this
          ->hotpot_get_title($xml, $x, true);
        $question->questiontext = $this
          ->hotpot_prepare_str($text);
        $question->answer = array(
          $this
            ->hotpot_prepare_str($answer),
        );
        $question->fraction = array(
          1,
        );
        $question->feedback = array(
          '',
        );
        $questions[] = $question;
      }
      $x++;
    }
  }
  function process_jmatch(&$xml, &$questions) {

    // define default grade (per matched pair)
    $defaultgrade = 1;
    $match_count = 0;

    // xml tags to the start of the matching exercise
    $tags = 'data,matching-exercise';
    $x = 0;
    while (($exercise = "[{$x}]['#']") && $xml
      ->xml_value($tags, $exercise)) {

      // there is usually only one exercise in a file
      if (method_exists($this, 'defaultquestion')) {
        $question = $this
          ->defaultquestion();
      }
      else {
        $question = new stdClass();
        $question->usecase = 0;

        // Ignore case
        $question->image = "";

        // No images with this format
      }
      $question->qtype = MATCH;
      $question->name = $this
        ->hotpot_get_title($xml, $x);
      $question->questiontext = $this
        ->hotpot_get_reading($xml);
      $question->questiontext .= $this
        ->hotpot_get_instructions($xml);
      $question->subquestions = array();
      $question->subanswers = array();
      $p = 0;
      while (($pair = $exercise . "['pair'][{$p}]['#']") && $xml
        ->xml_value($tags, $pair)) {
        $left = $xml
          ->xml_value($tags, $pair . "['left-item'][0]['#']['text'][0]['#']");
        $right = $xml
          ->xml_value($tags, $pair . "['right-item'][0]['#']['text'][0]['#']");
        if ($left && $right) {
          $match_count++;
          $question->subquestions[$p] = $this
            ->hotpot_prepare_str($left);
          $question->subanswers[$p] = $this
            ->hotpot_prepare_str($right);
        }
        $p++;
      }
      $question->defaultgrade = $match_count * $defaultgrade;
      $questions[] = $question;
      $x++;
    }
  }
  function process_jmix(&$xml, &$questions) {

    // define default grade (per segment)
    $defaultgrade = 1;
    $segment_count = 0;

    // xml tags to the start of the jumbled order exercise
    $tags = 'data,jumbled-order-exercise';
    $x = 0;
    while (($exercise = "[{$x}]['#']") && $xml
      ->xml_value($tags, $exercise)) {

      // there is usually only one exercise in a file
      if (method_exists($this, 'defaultquestion')) {
        $question = $this
          ->defaultquestion();
      }
      else {
        $question = new stdClass();
        $question->usecase = 0;

        // Ignore case
        $question->image = "";

        // No images with this format
      }
      $question->qtype = SHORTANSWER;
      $question->name = $this
        ->hotpot_get_title($xml, $x);
      $question->answer = array();
      $question->fraction = array();
      $question->feedback = array();
      $i = 0;
      $segments = array();
      while ($segment = $xml
        ->xml_value($tags, $exercise . "['main-order'][0]['#']['segment'][{$i}]['#']")) {
        $segments[] = $this
          ->hotpot_prepare_str($segment);
        $segment_count++;
        $i++;
      }
      $answer = implode(' ', $segments);
      $this
        ->hotpot_seed_RNG();
      shuffle($segments);
      $question->questiontext = $this
        ->hotpot_get_reading($xml);
      $question->questiontext .= $this
        ->hotpot_get_instructions($xml);
      $question->questiontext .= ' &nbsp; <NOBR><B>[ &nbsp; ' . implode(' &nbsp; ', $segments) . ' &nbsp; ]</B></NOBR>';
      $a = 0;
      while (!empty($answer)) {
        $question->answer[$a] = $answer;
        $question->fraction[$a] = 1;
        $question->feedback[$a] = '';
        $answer = $this
          ->hotpot_prepare_str($xml
          ->xml_value($tags, $exercise . "['alternate'][{$a}]['#']"));
        $a++;
      }
      $question->defaultgrade = $segment_count * $defaultgrade;
      $questions[] = $question;
      $x++;
    }
  }
  function process_jquiz(&$xml, &$questions) {

    // define default grade (per question)
    $defaultgrade = 1;

    // xml tags to the start of the questions
    $tags = 'data,questions';
    $x = 0;
    while (($exercise = "[{$x}]['#']") && $xml
      ->xml_value($tags, $exercise)) {

      // there is usually only one 'questions' object in a single exercise
      $q = 0;
      while (($question_record = $exercise . "['question-record'][{$q}]['#']") && $xml
        ->xml_value($tags, $question_record)) {
        if (method_exists($this, 'defaultquestion')) {
          $question = $this
            ->defaultquestion();
        }
        else {
          $question = new stdClass();
          $question->usecase = 0;

          // Ignore case
          $question->image = "";

          // No images with this format
        }
        $question->defaultgrade = $defaultgrade;
        $question->name = $this
          ->hotpot_get_title($xml, $q, true);
        $text = $xml
          ->xml_value($tags, $question_record . "['question'][0]['#']");
        $question->questiontext = $this
          ->hotpot_prepare_str($text);
        if ($xml
          ->xml_value($tags, $question_record . "['answers']")) {

          // HP6 JQuiz
          $answers = $question_record . "['answers'][0]['#']";
        }
        else {

          // HP5 JBC or JQuiz
          $answers = $question_record;
        }
        if ($xml
          ->xml_value($tags, $question_record . "['question-type']")) {

          // HP6 JQuiz
          $type = $xml
            ->xml_value($tags, $question_record . "['question-type'][0]['#']");

          //  1 : multiple choice
          //  2 : short-answer
          //  3 : hybrid
          //  4 : multiple select
        }
        else {

          // HP5
          switch ($xml->quiztype) {
            case 'jbc':
              $must_select_all = $xml
                ->xml_value($tags, $question_record . "['must-select-all'][0]['#']");
              if (empty($must_select_all)) {
                $type = 1;

                // multichoice
              }
              else {
                $type = 4;

                // multiselect
              }
              break;
            case 'jquiz':
              $type = 2;

              // shortanswer
              break;
            default:
              $type = 0;
          }
        }
        $question->qtype = $type == 2 ? SHORTANSWER : MULTICHOICE;
        $question->single = $type == 4 ? 0 : 1;

        // workaround required to calculate scores for multiple select answers
        $no_of_correct_answers = 0;
        if ($type == 4) {
          $a = 0;
          while (($answer = $answers . "['answer'][{$a}]['#']") && $xml
            ->xml_value($tags, $answer)) {
            $correct = $xml
              ->xml_value($tags, $answer . "['correct'][0]['#']");
            if (empty($correct)) {

              // do nothing
            }
            else {
              $no_of_correct_answers++;
            }
            $a++;
          }
        }
        $a = 0;
        $question->answer = array();
        $question->fraction = array();
        $question->feedback = array();
        $aa = 0;
        $correct_answers = array();
        $correct_answers_all_zero = true;
        while (($answer = $answers . "['answer'][{$a}]['#']") && $xml
          ->xml_value($tags, $answer)) {
          $correct = $xml
            ->xml_value($tags, $answer . "['correct'][0]['#']");
          if (empty($correct)) {
            $fraction = 0;
          }
          else {
            if ($type == 4) {

              // multiple select
              // strange behavior if the $fraction isn't exact to 5 decimal places
              $fraction = round(1 / $no_of_correct_answers, 5);
            }
            else {
              if ($xml
                ->xml_value($tags, $answer . "['percent-correct']")) {

                // HP6 JQuiz
                $percent = $xml
                  ->xml_value($tags, $answer . "['percent-correct'][0]['#']");
                $fraction = $percent / 100;
              }
              else {

                // HP5 JBC or JQuiz
                $fraction = 1;
              }
            }
          }
          $answertext = $this
            ->hotpot_prepare_str($xml
            ->xml_value($tags, $answer . "['text'][0]['#']"));
          if ($answertext != '') {
            $question->answer[$aa] = $answertext;
            $question->fraction[$aa] = $fraction;
            $question->feedback[$aa] = $this
              ->hotpot_prepare_str($xml
              ->xml_value($tags, $answer . "['feedback'][0]['#']"));
            if ($correct) {
              if ($fraction) {
                $correct_answers_all_zero = false;
              }
              $correct_answers[] = $aa;
            }
            $aa++;
          }
          $a++;
        }
        if ($correct_answers_all_zero) {

          // correct answers all have score of 0%,
          // so reset score for correct answers 100%
          foreach ($correct_answers as $aa) {
            $question->fraction[$aa] = 1;
          }
        }

        // add a sanity check for empty questions, see MDL-17779
        if (!empty($question->questiontext)) {
          $questions[] = $question;
        }
        $q++;
      }
      $x++;
    }
  }
  function hotpot_seed_RNG() {

    // seed the random number generator
    static $HOTPOT_SEEDED_RNG = FALSE;
    if (!$HOTPOT_SEEDED_RNG) {
      srand((double) microtime() * 1000000);
      $HOTPOT_SEEDED_RNG = TRUE;
    }
  }
  function hotpot_get_title(&$xml, $x, $flag = false) {
    $title = $xml
      ->xml_value('data,title');
    if ($x || $flag) {
      $title .= ' (' . ($x + 1) . ')';
    }
    return $this
      ->hotpot_prepare_str($title);
  }
  function hotpot_get_instructions(&$xml) {
    $text = $xml
      ->xml_value('hotpot-config-file,instructions');
    if (empty($text)) {
      $text = "Hot Potatoes {$xml->quiztype}";
    }
    return $this
      ->hotpot_prepare_str($text);
  }
  function hotpot_get_reading(&$xml) {
    $str = '';
    $tags = 'data,reading';
    if ($xml
      ->xml_value("{$tags},include-reading")) {
      if ($title = $xml
        ->xml_value("{$tags},reading-title")) {
        $str .= "<H3>{$title}</H3>";
      }
      if ($text = $xml
        ->xml_value("{$tags},reading-text")) {
        $str .= "<P>{$text}</P>";
      }
    }
    return $this
      ->hotpot_prepare_str($str);
  }
  function hotpot_prepare_str($str) {

    // convert html entities to unicode and add slashes
    $str = preg_replace('/&#x([0-9a-f]+);/ie', "hotpot_charcode_to_utf8(hexdec('\\1'))", $str);
    $str = preg_replace('/&#([0-9]+);/e', "hotpot_charcode_to_utf8(\\1)", $str);
    return addslashes($str);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
qformat_default::$canaccessbackupdata property
qformat_default::$category property
qformat_default::$catfromfile property
qformat_default::$cattofile property
qformat_default::$contextfromfile property
qformat_default::$contexttofile property
qformat_default::$course property
qformat_default::$displayerrors property
qformat_default::$filename property
qformat_default::$importerrors property
qformat_default::$matchgrades property
qformat_default::$questionids property
qformat_default::$questions property
qformat_default::$realfilename property
qformat_default::$stoponerror property
qformat_default::$translator property
qformat_default::count_questions function Count all non-category questions in the questions array.
qformat_default::create_category_path function find and/or create the category described by a delimited list e.g. $course$/tom/dick/harry or tom/dick/harry
qformat_default::defaultquestion function return an "empty" question Somewhere to specify question parameters that are not handled by import but are required db fields. This should not be overridden.
qformat_default::error function Handle parsing error
qformat_default::exportpostprocess function Do an post-processing that may be required
qformat_default::exportpreprocess function Do any pre-processing that may be required 1
qformat_default::exportprocess function Do the export For most types this should not need to be overrided 1
qformat_default::export_file_extension function Return the files extension appropriate for this type override if you don't want .txt 3
qformat_default::format_question_text function where question specifies a moodle (text) format this performs the conversion.
qformat_default::get_category_path function get the category as a path (e.g., tom/dick/harry)
qformat_default::importimagefile function Import an image file encoded in base64 format
qformat_default::importpostprocess function Override if any post-processing is required 2
qformat_default::importpreprocess function Perform any required pre-processing 2
qformat_default::importprocess function Process the file This method should not normally be overidden 1
qformat_default::presave_process function Enable any processing to be done on the content just prior to the file being saved default is to do nothing 2
qformat_default::provide_export function 4
qformat_default::question_get_export_dir function get directory into which export is going
qformat_default::readdata function Return complete file within an array, one item per line 1
qformat_default::readquestion function Given the data known to define a question in this format, this function converts it into a question object suitable for processing and insertion into Moodle. 5
qformat_default::setCategory function set the category
qformat_default::setCatfromfile function set catfromfile
qformat_default::setCattofile function set cattofile
qformat_default::setContextfromfile function set contextfromfile
qformat_default::setContexts function set an array of contexts.
qformat_default::setContexttofile function set contexttofile
qformat_default::setCourse function set the course class variable
qformat_default::setFilename function set the filename
qformat_default::setMatchgrades function set matchgrades
qformat_default::setQuestions function Set the specific questions to export. Should not include questions with parents (sub questions of cloze question type). Only used for question export.
qformat_default::setRealfilename function set the "real" filename (this is what the user typed, regardless of wha happened next)
qformat_default::setStoponerror function set stoponerror
qformat_default::set_can_access_backupdata function
qformat_default::try_exporting_using_qtypes function Provide export functionality for plugin questiontypes Do not override
qformat_default::try_importing_using_qtypes function Import for questiontype plugins Do not override.
qformat_default::writequestion function convert a single question object into text output in the given format. This must be overriden 4
qformat_hotpot::hotpot_get_instructions function
qformat_hotpot::hotpot_get_reading function
qformat_hotpot::hotpot_get_title function
qformat_hotpot::hotpot_prepare_str function
qformat_hotpot::hotpot_seed_RNG function
qformat_hotpot::process_jcloze function
qformat_hotpot::process_jcross function
qformat_hotpot::process_jmatch function
qformat_hotpot::process_jmix function
qformat_hotpot::process_jquiz function
qformat_hotpot::provide_import function Overrides qformat_default::provide_import
qformat_hotpot::readquestions function Parses an array of lines into an array of questions, where each item is a question object as defined by readquestion(). Questions are defined as anything between blank lines. Overrides qformat_default::readquestions