[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/question/type/ -> questiontypebase.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   * The default questiontype class.
  19   *
  20   * @package    moodlecore
  21   * @subpackage questiontypes
  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/engine/lib.php');
  30  
  31  
  32  /**
  33   * This is the base class for Moodle question types.
  34   *
  35   * There are detailed comments on each method, explaining what the method is
  36   * for, and the circumstances under which you might need to override it.
  37   *
  38   * Note: the questiontype API should NOT be considered stable yet. Very few
  39   * question types have been produced yet, so we do not yet know all the places
  40   * where the current API is insufficient. I would rather learn from the
  41   * experiences of the first few question type implementors, and improve the
  42   * interface to meet their needs, rather the freeze the API prematurely and
  43   * condem everyone to working round a clunky interface for ever afterwards.
  44   *
  45   * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
  46   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  47   */
  48  class question_type {
  49      protected $fileoptions = array(
  50          'subdirs' => true,
  51          'maxfiles' => -1,
  52          'maxbytes' => 0,
  53      );
  54  
  55      public function __construct() {
  56      }
  57  
  58      /**
  59       * @return string the name of this question type.
  60       */
  61      public function name() {
  62          return substr(get_class($this), 6);
  63      }
  64  
  65      /**
  66       * @return string the full frankenstyle name for this plugin.
  67       */
  68      public function plugin_name() {
  69          return get_class($this);
  70      }
  71  
  72      /**
  73       * @return string the name of this question type in the user's language.
  74       * You should not need to override this method, the default behaviour should be fine.
  75       */
  76      public function local_name() {
  77          return get_string('pluginname', $this->plugin_name());
  78      }
  79  
  80      /**
  81       * The name this question should appear as in the create new question
  82       * dropdown. Override this method to return false if you don't want your
  83       * question type to be createable, for example if it is an abstract base type,
  84       * otherwise, you should not need to override this method.
  85       *
  86       * @return mixed the desired string, or false to hide this question type in the menu.
  87       */
  88      public function menu_name() {
  89          return $this->local_name();
  90      }
  91  
  92      /**
  93       * @return bool override this to return false if this is not really a
  94       *      question type, for example the description question type is not
  95       *      really a question type.
  96       */
  97      public function is_real_question_type() {
  98          return true;
  99      }
 100  
 101      /**
 102       * @return bool true if this question type sometimes requires manual grading.
 103       */
 104      public function is_manual_graded() {
 105          return false;
 106      }
 107  
 108      /**
 109       * @param object $question a question of this type.
 110       * @param string $otherquestionsinuse comma-separate list of other question ids in this attempt.
 111       * @return bool true if a particular instance of this question requires manual grading.
 112       */
 113      public function is_question_manual_graded($question, $otherquestionsinuse) {
 114          return $this->is_manual_graded();
 115      }
 116  
 117      /**
 118       * @return bool true if this question type can be used by the random question type.
 119       */
 120      public function is_usable_by_random() {
 121          return true;
 122      }
 123  
 124      /**
 125       * Whether this question type can perform a frequency analysis of student
 126       * responses.
 127       *
 128       * If this method returns true, you must implement the get_possible_responses
 129       * method, and the question_definition class must implement the
 130       * classify_response method.
 131       *
 132       * @return bool whether this report can analyse all the student responses
 133       * for things like the quiz statistics report.
 134       */
 135      public function can_analyse_responses() {
 136          // This works in most cases.
 137          return !$this->is_manual_graded();
 138      }
 139  
 140      /**
 141       * @return whether the question_answers.answer field needs to have
 142       * restore_decode_content_links_worker called on it.
 143       */
 144      public function has_html_answers() {
 145          return false;
 146      }
 147  
 148      /**
 149       * If your question type has a table that extends the question table, and
 150       * you want the base class to automatically save, backup and restore the extra fields,
 151       * override this method to return an array wherer the first element is the table name,
 152       * and the subsequent entries are the column names (apart from id and questionid).
 153       *
 154       * @return mixed array as above, or null to tell the base class to do nothing.
 155       */
 156      public function extra_question_fields() {
 157          return null;
 158      }
 159  
 160      /**
 161       * If you use extra_question_fields, overload this function to return question id field name
 162       *  in case you table use another name for this column
 163       */
 164      public function questionid_column_name() {
 165          return 'questionid';
 166      }
 167  
 168      /**
 169       * If your question type has a table that extends the question_answers table,
 170       * make this method return an array wherer the first element is the table name,
 171       * and the subsequent entries are the column names (apart from id and answerid).
 172       *
 173       * @return mixed array as above, or null to tell the base class to do nothing.
 174       */
 175      public function extra_answer_fields() {
 176          return null;
 177      }
 178  
 179      /**
 180       * If the quetsion type uses files in responses, then this method should
 181       * return an array of all the response variables that might have corresponding
 182       * files. For example, the essay qtype returns array('attachments', 'answers').
 183       *
 184       * @return array response variable names that may have associated files.
 185       */
 186      public function response_file_areas() {
 187          return array();
 188      }
 189  
 190      /**
 191       * Return an instance of the question editing form definition. This looks for a
 192       * class called edit_{$this->name()}_question_form in the file
 193       * question/type/{$this->name()}/edit_{$this->name()}_question_form.php
 194       * and if it exists returns an instance of it.
 195       *
 196       * @param string $submiturl passed on to the constructor call.
 197       * @return object an instance of the form definition, or null if one could not be found.
 198       */
 199      public function create_editing_form($submiturl, $question, $category,
 200              $contexts, $formeditable) {
 201          global $CFG;
 202          require_once($CFG->dirroot . '/question/type/edit_question_form.php');
 203          $definitionfile = $CFG->dirroot . '/question/type/' . $this->name() .
 204                  '/edit_' . $this->name() . '_form.php';
 205          if (!is_readable($definitionfile) || !is_file($definitionfile)) {
 206              throw new coding_exception($this->plugin_name() .
 207                      ' is missing the definition of its editing formin file ' .
 208                      $definitionfile . '.');
 209          }
 210          require_once($definitionfile);
 211          $classname = $this->plugin_name() . '_edit_form';
 212          if (!class_exists($classname)) {
 213              throw new coding_exception($this->plugin_name() .
 214                      ' does not define the class ' . $this->plugin_name() .
 215                      '_edit_form.');
 216          }
 217          return new $classname($submiturl, $question, $category, $contexts, $formeditable);
 218      }
 219  
 220      /**
 221       * @return string the full path of the folder this plugin's files live in.
 222       */
 223      public function plugin_dir() {
 224          global $CFG;
 225          return $CFG->dirroot . '/question/type/' . $this->name();
 226      }
 227  
 228      /**
 229       * @return string the URL of the folder this plugin's files live in.
 230       */
 231      public function plugin_baseurl() {
 232          global $CFG;
 233          return $CFG->wwwroot . '/question/type/' . $this->name();
 234      }
 235  
 236      /**
 237       * This method should be overriden if you want to include a special heading or some other
 238       * html on a question editing page besides the question editing form.
 239       *
 240       * @param question_edit_form $mform a child of question_edit_form
 241       * @param object $question
 242       * @param string $wizardnow is '' for first page.
 243       */
 244      public function display_question_editing_page($mform, $question, $wizardnow) {
 245          global $OUTPUT;
 246          $heading = $this->get_heading(empty($question->id));
 247          echo $OUTPUT->heading_with_help($heading, 'pluginname', $this->plugin_name());
 248          $mform->display();
 249      }
 250  
 251      /**
 252       * Method called by display_question_editing_page and by question.php to get
 253       * heading for breadcrumbs.
 254       *
 255       * @return string the heading
 256       */
 257      public function get_heading($adding = false) {
 258          if ($adding) {
 259              $string = 'pluginnameadding';
 260          } else {
 261              $string = 'pluginnameediting';
 262          }
 263          return get_string($string, $this->plugin_name());
 264      }
 265  
 266      /**
 267       * Set any missing settings for this question to the default values. This is
 268       * called before displaying the question editing form.
 269       *
 270       * @param object $questiondata the question data, loaded from the databsae,
 271       *      or more likely a newly created question object that is only partially
 272       *      initialised.
 273       */
 274      public function set_default_options($questiondata) {
 275      }
 276  
 277      /**
 278       * Saves (creates or updates) a question.
 279       *
 280       * Given some question info and some data about the answers
 281       * this function parses, organises and saves the question
 282       * It is used by {@link question.php} when saving new data from
 283       * a form, and also by {@link import.php} when importing questions
 284       * This function in turn calls {@link save_question_options}
 285       * to save question-type specific data.
 286       *
 287       * Whether we are saving a new question or updating an existing one can be
 288       * determined by testing !empty($question->id). If it is not empty, we are updating.
 289       *
 290       * The question will be saved in category $form->category.
 291       *
 292       * @param object $question the question object which should be updated. For a
 293       *      new question will be mostly empty.
 294       * @param object $form the object containing the information to save, as if
 295       *      from the question editing form.
 296       * @param object $course not really used any more.
 297       * @return object On success, return the new question object. On failure,
 298       *       return an object as follows. If the error object has an errors field,
 299       *       display that as an error message. Otherwise, the editing form will be
 300       *       redisplayed with validation errors, from validation_errors field, which
 301       *       is itself an object, shown next to the form fields. (I don't think this
 302       *       is accurate any more.)
 303       */
 304      public function save_question($question, $form) {
 305          global $USER, $DB, $OUTPUT;
 306  
 307          list($question->category) = explode(',', $form->category);
 308          $context = $this->get_context_by_category_id($question->category);
 309  
 310          // This default implementation is suitable for most
 311          // question types.
 312  
 313          // First, save the basic question itself.
 314          $question->name = trim($form->name);
 315          $question->parent = isset($form->parent) ? $form->parent : 0;
 316          $question->length = $this->actual_number_of_questions($question);
 317          $question->penalty = isset($form->penalty) ? $form->penalty : 0;
 318  
 319          // The trim call below has the effect of casting any strange values received,
 320          // like null or false, to an appropriate string, so we only need to test for
 321          // missing values. Be careful not to break the value '0' here.
 322          if (!isset($form->questiontext['text'])) {
 323              $question->questiontext = '';
 324          } else {
 325              $question->questiontext = trim($form->questiontext['text']);
 326          }
 327          $question->questiontextformat = !empty($form->questiontext['format']) ?
 328                  $form->questiontext['format'] : 0;
 329  
 330          if (empty($form->generalfeedback['text'])) {
 331              $question->generalfeedback = '';
 332          } else {
 333              $question->generalfeedback = trim($form->generalfeedback['text']);
 334          }
 335          $question->generalfeedbackformat = !empty($form->generalfeedback['format']) ?
 336                  $form->generalfeedback['format'] : 0;
 337  
 338          if ($question->name === '') {
 339              $question->name = shorten_text(strip_tags($form->questiontext['text']), 15);
 340              if ($question->name === '') {
 341                  $question->name = '-';
 342              }
 343          }
 344  
 345          if ($question->penalty > 1 or $question->penalty < 0) {
 346              $question->errors['penalty'] = get_string('invalidpenalty', 'question');
 347          }
 348  
 349          if (isset($form->defaultmark)) {
 350              $question->defaultmark = $form->defaultmark;
 351          }
 352  
 353          // If the question is new, create it.
 354          if (empty($question->id)) {
 355              // Set the unique code.
 356              $question->stamp = make_unique_id_code();
 357              $question->createdby = $USER->id;
 358              $question->timecreated = time();
 359              $question->id = $DB->insert_record('question', $question);
 360          }
 361  
 362          // Now, whether we are updating a existing question, or creating a new
 363          // one, we have to do the files processing and update the record.
 364          // Question already exists, update.
 365          $question->modifiedby = $USER->id;
 366          $question->timemodified = time();
 367  
 368          if (!empty($question->questiontext) && !empty($form->questiontext['itemid'])) {
 369              $question->questiontext = file_save_draft_area_files($form->questiontext['itemid'],
 370                      $context->id, 'question', 'questiontext', (int)$question->id,
 371                      $this->fileoptions, $question->questiontext);
 372          }
 373          if (!empty($question->generalfeedback) && !empty($form->generalfeedback['itemid'])) {
 374              $question->generalfeedback = file_save_draft_area_files(
 375                      $form->generalfeedback['itemid'], $context->id,
 376                      'question', 'generalfeedback', (int)$question->id,
 377                      $this->fileoptions, $question->generalfeedback);
 378          }
 379          $DB->update_record('question', $question);
 380  
 381          // Now to save all the answers and type-specific options.
 382          $form->id = $question->id;
 383          $form->qtype = $question->qtype;
 384          $form->category = $question->category;
 385          $form->questiontext = $question->questiontext;
 386          $form->questiontextformat = $question->questiontextformat;
 387          // Current context.
 388          $form->context = $context;
 389  
 390          $result = $this->save_question_options($form);
 391  
 392          if (!empty($result->error)) {
 393              print_error($result->error);
 394          }
 395  
 396          if (!empty($result->notice)) {
 397              notice($result->notice, "question.php?id={$question->id}");
 398          }
 399  
 400          if (!empty($result->noticeyesno)) {
 401              throw new coding_exception(
 402                      '$result->noticeyesno no longer supported in save_question.');
 403          }
 404  
 405          // Give the question a unique version stamp determined by question_hash().
 406          $DB->set_field('question', 'version', question_hash($question),
 407                  array('id' => $question->id));
 408  
 409          return $question;
 410      }
 411  
 412      /**
 413       * Saves question-type specific options
 414       *
 415       * This is called by {@link save_question()} to save the question-type specific data
 416       * @return object $result->error or $result->notice
 417       * @param object $question  This holds the information from the editing form,
 418       *      it is not a standard question object.
 419       */
 420      public function save_question_options($question) {
 421          global $DB;
 422          $extraquestionfields = $this->extra_question_fields();
 423  
 424          if (is_array($extraquestionfields)) {
 425              $question_extension_table = array_shift($extraquestionfields);
 426  
 427              $function = 'update_record';
 428              $questionidcolname = $this->questionid_column_name();
 429              $options = $DB->get_record($question_extension_table,
 430                      array($questionidcolname => $question->id));
 431              if (!$options) {
 432                  $function = 'insert_record';
 433                  $options = new stdClass();
 434                  $options->$questionidcolname = $question->id;
 435              }
 436              foreach ($extraquestionfields as $field) {
 437                  if (property_exists($question, $field)) {
 438                      $options->$field = $question->$field;
 439                  }
 440              }
 441  
 442              $DB->{$function}($question_extension_table, $options);
 443          }
 444      }
 445  
 446      /**
 447       * Save the answers, with any extra data.
 448       *
 449       * Questions that use answers will call it from {@link save_question_options()}.
 450       * @param object $question  This holds the information from the editing form,
 451       *      it is not a standard question object.
 452       * @return object $result->error or $result->notice
 453       */
 454      public function save_question_answers($question) {
 455          global $DB;
 456  
 457          $context = $question->context;
 458          $oldanswers = $DB->get_records('question_answers',
 459                  array('question' => $question->id), 'id ASC');
 460  
 461          // We need separate arrays for answers and extra answer data, so no JOINS there.
 462          $extraanswerfields = $this->extra_answer_fields();
 463          $isextraanswerfields = is_array($extraanswerfields);
 464          $extraanswertable = '';
 465          $oldanswerextras = array();
 466          if ($isextraanswerfields) {
 467              $extraanswertable = array_shift($extraanswerfields);
 468              if (!empty($oldanswers)) {
 469                  $oldanswerextras = $DB->get_records_sql("SELECT * FROM {{$extraanswertable}} WHERE " .
 470                      'answerid IN (SELECT id FROM {question_answers} WHERE question = ' . $question->id . ')' );
 471              }
 472          }
 473  
 474          // Insert all the new answers.
 475          foreach ($question->answer as $key => $answerdata) {
 476              // Check for, and ignore, completely blank answer from the form.
 477              if ($this->is_answer_empty($question, $key)) {
 478                  continue;
 479              }
 480  
 481              // Update an existing answer if possible.
 482              $answer = array_shift($oldanswers);
 483              if (!$answer) {
 484                  $answer = new stdClass();
 485                  $answer->question = $question->id;
 486                  $answer->answer = '';
 487                  $answer->feedback = '';
 488                  $answer->id = $DB->insert_record('question_answers', $answer);
 489              }
 490  
 491              $answer = $this->fill_answer_fields($answer, $question, $key, $context);
 492              $DB->update_record('question_answers', $answer);
 493  
 494              if ($isextraanswerfields) {
 495                  // Check, if this answer contains some extra field data.
 496                  if ($this->is_extra_answer_fields_empty($question, $key)) {
 497                      continue;
 498                  }
 499  
 500                  $answerextra = array_shift($oldanswerextras);
 501                  if (!$answerextra) {
 502                      $answerextra = new stdClass();
 503                      $answerextra->answerid = $answer->id;
 504                      // Avoid looking for correct default for any possible DB field type
 505                      // by setting real values.
 506                      $answerextra = $this->fill_extra_answer_fields($answerextra, $question, $key, $context, $extraanswerfields);
 507                      $answerextra->id = $DB->insert_record($extraanswertable, $answerextra);
 508                  } else {
 509                      // Update answerid, as record may be reused from another answer.
 510                      $answerextra->answerid = $answer->id;
 511                      $answerextra = $this->fill_extra_answer_fields($answerextra, $question, $key, $context, $extraanswerfields);
 512                      $DB->update_record($extraanswertable, $answerextra);
 513                  }
 514              }
 515          }
 516  
 517          if ($isextraanswerfields) {
 518              // Delete any left over extra answer fields records.
 519              $oldanswerextraids = array();
 520              foreach ($oldanswerextras as $oldextra) {
 521                  $oldanswerextraids[] = $oldextra->id;
 522              }
 523              $DB->delete_records_list($extraanswertable, 'id', $oldanswerextraids);
 524          }
 525  
 526          // Delete any left over old answer records.
 527          $fs = get_file_storage();
 528          foreach ($oldanswers as $oldanswer) {
 529              $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id);
 530              $DB->delete_records('question_answers', array('id' => $oldanswer->id));
 531          }
 532      }
 533  
 534      /**
 535       * Returns true is answer with the $key is empty in the question data and should not be saved in DB.
 536       *
 537       * The questions using question_answers table may want to overload this. Default code will work
 538       * for shortanswer and similar question types.
 539       * @param object $questiondata This holds the information from the question editing form or import.
 540       * @param int $key A key of the answer in question.
 541       * @return bool True if answer shouldn't be saved in DB.
 542       */
 543      protected function is_answer_empty($questiondata, $key) {
 544          return trim($questiondata->answer[$key]) == '' && $questiondata->fraction[$key] == 0 &&
 545                      html_is_blank($questiondata->feedback[$key]['text']);
 546      }
 547  
 548      /**
 549       * Return $answer, filling necessary fields for the question_answers table.
 550       *
 551       * The questions using question_answers table may want to overload this. Default code will work
 552       * for shortanswer and similar question types.
 553       * @param stdClass $answer Object to save data.
 554       * @param object $questiondata This holds the information from the question editing form or import.
 555       * @param int $key A key of the answer in question.
 556       * @param object $context needed for working with files.
 557       * @return $answer answer with filled data.
 558       */
 559      protected function fill_answer_fields($answer, $questiondata, $key, $context) {
 560          $answer->answer   = $questiondata->answer[$key];
 561          $answer->fraction = $questiondata->fraction[$key];
 562          $answer->feedback = $this->import_or_save_files($questiondata->feedback[$key],
 563                  $context, 'question', 'answerfeedback', $answer->id);
 564          $answer->feedbackformat = $questiondata->feedback[$key]['format'];
 565          return $answer;
 566      }
 567  
 568      /**
 569       * Returns true if extra answer fields for answer with the $key is empty
 570       * in the question data and should not be saved in DB.
 571       *
 572       * Questions where extra answer fields are optional will want to overload this.
 573       * @param object $questiondata This holds the information from the question editing form or import.
 574       * @param int $key A key of the answer in question.
 575       * @return bool True if extra answer data shouldn't be saved in DB.
 576       */
 577      protected function is_extra_answer_fields_empty($questiondata, $key) {
 578          // No extra answer data in base class.
 579          return true;
 580      }
 581  
 582      /**
 583       * Return $answerextra, filling necessary fields for the extra answer fields table.
 584       *
 585       * The questions may want to overload it to save files or do other data processing.
 586       * @param stdClass $answerextra Object to save data.
 587       * @param object $questiondata This holds the information from the question editing form or import.
 588       * @param int $key A key of the answer in question.
 589       * @param object $context needed for working with files.
 590       * @param array $extraanswerfields extra answer fields (without table name).
 591       * @return $answer answerextra with filled data.
 592       */
 593      protected function fill_extra_answer_fields($answerextra, $questiondata, $key, $context, $extraanswerfields) {
 594          foreach ($extraanswerfields as $field) {
 595              // The $questiondata->$field[$key] won't work in PHP, break it down to two strings of code.
 596              $fieldarray = $questiondata->$field;
 597              $answerextra->$field = $fieldarray[$key];
 598          }
 599          return $answerextra;
 600      }
 601  
 602      public function save_hints($formdata, $withparts = false) {
 603          global $DB;
 604          $context = $formdata->context;
 605  
 606          $oldhints = $DB->get_records('question_hints',
 607                  array('questionid' => $formdata->id), 'id ASC');
 608  
 609  
 610          $numhints = $this->count_hints_on_form($formdata, $withparts);
 611  
 612          for ($i = 0; $i < $numhints; $i += 1) {
 613              if (html_is_blank($formdata->hint[$i]['text'])) {
 614                  $formdata->hint[$i]['text'] = '';
 615              }
 616  
 617              if ($withparts) {
 618                  $clearwrong = !empty($formdata->hintclearwrong[$i]);
 619                  $shownumcorrect = !empty($formdata->hintshownumcorrect[$i]);
 620              }
 621  
 622              if ($this->is_hint_empty_in_form_data($formdata, $i, $withparts)) {
 623                  continue;
 624              }
 625  
 626              // Update an existing hint if possible.
 627              $hint = array_shift($oldhints);
 628              if (!$hint) {
 629                  $hint = new stdClass();
 630                  $hint->questionid = $formdata->id;
 631                  $hint->hint = '';
 632                  $hint->id = $DB->insert_record('question_hints', $hint);
 633              }
 634  
 635              $hint->hint = $this->import_or_save_files($formdata->hint[$i],
 636                      $context, 'question', 'hint', $hint->id);
 637              $hint->hintformat = $formdata->hint[$i]['format'];
 638              if ($withparts) {
 639                  $hint->clearwrong = $clearwrong;
 640                  $hint->shownumcorrect = $shownumcorrect;
 641              }
 642              $hint->options = $this->save_hint_options($formdata, $i, $withparts);
 643              $DB->update_record('question_hints', $hint);
 644          }
 645  
 646          // Delete any remaining old hints.
 647          $fs = get_file_storage();
 648          foreach ($oldhints as $oldhint) {
 649              $fs->delete_area_files($context->id, 'question', 'hint', $oldhint->id);
 650              $DB->delete_records('question_hints', array('id' => $oldhint->id));
 651          }
 652      }
 653  
 654      /**
 655       * Count number of hints on the form.
 656       * Overload if you use custom hint controls.
 657       * @param object $formdata the data from the form.
 658       * @param bool $withparts whether to take into account clearwrong and shownumcorrect options.
 659       * @return int count of hints on the form.
 660       */
 661      protected function count_hints_on_form($formdata, $withparts) {
 662          if (!empty($formdata->hint)) {
 663              $numhints = max(array_keys($formdata->hint)) + 1;
 664          } else {
 665              $numhints = 0;
 666          }
 667  
 668          if ($withparts) {
 669              if (!empty($formdata->hintclearwrong)) {
 670                  $numclears = max(array_keys($formdata->hintclearwrong)) + 1;
 671              } else {
 672                  $numclears = 0;
 673              }
 674              if (!empty($formdata->hintshownumcorrect)) {
 675                  $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1;
 676              } else {
 677                  $numshows = 0;
 678              }
 679              $numhints = max($numhints, $numclears, $numshows);
 680          }
 681          return $numhints;
 682      }
 683  
 684      /**
 685       * Determine if the hint with specified number is not empty and should be saved.
 686       * Overload if you use custom hint controls.
 687       * @param object $formdata the data from the form.
 688       * @param int $number number of hint under question.
 689       * @param bool $withparts whether to take into account clearwrong and shownumcorrect options.
 690       * @return bool is this particular hint data empty.
 691       */
 692      protected function is_hint_empty_in_form_data($formdata, $number, $withparts) {
 693          if ($withparts) {
 694              return empty($formdata->hint[$number]['text']) && empty($formdata->hintclearwrong[$number]) &&
 695                      empty($formdata->hintshownumcorrect[$number]);
 696          } else {
 697              return  empty($formdata->hint[$number]['text']);
 698          }
 699      }
 700  
 701      /**
 702       * Save additional question type data into the hint optional field.
 703       * Overload if you use custom hint information.
 704       * @param object $formdata the data from the form.
 705       * @param int $number number of hint to get options from.
 706       * @param bool $withparts whether question have parts.
 707       * @return string value to save into the options field of question_hints table.
 708       */
 709      protected function save_hint_options($formdata, $number, $withparts) {
 710          return null;    // By default, options field is unused.
 711      }
 712  
 713      /**
 714       * Can be used to {@link save_question_options()} to transfer the combined
 715       * feedback fields from $formdata to $options.
 716       * @param object $options the $question->options object being built.
 717       * @param object $formdata the data from the form.
 718       * @param object $context the context the quetsion is being saved into.
 719       * @param bool $withparts whether $options->shownumcorrect should be set.
 720       */
 721      protected function save_combined_feedback_helper($options, $formdata,
 722              $context, $withparts = false) {
 723          $options->correctfeedback = $this->import_or_save_files($formdata->correctfeedback,
 724                  $context, 'question', 'correctfeedback', $formdata->id);
 725          $options->correctfeedbackformat = $formdata->correctfeedback['format'];
 726  
 727          $options->partiallycorrectfeedback = $this->import_or_save_files(
 728                  $formdata->partiallycorrectfeedback,
 729                  $context, 'question', 'partiallycorrectfeedback', $formdata->id);
 730          $options->partiallycorrectfeedbackformat = $formdata->partiallycorrectfeedback['format'];
 731  
 732          $options->incorrectfeedback = $this->import_or_save_files($formdata->incorrectfeedback,
 733                  $context, 'question', 'incorrectfeedback', $formdata->id);
 734          $options->incorrectfeedbackformat = $formdata->incorrectfeedback['format'];
 735  
 736          if ($withparts) {
 737              $options->shownumcorrect = !empty($formdata->shownumcorrect);
 738          }
 739  
 740          return $options;
 741      }
 742  
 743      /**
 744       * Loads the question type specific options for the question.
 745       *
 746       * This function loads any question type specific options for the
 747       * question from the database into the question object. This information
 748       * is placed in the $question->options field. A question type is
 749       * free, however, to decide on a internal structure of the options field.
 750       * @return bool            Indicates success or failure.
 751       * @param object $question The question object for the question. This object
 752       *                         should be updated to include the question type
 753       *                         specific information (it is passed by reference).
 754       */
 755      public function get_question_options($question) {
 756          global $CFG, $DB, $OUTPUT;
 757  
 758          if (!isset($question->options)) {
 759              $question->options = new stdClass();
 760          }
 761  
 762          $extraquestionfields = $this->extra_question_fields();
 763          if (is_array($extraquestionfields)) {
 764              $question_extension_table = array_shift($extraquestionfields);
 765              $extra_data = $DB->get_record($question_extension_table,
 766                      array($this->questionid_column_name() => $question->id),
 767                      implode(', ', $extraquestionfields));
 768              if ($extra_data) {
 769                  foreach ($extraquestionfields as $field) {
 770                      $question->options->$field = $extra_data->$field;
 771                  }
 772              } else {
 773                  echo $OUTPUT->notification('Failed to load question options from the table ' .
 774                          $question_extension_table . ' for questionid ' . $question->id);
 775                  return false;
 776              }
 777          }
 778  
 779          $extraanswerfields = $this->extra_answer_fields();
 780          if (is_array($extraanswerfields)) {
 781              $answerextensiontable = array_shift($extraanswerfields);
 782              // Use LEFT JOIN in case not every answer has extra data.
 783              $question->options->answers = $DB->get_records_sql("
 784                      SELECT qa.*, qax." . implode(', qax.', $extraanswerfields) . '
 785                      FROM {question_answers} qa ' . "
 786                      LEFT JOIN {{$answerextensiontable}} qax ON qa.id = qax.answerid
 787                      WHERE qa.question = ?
 788                      ORDER BY qa.id", array($question->id));
 789              if (!$question->options->answers) {
 790                  echo $OUTPUT->notification('Failed to load question answers from the table ' .
 791                          $answerextensiontable . 'for questionid ' . $question->id);
 792                  return false;
 793              }
 794          } else {
 795              // Don't check for success or failure because some question types do
 796              // not use the answers table.
 797              $question->options->answers = $DB->get_records('question_answers',
 798                      array('question' => $question->id), 'id ASC');
 799          }
 800  
 801          $question->hints = $DB->get_records('question_hints',
 802                  array('questionid' => $question->id), 'id ASC');
 803  
 804          return true;
 805      }
 806  
 807      /**
 808       * Create an appropriate question_definition for the question of this type
 809       * using data loaded from the database.
 810       * @param object $questiondata the question data loaded from the database.
 811       * @return question_definition the corresponding question_definition.
 812       */
 813      public function make_question($questiondata) {
 814          $question = $this->make_question_instance($questiondata);
 815          $this->initialise_question_instance($question, $questiondata);
 816          return $question;
 817      }
 818  
 819      /**
 820       * Create an appropriate question_definition for the question of this type
 821       * using data loaded from the database.
 822       * @param object $questiondata the question data loaded from the database.
 823       * @return question_definition an instance of the appropriate question_definition subclass.
 824       *      Still needs to be initialised.
 825       */
 826      protected function make_question_instance($questiondata) {
 827          question_bank::load_question_definition_classes($this->name());
 828          $class = 'qtype_' . $this->name() . '_question';
 829          return new $class();
 830      }
 831  
 832      /**
 833       * Initialise the common question_definition fields.
 834       * @param question_definition $question the question_definition we are creating.
 835       * @param object $questiondata the question data loaded from the database.
 836       */
 837      protected function initialise_question_instance(question_definition $question, $questiondata) {
 838          $question->id = $questiondata->id;
 839          $question->category = $questiondata->category;
 840          $question->contextid = $questiondata->contextid;
 841          $question->parent = $questiondata->parent;
 842          $question->qtype = $this;
 843          $question->name = $questiondata->name;
 844          $question->questiontext = $questiondata->questiontext;
 845          $question->questiontextformat = $questiondata->questiontextformat;
 846          $question->generalfeedback = $questiondata->generalfeedback;
 847          $question->generalfeedbackformat = $questiondata->generalfeedbackformat;
 848          $question->defaultmark = $questiondata->defaultmark + 0;
 849          $question->length = $questiondata->length;
 850          $question->penalty = $questiondata->penalty;
 851          $question->stamp = $questiondata->stamp;
 852          $question->version = $questiondata->version;
 853          $question->hidden = $questiondata->hidden;
 854          $question->timecreated = $questiondata->timecreated;
 855          $question->timemodified = $questiondata->timemodified;
 856          $question->createdby = $questiondata->createdby;
 857          $question->modifiedby = $questiondata->modifiedby;
 858  
 859          // Fill extra question fields values.
 860          $extraquestionfields = $this->extra_question_fields();
 861          if (is_array($extraquestionfields)) {
 862              // Omit table name.
 863              array_shift($extraquestionfields);
 864              foreach ($extraquestionfields as $field) {
 865                  $question->$field = $questiondata->options->$field;
 866              }
 867          }
 868  
 869          $this->initialise_question_hints($question, $questiondata);
 870      }
 871  
 872      /**
 873       * Initialise question_definition::hints field.
 874       * @param question_definition $question the question_definition we are creating.
 875       * @param object $questiondata the question data loaded from the database.
 876       */
 877      protected function initialise_question_hints(question_definition $question, $questiondata) {
 878          if (empty($questiondata->hints)) {
 879              return;
 880          }
 881          foreach ($questiondata->hints as $hint) {
 882              $question->hints[] = $this->make_hint($hint);
 883          }
 884      }
 885  
 886      /**
 887       * Create a question_hint, or an appropriate subclass for this question,
 888       * from a row loaded from the database.
 889       * @param object $hint the DB row from the question hints table.
 890       * @return question_hint
 891       */
 892      protected function make_hint($hint) {
 893          return question_hint::load_from_record($hint);
 894      }
 895  
 896      /**
 897       * Initialise the combined feedback fields.
 898       * @param question_definition $question the question_definition we are creating.
 899       * @param object $questiondata the question data loaded from the database.
 900       * @param bool $withparts whether to set the shownumcorrect field.
 901       */
 902      protected function initialise_combined_feedback(question_definition $question,
 903              $questiondata, $withparts = false) {
 904          $question->correctfeedback = $questiondata->options->correctfeedback;
 905          $question->correctfeedbackformat = $questiondata->options->correctfeedbackformat;
 906          $question->partiallycorrectfeedback = $questiondata->options->partiallycorrectfeedback;
 907          $question->partiallycorrectfeedbackformat =
 908                  $questiondata->options->partiallycorrectfeedbackformat;
 909          $question->incorrectfeedback = $questiondata->options->incorrectfeedback;
 910          $question->incorrectfeedbackformat = $questiondata->options->incorrectfeedbackformat;
 911          if ($withparts) {
 912              $question->shownumcorrect = $questiondata->options->shownumcorrect;
 913          }
 914      }
 915  
 916      /**
 917       * Initialise question_definition::answers field.
 918       * @param question_definition $question the question_definition we are creating.
 919       * @param object $questiondata the question data loaded from the database.
 920       * @param bool $forceplaintextanswers most qtypes assume that answers are
 921       *      FORMAT_PLAIN, and dont use the answerformat DB column (it contains
 922       *      the default 0 = FORMAT_MOODLE). Therefore, by default this method
 923       *      ingores answerformat. Pass false here to use answerformat. For example
 924       *      multichoice does this.
 925       */
 926      protected function initialise_question_answers(question_definition $question,
 927              $questiondata, $forceplaintextanswers = true) {
 928          $question->answers = array();
 929          if (empty($questiondata->options->answers)) {
 930              return;
 931          }
 932          foreach ($questiondata->options->answers as $a) {
 933              $question->answers[$a->id] = $this->make_answer($a);
 934              if (!$forceplaintextanswers) {
 935                  $question->answers[$a->id]->answerformat = $a->answerformat;
 936              }
 937          }
 938      }
 939  
 940      /**
 941       * Create a question_answer, or an appropriate subclass for this question,
 942       * from a row loaded from the database.
 943       * @param object $answer the DB row from the question_answers table plus extra answer fields.
 944       * @return question_answer
 945       */
 946      protected function make_answer($answer) {
 947          return new question_answer($answer->id, $answer->answer,
 948                      $answer->fraction, $answer->feedback, $answer->feedbackformat);
 949      }
 950  
 951      /**
 952       * Deletes the question-type specific data when a question is deleted.
 953       * @param int $question the question being deleted.
 954       * @param int $contextid the context this quesiotn belongs to.
 955       */
 956      public function delete_question($questionid, $contextid) {
 957          global $DB;
 958  
 959          $this->delete_files($questionid, $contextid);
 960  
 961          $extraquestionfields = $this->extra_question_fields();
 962          if (is_array($extraquestionfields)) {
 963              $question_extension_table = array_shift($extraquestionfields);
 964              $DB->delete_records($question_extension_table,
 965                      array($this->questionid_column_name() => $questionid));
 966          }
 967  
 968          $extraanswerfields = $this->extra_answer_fields();
 969          if (is_array($extraanswerfields)) {
 970              $answer_extension_table = array_shift($extraanswerfields);
 971              $DB->delete_records_select($answer_extension_table,
 972                      'answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)',
 973                      array($questionid));
 974          }
 975  
 976          $DB->delete_records('question_answers', array('question' => $questionid));
 977  
 978          $DB->delete_records('question_hints', array('questionid' => $questionid));
 979      }
 980  
 981      /**
 982       * Returns the number of question numbers which are used by the question
 983       *
 984       * This function returns the number of question numbers to be assigned
 985       * to the question. Most question types will have length one; they will be
 986       * assigned one number. The 'description' type, however does not use up a
 987       * number and so has a length of zero. Other question types may wish to
 988       * handle a bundle of questions and hence return a number greater than one.
 989       * @return int         The number of question numbers which should be
 990       *                         assigned to the question.
 991       * @param object $question The question whose length is to be determined.
 992       *                         Question type specific information is included.
 993       */
 994      public function actual_number_of_questions($question) {
 995          // By default, each question is given one number.
 996          return 1;
 997      }
 998  
 999      /**
1000       * @param object $question
1001       * @return number|null either a fraction estimating what the student would
1002       * score by guessing, or null, if it is not possible to estimate.
1003       */
1004      public function get_random_guess_score($questiondata) {
1005          return 0;
1006      }
1007  
1008      /**
1009       * Whether or not to break down question stats and response analysis, for a question defined by $questiondata.
1010       *
1011       * @param object $questiondata The full question definition data.
1012       * @return bool
1013       */
1014      public function break_down_stats_and_response_analysis_by_variant($questiondata) {
1015          return true;
1016      }
1017  
1018      /**
1019       * This method should return all the possible types of response that are
1020       * recognised for this question.
1021       *
1022       * The question is modelled as comprising one or more subparts. For each
1023       * subpart, there are one or more classes that that students response
1024       * might fall into, each of those classes earning a certain score.
1025       *
1026       * For example, in a shortanswer question, there is only one subpart, the
1027       * text entry field. The response the student gave will be classified according
1028       * to which of the possible $question->options->answers it matches.
1029       *
1030       * For the matching question type, there will be one subpart for each
1031       * question stem, and for each stem, each of the possible choices is a class
1032       * of student's response.
1033       *
1034       * A response is an object with two fields, ->responseclass is a string
1035       * presentation of that response, and ->fraction, the credit for a response
1036       * in that class.
1037       *
1038       * Array keys have no specific meaning, but must be unique, and must be
1039       * the same if this function is called repeatedly.
1040       *
1041       * @param object $question the question definition data.
1042       * @return array keys are subquestionid, values are arrays of possible
1043       *      responses to that subquestion.
1044       */
1045      public function get_possible_responses($questiondata) {
1046          return array();
1047      }
1048  
1049      /**
1050       * Utility method used by {@link qtype_renderer::head_code()}. It looks
1051       * for any of the files script.js or script.php that exist in the plugin
1052       * folder and ensures they get included.
1053       */
1054      public function find_standard_scripts() {
1055          global $PAGE;
1056  
1057          $plugindir = $this->plugin_dir();
1058          $plugindirrel = 'question/type/' . $this->name();
1059  
1060          if (file_exists($plugindir . '/script.js')) {
1061              $PAGE->requires->js('/' . $plugindirrel . '/script.js');
1062          }
1063          if (file_exists($plugindir . '/script.php')) {
1064              $PAGE->requires->js('/' . $plugindirrel . '/script.php');
1065          }
1066      }
1067  
1068      /**
1069       * Returns true if the editing wizard is finished, false otherwise.
1070       *
1071       * The default implementation returns true, which is suitable for all question-
1072       * types that only use one editing form. This function is used in
1073       * question.php to decide whether we can regrade any states of the edited
1074       * question and redirect to edit.php.
1075       *
1076       * The dataset dependent question-type, which is extended by the calculated
1077       * question-type, overwrites this method because it uses multiple pages (i.e.
1078       * a wizard) to set up the question and associated datasets.
1079       *
1080       * @param object $form  The data submitted by the previous page.
1081       *
1082       * @return bool      Whether the wizard's last page was submitted or not.
1083       */
1084      public function finished_edit_wizard($form) {
1085          // In the default case there is only one edit page.
1086          return true;
1087      }
1088  
1089      // IMPORT/EXPORT FUNCTIONS --------------------------------- .
1090  
1091      /*
1092       * Imports question from the Moodle XML format
1093       *
1094       * Imports question using information from extra_question_fields function
1095       * If some of you fields contains id's you'll need to reimplement this
1096       */
1097      public function import_from_xml($data, $question, qformat_xml $format, $extra=null) {
1098          $question_type = $data['@']['type'];
1099          if ($question_type != $this->name()) {
1100              return false;
1101          }
1102  
1103          $extraquestionfields = $this->extra_question_fields();
1104          if (!is_array($extraquestionfields)) {
1105              return false;
1106          }
1107  
1108          // Omit table name.
1109          array_shift($extraquestionfields);
1110          $qo = $format->import_headers($data);
1111          $qo->qtype = $question_type;
1112  
1113          foreach ($extraquestionfields as $field) {
1114              $qo->$field = $format->getpath($data, array('#', $field, 0, '#'), '');
1115          }
1116  
1117          // Run through the answers.
1118          $answers = $data['#']['answer'];
1119          $a_count = 0;
1120          $extraanswersfields = $this->extra_answer_fields();
1121          if (is_array($extraanswersfields)) {
1122              array_shift($extraanswersfields);
1123          }
1124          foreach ($answers as $answer) {
1125              $ans = $format->import_answer($answer);
1126              if (!$this->has_html_answers()) {
1127                  $qo->answer[$a_count] = $ans->answer['text'];
1128              } else {
1129                  $qo->answer[$a_count] = $ans->answer;
1130              }
1131              $qo->fraction[$a_count] = $ans->fraction;
1132              $qo->feedback[$a_count] = $ans->feedback;
1133              if (is_array($extraanswersfields)) {
1134                  foreach ($extraanswersfields as $field) {
1135                      $qo->{$field}[$a_count] =
1136                          $format->getpath($answer, array('#', $field, 0, '#'), '');
1137                  }
1138              }
1139              ++$a_count;
1140          }
1141          return $qo;
1142      }
1143  
1144      /*
1145       * Export question to the Moodle XML format
1146       *
1147       * Export question using information from extra_question_fields function
1148       * If some of you fields contains id's you'll need to reimplement this
1149       */
1150      public function export_to_xml($question, qformat_xml $format, $extra=null) {
1151          $extraquestionfields = $this->extra_question_fields();
1152          if (!is_array($extraquestionfields)) {
1153              return false;
1154          }
1155  
1156          // Omit table name.
1157          array_shift($extraquestionfields);
1158          $expout='';
1159          foreach ($extraquestionfields as $field) {
1160              $exportedvalue = $format->xml_escape($question->options->$field);
1161              $expout .= "    <{$field}>{$exportedvalue}</{$field}>\n";
1162          }
1163  
1164          $extraanswersfields = $this->extra_answer_fields();
1165          if (is_array($extraanswersfields)) {
1166              array_shift($extraanswersfields);
1167          }
1168          foreach ($question->options->answers as $answer) {
1169              $extra = '';
1170              if (is_array($extraanswersfields)) {
1171                  foreach ($extraanswersfields as $field) {
1172                      $exportedvalue = $format->xml_escape($answer->$field);
1173                      $extra .= "      <{$field}>{$exportedvalue}</{$field}>\n";
1174                  }
1175              }
1176  
1177              $expout .= $format->write_answer($answer, $extra);
1178          }
1179          return $expout;
1180      }
1181  
1182      /**
1183       * Abstract function implemented by each question type. It runs all the code
1184       * required to set up and save a question of any type for testing purposes.
1185       * Alternate DB table prefix may be used to facilitate data deletion.
1186       */
1187      public function generate_test($name, $courseid=null) {
1188          $form = new stdClass();
1189          $form->name = $name;
1190          $form->questiontextformat = 1;
1191          $form->questiontext = 'test question, generated by script';
1192          $form->defaultmark = 1;
1193          $form->penalty = 0.3333333;
1194          $form->generalfeedback = "Well done";
1195  
1196          $context = context_course::instance($courseid);
1197          $newcategory = question_make_default_categories(array($context));
1198          $form->category = $newcategory->id . ',1';
1199  
1200          $question = new stdClass();
1201          $question->courseid = $courseid;
1202          $question->qtype = $this->qtype;
1203          return array($form, $question);
1204      }
1205  
1206      /**
1207       * Get question context by category id
1208       * @param int $category
1209       * @return object $context
1210       */
1211      protected function get_context_by_category_id($category) {
1212          global $DB;
1213          $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category));
1214          $context = context::instance_by_id($contextid, IGNORE_MISSING);
1215          return $context;
1216      }
1217  
1218      /**
1219       * Save the file belonging to one text field.
1220       *
1221       * @param array $field the data from the form (or from import). This will
1222       *      normally have come from the formslib editor element, so it will be an
1223       *      array with keys 'text', 'format' and 'itemid'. However, when we are
1224       *      importing, it will be an array with keys 'text', 'format' and 'files'
1225       * @param object $context the context the question is in.
1226       * @param string $component indentifies the file area question.
1227       * @param string $filearea indentifies the file area questiontext,
1228       *      generalfeedback, answerfeedback, etc.
1229       * @param int $itemid identifies the file area.
1230       *
1231       * @return string the text for this field, after files have been processed.
1232       */
1233      protected function import_or_save_files($field, $context, $component, $filearea, $itemid) {
1234          if (!empty($field['itemid'])) {
1235              // This is the normal case. We are safing the questions editing form.
1236              return file_save_draft_area_files($field['itemid'], $context->id, $component,
1237                      $filearea, $itemid, $this->fileoptions, trim($field['text']));
1238  
1239          } else if (!empty($field['files'])) {
1240              // This is the case when we are doing an import.
1241              foreach ($field['files'] as $file) {
1242                  $this->import_file($context, $component,  $filearea, $itemid, $file);
1243              }
1244          }
1245          return trim($field['text']);
1246      }
1247  
1248      /**
1249       * Move all the files belonging to this question from one context to another.
1250       * @param int $questionid the question being moved.
1251       * @param int $oldcontextid the context it is moving from.
1252       * @param int $newcontextid the context it is moving to.
1253       */
1254      public function move_files($questionid, $oldcontextid, $newcontextid) {
1255          $fs = get_file_storage();
1256          $fs->move_area_files_to_new_context($oldcontextid,
1257                  $newcontextid, 'question', 'questiontext', $questionid);
1258          $fs->move_area_files_to_new_context($oldcontextid,
1259                  $newcontextid, 'question', 'generalfeedback', $questionid);
1260      }
1261  
1262      /**
1263       * Move all the files belonging to this question's answers when the question
1264       * is moved from one context to another.
1265       * @param int $questionid the question being moved.
1266       * @param int $oldcontextid the context it is moving from.
1267       * @param int $newcontextid the context it is moving to.
1268       * @param bool $answerstoo whether there is an 'answer' question area,
1269       *      as well as an 'answerfeedback' one. Default false.
1270       */
1271      protected function move_files_in_answers($questionid, $oldcontextid,
1272              $newcontextid, $answerstoo = false) {
1273          global $DB;
1274          $fs = get_file_storage();
1275  
1276          $answerids = $DB->get_records_menu('question_answers',
1277                  array('question' => $questionid), 'id', 'id,1');
1278          foreach ($answerids as $answerid => $notused) {
1279              if ($answerstoo) {
1280                  $fs->move_area_files_to_new_context($oldcontextid,
1281                          $newcontextid, 'question', 'answer', $answerid);
1282              }
1283              $fs->move_area_files_to_new_context($oldcontextid,
1284                      $newcontextid, 'question', 'answerfeedback', $answerid);
1285          }
1286      }
1287  
1288      /**
1289       * Move all the files belonging to this question's hints when the question
1290       * is moved from one context to another.
1291       * @param int $questionid the question being moved.
1292       * @param int $oldcontextid the context it is moving from.
1293       * @param int $newcontextid the context it is moving to.
1294       * @param bool $answerstoo whether there is an 'answer' question area,
1295       *      as well as an 'answerfeedback' one. Default false.
1296       */
1297      protected function move_files_in_hints($questionid, $oldcontextid, $newcontextid) {
1298          global $DB;
1299          $fs = get_file_storage();
1300  
1301          $hintids = $DB->get_records_menu('question_hints',
1302                  array('questionid' => $questionid), 'id', 'id,1');
1303          foreach ($hintids as $hintid => $notused) {
1304              $fs->move_area_files_to_new_context($oldcontextid,
1305                      $newcontextid, 'question', 'hint', $hintid);
1306          }
1307      }
1308  
1309      /**
1310       * Move all the files belonging to this question's answers when the question
1311       * is moved from one context to another.
1312       * @param int $questionid the question being moved.
1313       * @param int $oldcontextid the context it is moving from.
1314       * @param int $newcontextid the context it is moving to.
1315       * @param bool $answerstoo whether there is an 'answer' question area,
1316       *      as well as an 'answerfeedback' one. Default false.
1317       */
1318      protected function move_files_in_combined_feedback($questionid, $oldcontextid,
1319              $newcontextid) {
1320          global $DB;
1321          $fs = get_file_storage();
1322  
1323          $fs->move_area_files_to_new_context($oldcontextid,
1324                  $newcontextid, 'question', 'correctfeedback', $questionid);
1325          $fs->move_area_files_to_new_context($oldcontextid,
1326                  $newcontextid, 'question', 'partiallycorrectfeedback', $questionid);
1327          $fs->move_area_files_to_new_context($oldcontextid,
1328                  $newcontextid, 'question', 'incorrectfeedback', $questionid);
1329      }
1330  
1331      /**
1332       * Delete all the files belonging to this question.
1333       * @param int $questionid the question being deleted.
1334       * @param int $contextid the context the question is in.
1335       */
1336      protected function delete_files($questionid, $contextid) {
1337          $fs = get_file_storage();
1338          $fs->delete_area_files($contextid, 'question', 'questiontext', $questionid);
1339          $fs->delete_area_files($contextid, 'question', 'generalfeedback', $questionid);
1340      }
1341  
1342      /**
1343       * Delete all the files belonging to this question's answers.
1344       * @param int $questionid the question being deleted.
1345       * @param int $contextid the context the question is in.
1346       * @param bool $answerstoo whether there is an 'answer' question area,
1347       *      as well as an 'answerfeedback' one. Default false.
1348       */
1349      protected function delete_files_in_answers($questionid, $contextid, $answerstoo = false) {
1350          global $DB;
1351          $fs = get_file_storage();
1352  
1353          $answerids = $DB->get_records_menu('question_answers',
1354                  array('question' => $questionid), 'id', 'id,1');
1355          foreach ($answerids as $answerid => $notused) {
1356              if ($answerstoo) {
1357                  $fs->delete_area_files($contextid, 'question', 'answer', $answerid);
1358              }
1359              $fs->delete_area_files($contextid, 'question', 'answerfeedback', $answerid);
1360          }
1361      }
1362  
1363      /**
1364       * Delete all the files belonging to this question's hints.
1365       * @param int $questionid the question being deleted.
1366       * @param int $contextid the context the question is in.
1367       */
1368      protected function delete_files_in_hints($questionid, $contextid) {
1369          global $DB;
1370          $fs = get_file_storage();
1371  
1372          $hintids = $DB->get_records_menu('question_hints',
1373                  array('questionid' => $questionid), 'id', 'id,1');
1374          foreach ($hintids as $hintid => $notused) {
1375              $fs->delete_area_files($contextid, 'question', 'hint', $hintid);
1376          }
1377      }
1378  
1379      /**
1380       * Delete all the files belonging to this question's answers.
1381       * @param int $questionid the question being deleted.
1382       * @param int $contextid the context the question is in.
1383       * @param bool $answerstoo whether there is an 'answer' question area,
1384       *      as well as an 'answerfeedback' one. Default false.
1385       */
1386      protected function delete_files_in_combined_feedback($questionid, $contextid) {
1387          global $DB;
1388          $fs = get_file_storage();
1389  
1390          $fs->delete_area_files($contextid,
1391                  'question', 'correctfeedback', $questionid);
1392          $fs->delete_area_files($contextid,
1393                  'question', 'partiallycorrectfeedback', $questionid);
1394          $fs->delete_area_files($contextid,
1395                  'question', 'incorrectfeedback', $questionid);
1396      }
1397  
1398      public function import_file($context, $component, $filearea, $itemid, $file) {
1399          $fs = get_file_storage();
1400          $record = new stdClass();
1401          if (is_object($context)) {
1402              $record->contextid = $context->id;
1403          } else {
1404              $record->contextid = $context;
1405          }
1406          $record->component = $component;
1407          $record->filearea  = $filearea;
1408          $record->itemid    = $itemid;
1409          $record->filename  = $file->name;
1410          $record->filepath  = '/';
1411          return $fs->create_file_from_string($record, $this->decode_file($file));
1412      }
1413  
1414      protected function decode_file($file) {
1415          switch ($file->encoding) {
1416              case 'base64':
1417              default:
1418                  return base64_decode($file->content);
1419          }
1420      }
1421  }
1422  
1423  
1424  /**
1425   * This class is used in the return value from
1426   * {@link question_type::get_possible_responses()}.
1427   *
1428   * @copyright  2010 The Open University
1429   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1430   */
1431  class question_possible_response {
1432      /**
1433       * @var string the classification of this response the student gave to this
1434       * part of the question. Must match one of the responseclasses returned by
1435       * {@link question_type::get_possible_responses()}.
1436       */
1437      public $responseclass;
1438  
1439      /** @var string the (partial) credit awarded for this responses. */
1440      public $fraction;
1441  
1442      /**
1443       * Constructor, just an easy way to set the fields.
1444       * @param string $responseclassid see the field descriptions above.
1445       * @param string $response see the field descriptions above.
1446       * @param number $fraction see the field descriptions above.
1447       */
1448      public function __construct($responseclass, $fraction) {
1449          $this->responseclass = $responseclass;
1450          $this->fraction = $fraction;
1451      }
1452  
1453      public static function no_response() {
1454          return new question_possible_response(get_string('noresponse', 'question'), 0);
1455      }
1456  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1