Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 39 and 311]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   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.
   8  //
   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.
  13  //
  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/>.
  16  
  17  /**
  18   * Examview question importer.
  19   *
  20   * @package    qformat_examview
  21   * @copyright  2005 Howard Miller
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once($CFG->libdir . '/xmlize.php');
  29  
  30  
  31  /**
  32   * Examview question importer.
  33   *
  34   * @copyright  2005 Howard Miller
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class qformat_examview extends qformat_based_on_xml {
  38  
  39      public $qtypes = array(
  40          'tf' => 'truefalse',
  41          'mc' => 'multichoice',
  42          'yn' => 'truefalse',
  43          'co' => 'shortanswer',
  44          'ma' => 'match',
  45          'mtf' => 99,
  46          'nr' => 'numerical',
  47          'pr' => 99,
  48          'es' => 'essay',
  49          'ca' => 99,
  50          'ot' => 99,
  51          'sa' => 'shortanswer',
  52      );
  53  
  54      public $matchingquestions = array();
  55  
  56      public function provide_import() {
  57          return true;
  58      }
  59  
  60      public function mime_type() {
  61          return 'application/xml';
  62      }
  63  
  64      /**
  65       * Validate the given file.
  66       *
  67       * For more expensive or detailed integrity checks.
  68       *
  69       * @param stored_file $file the file to check
  70       * @return string the error message that occurred while validating the given file
  71       */
  72      public function validate_file(stored_file $file): string {
  73          return $this->validate_is_utf8_file($file);
  74      }
  75  
  76      /**
  77       * unxmlise reconstructs part of the xml data structure in order
  78       * to identify the actual data therein
  79       * @param array $xml section of the xml data structure
  80       * @return string data with evrything else removed
  81       */
  82      protected function unxmlise( $xml ) {
  83          // If it's not an array then it's probably just data.
  84          if (!is_array($xml)) {
  85              $text = s($xml);
  86          } else {
  87              // Otherwise parse the array.
  88              $text = '';
  89              foreach ($xml as $tag => $data) {
  90                  // If tag is '@' then it's attributes and we don't care.
  91                  if ($tag!=='@') {
  92                      $text = $text . $this->unxmlise( $data );
  93                  }
  94              }
  95          }
  96  
  97          // Currently we throw the tags we found.
  98          $text = strip_tags($text);
  99          return $text;
 100      }
 101  
 102      public function parse_matching_groups($matchinggroups) {
 103          if (empty($matchinggroups)) {
 104              return;
 105          }
 106          foreach ($matchinggroups as $matchgroup) {
 107              $newgroup = new stdClass();
 108              $groupname = trim($matchgroup['@']['name']);
 109              $questiontext = $this->unxmlise($matchgroup['#']['text'][0]['#']);
 110              $newgroup->questiontext = trim($questiontext);
 111              $newgroup->subchoices = array();
 112              $newgroup->subquestions = array();
 113              $newgroup->subanswers = array();
 114              $choices = $matchgroup['#']['choices']['0']['#'];
 115              foreach ($choices as $key => $value) {
 116                  if (strpos(trim($key), 'choice-') !== false) {
 117                      $key = strtoupper(trim(str_replace('choice-', '', $key)));
 118                      $newgroup->subchoices[$key] = trim($value['0']['#']);
 119                  }
 120              }
 121              $this->matching_questions[$groupname] = $newgroup;
 122          }
 123      }
 124  
 125      protected function parse_ma($qrec, $groupname) {
 126          $matchgroup = $this->matching_questions[$groupname];
 127          $phrase = trim($this->unxmlise($qrec['text']['0']['#']));
 128          $answer = trim($this->unxmlise($qrec['answer']['0']['#']));
 129          $answer = strip_tags( $answer );
 130          $matchgroup->mappings[$phrase] = $matchgroup->subchoices[$answer];
 131          $this->matching_questions[$groupname] = $matchgroup;
 132          return null;
 133      }
 134  
 135      protected function process_matches(&$questions) {
 136          if (empty($this->matching_questions)) {
 137              return;
 138          }
 139  
 140          foreach ($this->matching_questions as $matchgroup) {
 141              $question = $this->defaultquestion();
 142              $htmltext = s($matchgroup->questiontext);
 143              $question->questiontext = $htmltext;
 144              $question->questiontextformat = FORMAT_HTML;
 145              $question->questiontextfiles = array();
 146              $question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
 147              $question->qtype = 'match';
 148              $question = $this->add_blank_combined_feedback($question);
 149              $question->subquestions = array();
 150              $question->subanswers = array();
 151              foreach ($matchgroup->subchoices as $subchoice) {
 152                  $fiber = array_keys ($matchgroup->mappings, $subchoice);
 153                  $subquestion = '';
 154                  foreach ($fiber as $subquestion) {
 155                      $question->subquestions[] = $this->text_field($subquestion);
 156                      $question->subanswers[] = $subchoice;
 157                  }
 158                  if ($subquestion == '') { // Then in this case, $subchoice is a distractor.
 159                      $question->subquestions[] = $this->text_field('');
 160                      $question->subanswers[] = $subchoice;
 161                  }
 162              }
 163              $questions[] = $question;
 164          }
 165      }
 166  
 167      protected function cleanunicode($text) {
 168          return str_replace('&#x2019;', "'", $text);
 169      }
 170  
 171      public function readquestions($lines) {
 172          // Parses an array of lines into an array of questions,
 173          // where each item is a question object as defined by
 174          // readquestion().
 175  
 176          $questions = array();
 177          $currentquestion = array();
 178  
 179          $text = implode(' ', $lines);
 180          $text = $this->cleanunicode($text);
 181  
 182          $xml = xmlize($text, 0);
 183          if (!empty($xml['examview']['#']['matching-group'])) {
 184              $this->parse_matching_groups($xml['examview']['#']['matching-group']);
 185          }
 186  
 187          $questionnode = $xml['examview']['#']['question'];
 188          foreach ($questionnode as $currentquestion) {
 189              if ($question = $this->readquestion($currentquestion)) {
 190                  $questions[] = $question;
 191              }
 192          }
 193  
 194          $this->process_matches($questions);
 195          return $questions;
 196      }
 197  
 198      public function readquestion($qrec) {
 199          global $OUTPUT;
 200  
 201          $type = trim($qrec['@']['type']);
 202          $question = $this->defaultquestion();
 203          if (array_key_exists($type, $this->qtypes)) {
 204              $question->qtype = $this->qtypes[$type];
 205          } else {
 206              $question->qtype = null;
 207          }
 208          $question->single = 1;
 209  
 210          // Only one answer is allowed.
 211          $htmltext = $this->unxmlise($qrec['#']['text'][0]['#']);
 212  
 213          $question->questiontext = $this->cleaninput($htmltext);
 214          $question->questiontextformat = FORMAT_HTML;
 215          $question->questiontextfiles = array();
 216          $question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
 217  
 218          switch ($question->qtype) {
 219              case 'multichoice':
 220                  $question = $this->parse_mc($qrec['#'], $question);
 221                  break;
 222              case 'match':
 223                  $groupname = trim($qrec['@']['group']);
 224                  $question = $this->parse_ma($qrec['#'], $groupname);
 225                  break;
 226              case 'truefalse':
 227                  $question = $this->parse_tf_yn($qrec['#'], $question);
 228                  break;
 229              case 'shortanswer':
 230                  $question = $this->parse_co($qrec['#'], $question);
 231                  break;
 232              case 'essay':
 233                  $question = $this->parse_es($qrec['#'], $question);
 234                  break;
 235              case 'numerical':
 236                  $question = $this->parse_nr($qrec['#'], $question);
 237                  break;
 238                  break;
 239              default:
 240                  echo $OUTPUT->notification(get_string('unknownorunhandledtype', 'question', $type));
 241                  $question = null;
 242          }
 243  
 244          return $question;
 245      }
 246  
 247      protected function parse_tf_yn($qrec, $question) {
 248          $choices = array('T' => 1, 'Y' => 1, 'F' => 0, 'N' => 0 );
 249          $answer = trim($qrec['answer'][0]['#']);
 250          $question->answer = $choices[$answer];
 251          $question->correctanswer = $question->answer;
 252          if ($question->answer == 1) {
 253              $question->feedbacktrue = $this->text_field(get_string('correct', 'question'));
 254              $question->feedbackfalse = $this->text_field(get_string('incorrect', 'question'));
 255          } else {
 256              $question->feedbacktrue = $this->text_field(get_string('incorrect', 'question'));
 257              $question->feedbackfalse = $this->text_field(get_string('correct', 'question'));
 258          }
 259          return $question;
 260      }
 261  
 262      protected function parse_mc($qrec, $question) {
 263          $question = $this->add_blank_combined_feedback($question);
 264          $answer = 'choice-'.strtolower(trim($qrec['answer'][0]['#']));
 265  
 266          $choices = $qrec['choices'][0]['#'];
 267          foreach ($choices as $key => $value) {
 268              if (strpos(trim($key), 'choice-') !== false) {
 269  
 270                  $question->answer[] = $this->text_field(s($this->unxmlise($value[0]['#'])));
 271                  if (strcmp($key, $answer) == 0) {
 272                      $question->fraction[] = 1;
 273                      $question->feedback[] = $this->text_field(get_string('correct', 'question'));
 274                  } else {
 275                      $question->fraction[] = 0;
 276                      $question->feedback[] = $this->text_field(get_string('incorrect', 'question'));
 277                  }
 278              }
 279          }
 280          return $question;
 281      }
 282  
 283      protected function parse_co($qrec, $question) {
 284          $question->usecase = 0;
 285          $answer = trim($this->unxmlise($qrec['answer'][0]['#']));
 286          $answer = strip_tags( $answer );
 287          $answers = explode("\n", $answer);
 288  
 289          foreach ($answers as $key => $value) {
 290              $value = trim($value);
 291              if (strlen($value) > 0) {
 292                  $question->answer[] = $value;
 293                  $question->fraction[] = 1;
 294                  $question->feedback[] = $this->text_field(get_string('correct', 'question'));
 295              }
 296          }
 297          $question->answer[] = '*';
 298          $question->fraction[] = 0;
 299          $question->feedback[] = $this->text_field(get_string('incorrect', 'question'));
 300  
 301          return $question;
 302      }
 303  
 304      protected function parse_es($qrec, $question) {
 305          $feedback = trim($this->unxmlise($qrec['answer'][0]['#']));
 306          $question->graderinfo =  $this->text_field($feedback);
 307          $question->responsetemplate =  $this->text_field('');
 308          $question->responserequired = 1;
 309          $question->feedback = $feedback;
 310          $question->responseformat = 'editor';
 311          $question->responsefieldlines = 15;
 312          $question->attachments = 0;
 313          $question->attachmentsrequired = 0;
 314          $question->fraction = 0;
 315          return $question;
 316      }
 317  
 318      protected function parse_nr($qrec, $question) {
 319          $answer = trim($this->unxmlise($qrec['answer'][0]['#']));
 320          $answer = strip_tags( $answer );
 321          $answers = explode("\n", $answer);
 322  
 323          foreach ($answers as $key => $value) {
 324              $value = trim($value);
 325              if (is_numeric($value)) {
 326                  $errormargin = 0;
 327                  $question->answer[] = $value;
 328                  $question->fraction[] = 1;
 329                  $question->feedback[] = $this->text_field(get_string('correct', 'question'));
 330                  $question->tolerance[] = $errormargin;
 331              }
 332          }
 333          return $question;
 334      }
 335  
 336  }
 337  // End class.
 338  
 339