[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/question/type/multianswer/ -> questiontype.php (source)

   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   * Question type class for the multi-answer question type.
  19   *
  20   * @package    qtype
  21   * @subpackage multianswer
  22   * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  require_once($CFG->dirroot . '/question/type/multichoice/question.php');
  30  
  31  
  32  /**
  33   * The multi-answer question type class.
  34   *
  35   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class qtype_multianswer extends question_type {
  39  
  40      public function can_analyse_responses() {
  41          return false;
  42      }
  43  
  44      public function get_question_options($question) {
  45          global $DB, $OUTPUT;
  46  
  47          // Get relevant data indexed by positionkey from the multianswers table.
  48          $sequence = $DB->get_field('question_multianswer', 'sequence',
  49                  array('question' => $question->id), '*', MUST_EXIST);
  50  
  51          $wrappedquestions = $DB->get_records_list('question', 'id',
  52                  explode(',', $sequence), 'id ASC');
  53  
  54          // We want an array with question ids as index and the positions as values.
  55          $sequence = array_flip(explode(',', $sequence));
  56          array_walk($sequence, create_function('&$val', '$val++;'));
  57  
  58          // If a question is lost, the corresponding index is null
  59          // so this null convention is used to test $question->options->questions
  60          // before using the values.
  61          // First all possible questions from sequence are nulled
  62          // then filled with the data if available in  $wrappedquestions.
  63          foreach ($sequence as $seq) {
  64              $question->options->questions[$seq] = '';
  65          }
  66  
  67          foreach ($wrappedquestions as $wrapped) {
  68              question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
  69              // For wrapped questions the maxgrade is always equal to the defaultmark,
  70              // there is no entry in the question_instances table for them.
  71              $wrapped->maxmark = $wrapped->defaultmark;
  72              $question->options->questions[$sequence[$wrapped->id]] = $wrapped;
  73          }
  74  
  75          $question->hints = $DB->get_records('question_hints',
  76                  array('questionid' => $question->id), 'id ASC');
  77  
  78          return true;
  79      }
  80  
  81      public function save_question_options($question) {
  82          global $DB;
  83          $result = new stdClass();
  84  
  85          // This function needs to be able to handle the case where the existing set of wrapped
  86          // questions does not match the new set of wrapped questions so that some need to be
  87          // created, some modified and some deleted.
  88          // Unfortunately the code currently simply overwrites existing ones in sequence. This
  89          // will make re-marking after a re-ordering of wrapped questions impossible and
  90          // will also create difficulties if questiontype specific tables reference the id.
  91  
  92          // First we get all the existing wrapped questions.
  93          if (!$oldwrappedids = $DB->get_field('question_multianswer', 'sequence',
  94                  array('question' => $question->id))) {
  95              $oldwrappedquestions = array();
  96          } else {
  97              $oldwrappedquestions = $DB->get_records_list('question', 'id',
  98                      explode(',', $oldwrappedids), 'id ASC');
  99          }
 100  
 101          $sequence = array();
 102          foreach ($question->options->questions as $wrapped) {
 103              if (!empty($wrapped)) {
 104                  // If we still have some old wrapped question ids, reuse the next of them.
 105  
 106                  if (is_array($oldwrappedquestions) &&
 107                          $oldwrappedquestion = array_shift($oldwrappedquestions)) {
 108                      $wrapped->id = $oldwrappedquestion->id;
 109                      if ($oldwrappedquestion->qtype != $wrapped->qtype) {
 110                          switch ($oldwrappedquestion->qtype) {
 111                              case 'multichoice':
 112                                  $DB->delete_records('qtype_multichoice_options',
 113                                          array('questionid' => $oldwrappedquestion->id));
 114                                  break;
 115                              case 'shortanswer':
 116                                  $DB->delete_records('qtype_shortanswer_options',
 117                                          array('questionid' => $oldwrappedquestion->id));
 118                                  break;
 119                              case 'numerical':
 120                                  $DB->delete_records('question_numerical',
 121                                          array('question' => $oldwrappedquestion->id));
 122                                  break;
 123                              default:
 124                                  throw new moodle_exception('qtypenotrecognized',
 125                                          'qtype_multianswer', '', $oldwrappedquestion->qtype);
 126                                  $wrapped->id = 0;
 127                          }
 128                      }
 129                  } else {
 130                      $wrapped->id = 0;
 131                  }
 132              }
 133              $wrapped->name = $question->name;
 134              $wrapped->parent = $question->id;
 135              $previousid = $wrapped->id;
 136              // Save_question strips this extra bit off the category again.
 137              $wrapped->category = $question->category . ',1';
 138              $wrapped = question_bank::get_qtype($wrapped->qtype)->save_question(
 139                      $wrapped, clone($wrapped));
 140              $sequence[] = $wrapped->id;
 141              if ($previousid != 0 && $previousid != $wrapped->id) {
 142                  // For some reasons a new question has been created
 143                  // so delete the old one.
 144                  question_delete_question($previousid);
 145              }
 146          }
 147  
 148          // Delete redundant wrapped questions.
 149          if (is_array($oldwrappedquestions) && count($oldwrappedquestions)) {
 150              foreach ($oldwrappedquestions as $oldwrappedquestion) {
 151                  question_delete_question($oldwrappedquestion->id);
 152              }
 153          }
 154  
 155          if (!empty($sequence)) {
 156              $multianswer = new stdClass();
 157              $multianswer->question = $question->id;
 158              $multianswer->sequence = implode(',', $sequence);
 159              if ($oldid = $DB->get_field('question_multianswer', 'id',
 160                      array('question' => $question->id))) {
 161                  $multianswer->id = $oldid;
 162                  $DB->update_record('question_multianswer', $multianswer);
 163              } else {
 164                  $DB->insert_record('question_multianswer', $multianswer);
 165              }
 166          }
 167  
 168          $this->save_hints($question, true);
 169      }
 170  
 171      public function save_question($authorizedquestion, $form) {
 172          $question = qtype_multianswer_extract_question($form->questiontext);
 173          if (isset($authorizedquestion->id)) {
 174              $question->id = $authorizedquestion->id;
 175          }
 176  
 177          $question->category = $authorizedquestion->category;
 178          $form->defaultmark = $question->defaultmark;
 179          $form->questiontext = $question->questiontext;
 180          $form->questiontextformat = 0;
 181          $form->options = clone($question->options);
 182          unset($question->options);
 183          return parent::save_question($question, $form);
 184      }
 185  
 186      protected function make_hint($hint) {
 187          return question_hint_with_parts::load_from_record($hint);
 188      }
 189  
 190      public function delete_question($questionid, $contextid) {
 191          global $DB;
 192          $DB->delete_records('question_multianswer', array('question' => $questionid));
 193  
 194          parent::delete_question($questionid, $contextid);
 195      }
 196  
 197      protected function initialise_question_instance(question_definition $question, $questiondata) {
 198          parent::initialise_question_instance($question, $questiondata);
 199  
 200          $bits = preg_split('/\{#(\d+)\}/', $question->questiontext,
 201                  null, PREG_SPLIT_DELIM_CAPTURE);
 202          $question->textfragments[0] = array_shift($bits);
 203          $i = 1;
 204          while (!empty($bits)) {
 205              $question->places[$i] = array_shift($bits);
 206              $question->textfragments[$i] = array_shift($bits);
 207              $i += 1;
 208          }
 209  
 210          foreach ($questiondata->options->questions as $key => $subqdata) {
 211              $subqdata->contextid = $questiondata->contextid;
 212              $subqdata->options->shuffleanswers = !isset($questiondata->options->shuffleanswers) ||
 213                      $questiondata->options->shuffleanswers;
 214              $question->subquestions[$key] = question_bank::make_question($subqdata);
 215              $question->subquestions[$key]->maxmark = $subqdata->defaultmark;
 216              if (isset($subqdata->options->layout)) {
 217                  $question->subquestions[$key]->layout = $subqdata->options->layout;
 218              }
 219          }
 220      }
 221  
 222      public function get_random_guess_score($questiondata) {
 223          $fractionsum = 0;
 224          $fractionmax = 0;
 225          foreach ($questiondata->options->questions as $key => $subqdata) {
 226              $fractionmax += $subqdata->defaultmark;
 227              $fractionsum += question_bank::get_qtype(
 228                      $subqdata->qtype)->get_random_guess_score($subqdata);
 229          }
 230          return $fractionsum / $fractionmax;
 231      }
 232  
 233      public function move_files($questionid, $oldcontextid, $newcontextid) {
 234          parent::move_files($questionid, $oldcontextid, $newcontextid);
 235          $this->move_files_in_hints($questionid, $oldcontextid, $newcontextid);
 236      }
 237  
 238      protected function delete_files($questionid, $contextid) {
 239          parent::delete_files($questionid, $contextid);
 240          $this->delete_files_in_hints($questionid, $contextid);
 241      }
 242  }
 243  
 244  
 245  // ANSWER_ALTERNATIVE regexes.
 246  define('ANSWER_ALTERNATIVE_FRACTION_REGEX',
 247         '=|%(-?[0-9]+)%');
 248  // For the syntax '(?<!' see http://www.perl.com/doc/manual/html/pod/perlre.html#item_C.
 249  define('ANSWER_ALTERNATIVE_ANSWER_REGEX',
 250          '.+?(?<!\\\\|&|&amp;)(?=[~#}]|$)');
 251  define('ANSWER_ALTERNATIVE_FEEDBACK_REGEX',
 252          '.*?(?<!\\\\)(?=[~}]|$)');
 253  define('ANSWER_ALTERNATIVE_REGEX',
 254         '(' . ANSWER_ALTERNATIVE_FRACTION_REGEX .')?' .
 255         '(' . ANSWER_ALTERNATIVE_ANSWER_REGEX . ')' .
 256         '(#(' . ANSWER_ALTERNATIVE_FEEDBACK_REGEX .'))?');
 257  
 258  // Parenthesis positions for ANSWER_ALTERNATIVE_REGEX.
 259  define('ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION', 2);
 260  define('ANSWER_ALTERNATIVE_REGEX_FRACTION', 1);
 261  define('ANSWER_ALTERNATIVE_REGEX_ANSWER', 3);
 262  define('ANSWER_ALTERNATIVE_REGEX_FEEDBACK', 5);
 263  
 264  // NUMBER_FORMATED_ALTERNATIVE_ANSWER_REGEX is used
 265  // for identifying numerical answers in ANSWER_ALTERNATIVE_REGEX_ANSWER.
 266  define('NUMBER_REGEX',
 267          '-?(([0-9]+[.,]?[0-9]*|[.,][0-9]+)([eE][-+]?[0-9]+)?)');
 268  define('NUMERICAL_ALTERNATIVE_REGEX',
 269          '^(' . NUMBER_REGEX . ')(:' . NUMBER_REGEX . ')?$');
 270  
 271  // Parenthesis positions for NUMERICAL_FORMATED_ALTERNATIVE_ANSWER_REGEX.
 272  define('NUMERICAL_CORRECT_ANSWER', 1);
 273  define('NUMERICAL_ABS_ERROR_MARGIN', 6);
 274  
 275  // Remaining ANSWER regexes.
 276  define('ANSWER_TYPE_DEF_REGEX',
 277          '(NUMERICAL|NM)|(MULTICHOICE|MC)|(MULTICHOICE_V|MCV)|(MULTICHOICE_H|MCH)|' .
 278                  '(SHORTANSWER|SA|MW)|(SHORTANSWER_C|SAC|MWC)');
 279  define('ANSWER_START_REGEX',
 280         '\{([0-9]*):(' . ANSWER_TYPE_DEF_REGEX . '):');
 281  
 282  define('ANSWER_REGEX',
 283          ANSWER_START_REGEX
 284          . '(' . ANSWER_ALTERNATIVE_REGEX
 285          . '(~'
 286          . ANSWER_ALTERNATIVE_REGEX
 287          . ')*)\}');
 288  
 289  // Parenthesis positions for singulars in ANSWER_REGEX.
 290  define('ANSWER_REGEX_NORM', 1);
 291  define('ANSWER_REGEX_ANSWER_TYPE_NUMERICAL', 3);
 292  define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE', 4);
 293  define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR', 5);
 294  define('ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL', 6);
 295  define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER', 7);
 296  define('ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C', 8);
 297  define('ANSWER_REGEX_ALTERNATIVES', 9);
 298  
 299  function qtype_multianswer_extract_question($text) {
 300      // Variable $text is an array [text][format][itemid].
 301      $question = new stdClass();
 302      $question->qtype = 'multianswer';
 303      $question->questiontext = $text;
 304      $question->generalfeedback['text'] = '';
 305      $question->generalfeedback['format'] = FORMAT_HTML;
 306      $question->generalfeedback['itemid'] = '';
 307  
 308      $question->options = new stdClass();
 309      $question->options->questions = array();
 310      $question->defaultmark = 0; // Will be increased for each answer norm.
 311  
 312      for ($positionkey = 1;
 313              preg_match('/'.ANSWER_REGEX.'/s', $question->questiontext['text'], $answerregs);
 314              ++$positionkey) {
 315          $wrapped = new stdClass();
 316          $wrapped->generalfeedback['text'] = '';
 317          $wrapped->generalfeedback['format'] = FORMAT_HTML;
 318          $wrapped->generalfeedback['itemid'] = '';
 319          if (isset($answerregs[ANSWER_REGEX_NORM])&& $answerregs[ANSWER_REGEX_NORM]!== '') {
 320              $wrapped->defaultmark = $answerregs[ANSWER_REGEX_NORM];
 321          } else {
 322              $wrapped->defaultmark = '1';
 323          }
 324          if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])) {
 325              $wrapped->qtype = 'numerical';
 326              $wrapped->multiplier = array();
 327              $wrapped->units      = array();
 328              $wrapped->instructions['text'] = '';
 329              $wrapped->instructions['format'] = FORMAT_HTML;
 330              $wrapped->instructions['itemid'] = '';
 331          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER])) {
 332              $wrapped->qtype = 'shortanswer';
 333              $wrapped->usecase = 0;
 334          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_SHORTANSWER_C])) {
 335              $wrapped->qtype = 'shortanswer';
 336              $wrapped->usecase = 1;
 337          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE])) {
 338              $wrapped->qtype = 'multichoice';
 339              $wrapped->single = 1;
 340              $wrapped->shuffleanswers = 1;
 341              $wrapped->answernumbering = 0;
 342              $wrapped->correctfeedback['text'] = '';
 343              $wrapped->correctfeedback['format'] = FORMAT_HTML;
 344              $wrapped->correctfeedback['itemid'] = '';
 345              $wrapped->partiallycorrectfeedback['text'] = '';
 346              $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
 347              $wrapped->partiallycorrectfeedback['itemid'] = '';
 348              $wrapped->incorrectfeedback['text'] = '';
 349              $wrapped->incorrectfeedback['format'] = FORMAT_HTML;
 350              $wrapped->incorrectfeedback['itemid'] = '';
 351              $wrapped->layout = qtype_multichoice_base::LAYOUT_DROPDOWN;
 352          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_REGULAR])) {
 353              $wrapped->qtype = 'multichoice';
 354              $wrapped->single = 1;
 355              $wrapped->shuffleanswers = 0;
 356              $wrapped->answernumbering = 0;
 357              $wrapped->correctfeedback['text'] = '';
 358              $wrapped->correctfeedback['format'] = FORMAT_HTML;
 359              $wrapped->correctfeedback['itemid'] = '';
 360              $wrapped->partiallycorrectfeedback['text'] = '';
 361              $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
 362              $wrapped->partiallycorrectfeedback['itemid'] = '';
 363              $wrapped->incorrectfeedback['text'] = '';
 364              $wrapped->incorrectfeedback['format'] = FORMAT_HTML;
 365              $wrapped->incorrectfeedback['itemid'] = '';
 366              $wrapped->layout = qtype_multichoice_base::LAYOUT_VERTICAL;
 367          } else if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_MULTICHOICE_HORIZONTAL])) {
 368              $wrapped->qtype = 'multichoice';
 369              $wrapped->single = 1;
 370              $wrapped->shuffleanswers = 0;
 371              $wrapped->answernumbering = 0;
 372              $wrapped->correctfeedback['text'] = '';
 373              $wrapped->correctfeedback['format'] = FORMAT_HTML;
 374              $wrapped->correctfeedback['itemid'] = '';
 375              $wrapped->partiallycorrectfeedback['text'] = '';
 376              $wrapped->partiallycorrectfeedback['format'] = FORMAT_HTML;
 377              $wrapped->partiallycorrectfeedback['itemid'] = '';
 378              $wrapped->incorrectfeedback['text'] = '';
 379              $wrapped->incorrectfeedback['format'] = FORMAT_HTML;
 380              $wrapped->incorrectfeedback['itemid'] = '';
 381              $wrapped->layout = qtype_multichoice_base::LAYOUT_HORIZONTAL;
 382          } else {
 383              print_error('unknownquestiontype', 'question', '', $answerregs[2]);
 384              return false;
 385          }
 386  
 387          // Each $wrapped simulates a $form that can be processed by the
 388          // respective save_question and save_question_options methods of the
 389          // wrapped questiontypes.
 390          $wrapped->answer   = array();
 391          $wrapped->fraction = array();
 392          $wrapped->feedback = array();
 393          $wrapped->questiontext['text'] = $answerregs[0];
 394          $wrapped->questiontext['format'] = FORMAT_HTML;
 395          $wrapped->questiontext['itemid'] = '';
 396          $answerindex = 0;
 397  
 398          $remainingalts = $answerregs[ANSWER_REGEX_ALTERNATIVES];
 399          while (preg_match('/~?'.ANSWER_ALTERNATIVE_REGEX.'/s', $remainingalts, $altregs)) {
 400              if ('=' == $altregs[ANSWER_ALTERNATIVE_REGEX_FRACTION]) {
 401                  $wrapped->fraction["{$answerindex}"] = '1';
 402              } else if ($percentile = $altregs[ANSWER_ALTERNATIVE_REGEX_PERCENTILE_FRACTION]) {
 403                  $wrapped->fraction["{$answerindex}"] = .01 * $percentile;
 404              } else {
 405                  $wrapped->fraction["{$answerindex}"] = '0';
 406              }
 407              if (isset($altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK])) {
 408                  $feedback = html_entity_decode(
 409                          $altregs[ANSWER_ALTERNATIVE_REGEX_FEEDBACK], ENT_QUOTES, 'UTF-8');
 410                  $feedback = str_replace('\}', '}', $feedback);
 411                  $wrapped->feedback["{$answerindex}"]['text'] = str_replace('\#', '#', $feedback);
 412                  $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
 413                  $wrapped->feedback["{$answerindex}"]['itemid'] = '';
 414              } else {
 415                  $wrapped->feedback["{$answerindex}"]['text'] = '';
 416                  $wrapped->feedback["{$answerindex}"]['format'] = FORMAT_HTML;
 417                  $wrapped->feedback["{$answerindex}"]['itemid'] = '';
 418  
 419              }
 420              if (!empty($answerregs[ANSWER_REGEX_ANSWER_TYPE_NUMERICAL])
 421                      && preg_match('~'.NUMERICAL_ALTERNATIVE_REGEX.'~s',
 422                              $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], $numregs)) {
 423                  $wrapped->answer[] = $numregs[NUMERICAL_CORRECT_ANSWER];
 424                  if (array_key_exists(NUMERICAL_ABS_ERROR_MARGIN, $numregs)) {
 425                      $wrapped->tolerance["{$answerindex}"] =
 426                      $numregs[NUMERICAL_ABS_ERROR_MARGIN];
 427                  } else {
 428                      $wrapped->tolerance["{$answerindex}"] = 0;
 429                  }
 430              } else { // Tolerance can stay undefined for non numerical questions.
 431                  // Undo quoting done by the HTML editor.
 432                  $answer = html_entity_decode(
 433                          $altregs[ANSWER_ALTERNATIVE_REGEX_ANSWER], ENT_QUOTES, 'UTF-8');
 434                  $answer = str_replace('\}', '}', $answer);
 435                  $wrapped->answer["{$answerindex}"] = str_replace('\#', '#', $answer);
 436                  if ($wrapped->qtype == 'multichoice') {
 437                      $wrapped->answer["{$answerindex}"] = array(
 438                              'text' => $wrapped->answer["{$answerindex}"],
 439                              'format' => FORMAT_HTML,
 440                              'itemid' => '');
 441                  }
 442              }
 443              $tmp = explode($altregs[0], $remainingalts, 2);
 444              $remainingalts = $tmp[1];
 445              $answerindex++;
 446          }
 447  
 448          $question->defaultmark += $wrapped->defaultmark;
 449          $question->options->questions[$positionkey] = clone($wrapped);
 450          $question->questiontext['text'] = implode("{#$positionkey}",
 451                      explode($answerregs[0], $question->questiontext['text'], 2));
 452      }
 453      return $question;
 454  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1