[ Index ]

PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008]

title

Body

[close]

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

   1  <?php  // $Id: questiontype.php,v 1.74.2.13 2008/10/08 10:19:51 jamiesensei Exp $
   2  /**
   3   * The default questiontype class.
   4   *
   5   * @author Martin Dougiamas and many others. This has recently been completely
   6   *         rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of
   7   *         the Serving Mathematics project
   8   *         {@link http://maths.york.ac.uk/serving_maths}
   9   * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  10   * @package questionbank
  11   * @subpackage questiontypes
  12   */
  13  
  14  require_once($CFG->libdir . '/questionlib.php');
  15  
  16  /**
  17   * This is the base class for Moodle question types.
  18   *
  19   * There are detailed comments on each method, explaining what the method is
  20   * for, and the circumstances under which you might need to override it.
  21   *
  22   * Note: the questiontype API should NOT be considered stable yet. Very few
  23   * question tyeps have been produced yet, so we do not yet know all the places
  24   * where the current API is insufficient. I would rather learn from the
  25   * experiences of the first few question type implementors, and improve the
  26   * interface to meet their needs, rather the freeze the API prematurely and
  27   * condem everyone to working round a clunky interface for ever afterwards.
  28   *
  29   * @package questionbank
  30   * @subpackage questiontypes
  31   */
  32  class default_questiontype {
  33  
  34      /**
  35       * Name of the question type
  36       *
  37       * The name returned should coincide with the name of the directory
  38       * in which this questiontype is located
  39       *
  40       * @return string the name of this question type.
  41       */
  42      function name() {
  43          return 'default';
  44      }
  45  
  46      /**
  47       * The name this question should appear as in the create new question
  48       * dropdown.
  49       *
  50       * @return mixed the desired string, or false to hide this question type in the menu.
  51       */
  52      function menu_name() {
  53          $name = $this->name();
  54          $menu_name = get_string($name, 'qtype_' . $name);
  55          if ($menu_name[0] == '[') {
  56              // Legacy behavior, if the string was not in the proper qtype_name
  57              // language file, look it up in the quiz one.
  58              $menu_name = get_string($name, 'quiz');
  59          }
  60          return $menu_name;
  61      }
  62  
  63      /**
  64       * @return boolean true if this question can only be graded manually.
  65       */
  66      function is_manual_graded() {
  67          return false;
  68      }
  69  
  70      /**
  71       * @return boolean true if this question type can be used by the random question type.
  72       */
  73      function is_usable_by_random() {
  74          return true;
  75      }
  76  
  77      /**
  78       * @return whether the question_answers.answer field needs to have
  79       * restore_decode_content_links_worker called on it.
  80       */
  81      function has_html_answers() {
  82          return false;
  83      }
  84  
  85      /**
  86       * If your question type has a table that extends the question table, and
  87       * you want the base class to automatically save, backup and restore the extra fields,
  88       * override this method to return an array wherer the first element is the table name,
  89       * and the subsequent entries are the column names (apart from id and questionid).
  90       *
  91       * @return mixed array as above, or null to tell the base class to do nothing.
  92       */
  93      function extra_question_fields() {
  94          return null;
  95      }
  96  
  97      /**
  98       * If your question type has a table that extends the question_answers table,
  99       * make this method return an array wherer the first element is the table name,
 100       * and the subsequent entries are the column names (apart from id and answerid).
 101       *
 102       * @return mixed array as above, or null to tell the base class to do nothing.
 103       */
 104       function extra_answer_fields() {
 105           return null;
 106       }
 107  
 108      /**
 109       * Return an instance of the question editing form definition. This looks for a
 110       * class called edit_{$this->name()}_question_form in the file
 111       * {$CFG->docroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php
 112       * and if it exists returns an instance of it.
 113       *
 114       * @param string $submiturl passed on to the constructor call.
 115       * @return object an instance of the form definition, or null if one could not be found.
 116       */
 117      function create_editing_form($submiturl, $question, $category, $contexts, $formeditable) {
 118          global $CFG;
 119          require_once("{$CFG->dirroot}/question/type/edit_question_form.php");
 120          $definition_file = $CFG->dirroot.'/question/type/'.$this->name().'/edit_'.$this->name().'_form.php';
 121          if (!(is_readable($definition_file) && is_file($definition_file))) {
 122              return null;
 123          }
 124          require_once($definition_file);
 125          $classname = 'question_edit_'.$this->name().'_form';
 126          if (!class_exists($classname)) {
 127              return null;
 128          }
 129          return new $classname($submiturl, $question, $category, $contexts, $formeditable);
 130      }
 131  
 132      /**
 133       * @return string the full path of the folder this plugin's files live in.
 134       */
 135      function plugin_dir() {
 136          global $CFG;
 137          return $CFG->dirroot . '/question/type/' . $this->name();
 138      }
 139  
 140      /**
 141       * @return string the URL of the folder this plugin's files live in.
 142       */
 143      function plugin_baseurl() {
 144          global $CFG;
 145          return $CFG->wwwroot . '/question/type/' . $this->name();
 146      }
 147  
 148      /**
 149       * This method should be overriden if you want to include a special heading or some other
 150       * html on a question editing page besides the question editing form.
 151       *
 152       * @param question_edit_form $mform a child of question_edit_form
 153       * @param object $question
 154       * @param string $wizardnow is '' for first page.
 155       */
 156      function display_question_editing_page(&$mform, $question, $wizardnow){
 157          list($heading, $langmodule) = $this->get_heading(empty($question->id));
 158          print_heading_with_help($heading, $this->name(), $langmodule);
 159          $permissionstrs = array();
 160          if (!empty($question->id)){
 161              if ($question->formoptions->canedit){
 162                  $permissionstrs[] = get_string('permissionedit', 'question');
 163              }
 164              if ($question->formoptions->canmove){
 165                  $permissionstrs[] = get_string('permissionmove', 'question');
 166              }
 167              if ($question->formoptions->cansaveasnew){
 168                  $permissionstrs[] = get_string('permissionsaveasnew', 'question');
 169              }
 170          }
 171          if (!$question->formoptions->movecontext  && count($permissionstrs)){
 172              print_heading(get_string('permissionto', 'question'), 'center', 3);
 173              $html = '<ul>';
 174              foreach ($permissionstrs as $permissionstr){
 175                  $html .= '<li>'.$permissionstr.'</li>';
 176              }
 177              $html .= '</ul>';
 178              print_box($html, 'boxwidthnarrow boxaligncenter generalbox');
 179          }
 180          $mform->display();
 181      }
 182  
 183      /**
 184       * Method called by display_question_editing_page and by question.php to get heading for breadcrumbs.
 185       *
 186       * @return array a string heading and the langmodule in which it was found.
 187       */
 188      function get_heading($adding = false){
 189          $name = $this->name();
 190          $langmodule = 'qtype_' . $name;
 191          if (!$adding){
 192              $strtoget = 'editing' . $name;
 193          } else {
 194              $strtoget = 'adding' . $name;
 195          }
 196          $strheading = get_string($strtoget, $langmodule);
 197          if ($strheading[0] == '[') {
 198              // Legacy behavior, if the string was not in the proper qtype_name
 199              // language file, look it up in the quiz one.
 200              $langmodule = 'quiz';
 201              $strheading = get_string($strtoget, $langmodule);
 202          }
 203          return array($strheading, $langmodule);
 204      }
 205  
 206      /**
 207       *
 208       *
 209       * @param $question
 210       */
 211      function set_default_options(&$question) {
 212      }
 213  
 214      /**
 215      * Saves or updates a question after editing by a teacher
 216      *
 217      * Given some question info and some data about the answers
 218      * this function parses, organises and saves the question
 219      * It is used by {@link question.php} when saving new data from
 220      * a form, and also by {@link import.php} when importing questions
 221      * This function in turn calls {@link save_question_options}
 222      * to save question-type specific options
 223      * @param object $question the question object which should be updated
 224      * @param object $form the form submitted by the teacher
 225      * @param object $course the course we are in
 226      * @return object On success, return the new question object. On failure,
 227      *       return an object as follows. If the error object has an errors field,
 228      *       display that as an error message. Otherwise, the editing form will be
 229      *       redisplayed with validation errors, from validation_errors field, which
 230      *       is itself an object, shown next to the form fields.
 231      */
 232      function save_question($question, $form, $course) {
 233          global $USER;
 234  
 235          // This default implementation is suitable for most
 236          // question types.
 237  
 238          // First, save the basic question itself
 239          $question->name               = trim($form->name);
 240          $question->questiontext       = trim($form->questiontext);
 241          $question->questiontextformat = $form->questiontextformat;
 242          $question->parent             = isset($form->parent)? $form->parent : 0;
 243          $question->length = $this->actual_number_of_questions($question);
 244          $question->penalty = isset($form->penalty) ? $form->penalty : 0;
 245  
 246          if (empty($form->image)) {
 247              $question->image = "";
 248          } else {
 249              $question->image = $form->image;
 250          }
 251  
 252          if (empty($form->generalfeedback)) {
 253              $question->generalfeedback = '';
 254          } else {
 255              $question->generalfeedback = trim($form->generalfeedback);
 256          }
 257  
 258          if (empty($question->name)) {
 259              $question->name = shorten_text(strip_tags($question->questiontext), 15);
 260              if (empty($question->name)) {
 261                  $question->name = '-';
 262              }
 263          }
 264  
 265          if ($question->penalty > 1 or $question->penalty < 0) {
 266              $question->errors['penalty'] = get_string('invalidpenalty', 'quiz');
 267          }
 268  
 269          if (isset($form->defaultgrade)) {
 270              $question->defaultgrade = $form->defaultgrade;
 271          }
 272  
 273          if (!empty($question->id) && !empty($form->categorymoveto)) { // Question already exists
 274              list($movetocategory, $movetocontextid) = explode(',', $form->categorymoveto);
 275              if ($movetocategory != $question->category){
 276                  question_require_capability_on($question, 'move');
 277                  $question->category = $movetocategory;
 278                  //don't need to test add permission of category we are moving question to.
 279                  //Only categories that we have permission to add
 280                  //a question to will get through the form cleaning code for the select box.
 281              }
 282              // keep existing unique stamp code
 283              $question->stamp = get_field('question', 'stamp', 'id', $question->id);
 284              $question->modifiedby = $USER->id;
 285              $question->timemodified = time();
 286              if (!update_record('question', $question)) {
 287                  error('Could not update question!');
 288              }
 289          } else {         // Question is a new one
 290              if (isset($form->categorymoveto)){
 291                  // Doing save as new question, and we have move rights.
 292                  list($question->category, $notused) = explode(',', $form->categorymoveto);
 293                  //don't need to test add permission of category we are moving question to.
 294                  //Only categories that we have permission to add
 295                  //a question to will get through the form cleaning code for the select box.
 296              } else {
 297                  // Really a new question.
 298                  list($question->category, $notused) = explode(',', $form->category);
 299              }
 300              // Set the unique code
 301              $question->stamp = make_unique_id_code();
 302              $question->createdby = $USER->id;
 303              $question->modifiedby = $USER->id;
 304              $question->timecreated = time();
 305              $question->timemodified = time();
 306              if (!$question->id = insert_record('question', $question)) {
 307                  print_object($question);
 308                  error('Could not insert new question!');
 309              }
 310          }
 311  
 312          // Now to save all the answers and type-specific options
 313  
 314          $form->id = $question->id;
 315          $form->qtype = $question->qtype;
 316          $form->category = $question->category;
 317          $form->questiontext = $question->questiontext;
 318  
 319          $result = $this->save_question_options($form);
 320  
 321          if (!empty($result->error)) {
 322              error($result->error);
 323          }
 324  
 325          if (!empty($result->notice)) {
 326              notice($result->notice, "question.php?id=$question->id");
 327          }
 328  
 329          if (!empty($result->noticeyesno)) {
 330              notice_yesno($result->noticeyesno, "question.php?id=$question->id&amp;courseid={$course->id}",
 331                  "edit.php?courseid={$course->id}");
 332              print_footer($course);
 333              exit;
 334          }
 335  
 336          // Give the question a unique version stamp determined by question_hash()
 337          if (!set_field('question', 'version', question_hash($question), 'id', $question->id)) {
 338              error('Could not update question version field');
 339          }
 340  
 341          return $question;
 342      }
 343  
 344      /**
 345      * Saves question-type specific options
 346      *
 347      * This is called by {@link save_question()} to save the question-type specific data
 348      * @return object $result->error or $result->noticeyesno or $result->notice
 349      * @param object $question  This holds the information from the editing form,
 350      *                          it is not a standard question object.
 351      */
 352      function save_question_options($question) {
 353          $extra_question_fields = $this->extra_question_fields();
 354  
 355          if (is_array($extra_question_fields)) {
 356              $question_extension_table = array_shift($extra_question_fields);
 357  
 358              $function = 'update_record';
 359              $options = get_record($question_extension_table, 'questionid', $question->id);
 360              if (!$options) {
 361                  $function = 'insert_record';
 362                  $options = new stdClass;
 363                  $options->questionid = $question->id;
 364              }
 365              foreach ($extra_question_fields as $field) {
 366                  if (!isset($question->$field)) {
 367                      $result = new stdClass;
 368                      $result->error = "No data for field $field when saving " .
 369                              $this->name() . ' question id ' . $question->id;
 370                      return $result;
 371                  }
 372                  $options->$field = $question->$field;
 373              }
 374  
 375              if (!$function($question_extension_table, $options)) {
 376                  $result = new stdClass;
 377                  $result->error = 'Could not save question options for ' .
 378                          $this->name() . ' question id ' . $question->id;
 379                  return $result;
 380              }
 381          }
 382  
 383          $extra_answer_fields = $this->extra_answer_fields();
 384          // TODO save the answers, with any extra data.
 385  
 386          return null;
 387      }
 388  
 389      /**
 390      * Changes all states for the given attempts over to a new question
 391      *
 392      * This is used by the versioning code if the teacher requests that a question
 393      * gets replaced by the new version. In order for the attempts to be regraded
 394      * properly all data in the states referring to the old question need to be
 395      * changed to refer to the new version instead. In particular for question types
 396      * that use the answers table the answers belonging to the old question have to
 397      * be changed to those belonging to the new version.
 398      *
 399      * @param integer $oldquestionid  The id of the old question
 400      * @param object $newquestion    The new question
 401      * @param array  $attempts       An array of all attempt objects in whose states
 402      *                               replacement should take place
 403      */
 404      function replace_question_in_attempts($oldquestionid, $newquestion, $attemtps) {
 405          echo 'Not yet implemented';
 406          return;
 407      }
 408  
 409      /**
 410      * Loads the question type specific options for the question.
 411      *
 412      * This function loads any question type specific options for the
 413      * question from the database into the question object. This information
 414      * is placed in the $question->options field. A question type is
 415      * free, however, to decide on a internal structure of the options field.
 416      * @return bool            Indicates success or failure.
 417      * @param object $question The question object for the question. This object
 418      *                         should be updated to include the question type
 419      *                         specific information (it is passed by reference).
 420      */
 421      function get_question_options(&$question) {
 422          global $CFG;
 423  
 424          if (!isset($question->options)) {
 425              $question->options = new object;
 426          }
 427  
 428          $extra_question_fields = $this->extra_question_fields();
 429          if (is_array($extra_question_fields)) {
 430              $question_extension_table = array_shift($extra_question_fields);
 431              $extra_data = get_record($question_extension_table, 'questionid', $question->id, '', '', '', '', implode(', ', $extra_question_fields));
 432              if ($extra_data) {
 433                  foreach ($extra_question_fields as $field) {
 434                      $question->options->$field = $extra_data->$field;
 435                  }
 436              } else {
 437                  notify("Failed to load question options from the table $question_extension_table for questionid " .
 438                          $question->id);
 439                  return false;
 440              }
 441          }
 442  
 443          $extra_answer_fields = $this->extra_answer_fields();
 444          if (is_array($extra_answer_fields)) {
 445              $answer_extension_table = array_shift($extra_answer_fields);
 446              $question->options->answers = get_records_sql('
 447                      SELECT qa.*, qax.' . implode(', qax.', $extra_answer_fields) . '
 448                      FROM ' . $CFG->prefix . 'question_answers qa, ' . $CFG->prefix . '$answer_extension_table qax
 449                      WHERE qa.questionid = ' . $question->id . ' AND qax.answerid = qa.id');
 450              if (!$question->options->answers) {
 451                  notify("Failed to load question answers from the table $answer_extension_table for questionid " .
 452                          $question->id);
 453                  return false;
 454              }
 455          } else {
 456              // Don't check for success or failure because some question types do not use the answers table.
 457              $question->options->answers = get_records('question_answers', 'question', $question->id, 'id ASC');
 458          }
 459  
 460          return true;
 461      }
 462  
 463      /**
 464      * Deletes states from the question-type specific tables
 465      *
 466      * @param string $stateslist  Comma separated list of state ids to be deleted
 467      */
 468      function delete_states($stateslist) {
 469          /// The default question type does not have any tables of its own
 470          // therefore there is nothing to delete
 471  
 472          return true;
 473      }
 474  
 475      /**
 476      * Deletes a question from the question-type specific tables
 477      *
 478      * @return boolean Success/Failure
 479      * @param object $question  The question being deleted
 480      */
 481      function delete_question($questionid) {
 482          global $CFG;
 483          $success = true;
 484  
 485          $extra_question_fields = $this->extra_question_fields();
 486          if (is_array($extra_question_fields)) {
 487              $question_extension_table = array_shift($extra_question_fields);
 488              $success = $success && delete_records($question_extension_table, 'questionid', $questionid);
 489          }
 490  
 491          $extra_answer_fields = $this->extra_answer_fields();
 492          if (is_array($extra_answer_fields)) {
 493              $answer_extension_table = array_shift($extra_answer_fields);
 494              $success = $success && delete_records_select($answer_extension_table,
 495                      "answerid IN (SELECT qa.id FROM {$CFG->prefix}question_answers qa WHERE qa.question = $questionid)");
 496          }
 497  
 498          $success = $success && delete_records('question_answers', 'question', $questionid);
 499  
 500          return $success;
 501      }
 502  
 503      /**
 504      * Returns the number of question numbers which are used by the question
 505      *
 506      * This function returns the number of question numbers to be assigned
 507      * to the question. Most question types will have length one; they will be
 508      * assigned one number. The 'description' type, however does not use up a
 509      * number and so has a length of zero. Other question types may wish to
 510      * handle a bundle of questions and hence return a number greater than one.
 511      * @return integer         The number of question numbers which should be
 512      *                         assigned to the question.
 513      * @param object $question The question whose length is to be determined.
 514      *                         Question type specific information is included.
 515      */
 516      function actual_number_of_questions($question) {
 517          // By default, each question is given one number
 518          return 1;
 519      }
 520  
 521      /**
 522      * Creates empty session and response information for the question
 523      *
 524      * This function is called to start a question session. Empty question type
 525      * specific session data (if any) and empty response data will be added to the
 526      * state object. Session data is any data which must persist throughout the
 527      * attempt possibly with updates as the user interacts with the
 528      * question. This function does NOT create new entries in the database for
 529      * the session; a call to the {@link save_session_and_responses} member will
 530      * occur to do this.
 531      * @return bool            Indicates success or failure.
 532      * @param object $question The question for which the session is to be
 533      *                         created. Question type specific information is
 534      *                         included.
 535      * @param object $state    The state to create the session for. Note that
 536      *                         this will not have been saved in the database so
 537      *                         there will be no id. This object will be updated
 538      *                         to include the question type specific information
 539      *                         (it is passed by reference). In particular, empty
 540      *                         responses will be created in the ->responses
 541      *                         field.
 542      * @param object $cmoptions
 543      * @param object $attempt  The attempt for which the session is to be
 544      *                         started. Questions may wish to initialize the
 545      *                         session in different ways depending on the user id
 546      *                         or time available for the attempt.
 547      */
 548      function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
 549          // The default implementation should work for the legacy question types.
 550          // Most question types with only a single form field for the student's response
 551          // will use the empty string '' as the index for that one response. This will
 552          // automatically be stored in and restored from the answer field in the
 553          // question_states table.
 554          $state->responses = array(
 555                  '' => '',
 556          );
 557          return true;
 558      }
 559  
 560      /**
 561      * Restores the session data and most recent responses for the given state
 562      *
 563      * This function loads any session data associated with the question
 564      * session in the given state from the database into the state object.
 565      * In particular it loads the responses that have been saved for the given
 566      * state into the ->responses member of the state object.
 567      *
 568      * Question types with only a single form field for the student's response
 569      * will not need not restore the responses; the value of the answer
 570      * field in the question_states table is restored to ->responses['']
 571      * before this function is called. Question types with more response fields
 572      * should override this method and set the ->responses field to an
 573      * associative array of responses.
 574      * @return bool            Indicates success or failure.
 575      * @param object $question The question object for the question including any
 576      *                         question type specific information.
 577      * @param object $state    The saved state to load the session for. This
 578      *                         object should be updated to include the question
 579      *                         type specific session information and responses
 580      *                         (it is passed by reference).
 581      */
 582      function restore_session_and_responses(&$question, &$state) {
 583          // The default implementation does nothing (successfully)
 584          return true;
 585      }
 586  
 587      /**
 588      * Saves the session data and responses for the given question and state
 589      *
 590      * This function saves the question type specific session data from the
 591      * state object to the database. In particular for most question types it saves the
 592      * responses from the ->responses member of the state object. The question type
 593      * non-specific data for the state has already been saved in the question_states
 594      * table and the state object contains the corresponding id and
 595      * sequence number which may be used to index a question type specific table.
 596      *
 597      * Question types with only a single form field for the student's response
 598      * which is contained in ->responses[''] will not have to save this response,
 599      * it will already have been saved to the answer field of the question_states table.
 600      * Question types with more response fields should override this method to convert
 601      * the data the ->responses array into a single string field, and save it in the
 602      * database. The implementation in the multichoice question type is a good model to follow.
 603      * http://cvs.moodle.org/contrib/plugins/question/type/opaque/questiontype.php?view=markup
 604      * has a solution that is probably quite generally applicable.
 605      * @return bool            Indicates success or failure.
 606      * @param object $question The question object for the question including
 607      *                         the question type specific information.
 608      * @param object $state    The state for which the question type specific
 609      *                         data and responses should be saved.
 610      */
 611      function save_session_and_responses(&$question, &$state) {
 612          // The default implementation does nothing (successfully)
 613          return true;
 614      }
 615  
 616      /**
 617      * Returns an array of values which will give full marks if graded as
 618      * the $state->responses field
 619      *
 620      * The correct answer to the question in the given state, or an example of
 621      * a correct answer if there are many, is returned. This is used by some question
 622      * types in the {@link grade_responses()} function but it is also used by the
 623      * question preview screen to fill in correct responses.
 624      * @return mixed           A response array giving the responses corresponding
 625      *                         to the (or a) correct answer to the question. If there is
 626      *                         no correct answer that scores 100% then null is returned.
 627      * @param object $question The question for which the correct answer is to
 628      *                         be retrieved. Question type specific information is
 629      *                         available.
 630      * @param object $state    The state of the question, for which a correct answer is
 631      *                         needed. Question type specific information is included.
 632      */
 633      function get_correct_responses(&$question, &$state) {
 634          /* The default implementation returns the response for the first answer
 635          that gives full marks. */
 636          if ($question->options->answers) {
 637              foreach ($question->options->answers as $answer) {
 638                  if (((int) $answer->fraction) === 1) {
 639                      return array('' => addslashes($answer->answer));
 640                  }
 641              }
 642          }
 643          return null;
 644      }
 645  
 646      /**
 647      * Return an array of values with the texts for all possible responses stored
 648      * for the question
 649      *
 650      * All answers are found and their text values isolated
 651      * @return object          A mixed object
 652      *             ->id        question id. Needed to manage random questions:
 653      *                         it's the id of the actual question presented to user in a given attempt
 654      *             ->responses An array of values giving the responses corresponding
 655      *                         to all answers to the question. Answer ids are used as keys.
 656      *                         The text and partial credit are the object components
 657      * @param object $question The question for which the answers are to
 658      *                         be retrieved. Question type specific information is
 659      *                         available.
 660      */
 661      // ULPGC ecastro
 662      function get_all_responses(&$question, &$state) {
 663          if (isset($question->options->answers) && is_array($question->options->answers)) {
 664              $answers = array();
 665              foreach ($question->options->answers as $aid=>$answer) {
 666                  $r = new stdClass;
 667                  $r->answer = $answer->answer;
 668                  $r->credit = $answer->fraction;
 669                  $answers[$aid] = $r;
 670              }
 671              $result = new stdClass;
 672              $result->id = $question->id;
 673              $result->responses = $answers;
 674              return $result;
 675          } else {
 676              return null;
 677          }
 678      }
 679  
 680      /**
 681      * Return the actual response to the question in a given state
 682      * for the question.
 683      *
 684      * @return mixed           An array containing the response or reponses (multiple answer, match)
 685      *                         given by the user in a particular attempt.
 686      * @param object $question The question for which the correct answer is to
 687      *                         be retrieved. Question type specific information is
 688      *                         available.
 689      * @param object $state    The state object that corresponds to the question,
 690      *                         for which a correct answer is needed. Question
 691      *                         type specific information is included.
 692      */
 693      // ULPGC ecastro
 694      function get_actual_response($question, $state) {
 695         if (!empty($state->responses)) {
 696             $responses[] = stripslashes($state->responses['']);
 697         } else {
 698             $responses[] = '';
 699         }
 700         return $responses;
 701      }
 702  
 703      // ULPGC ecastro
 704      function get_fractional_grade(&$question, &$state) {
 705          $maxgrade = $question->maxgrade;
 706          $grade = $state->grade;
 707          if ($maxgrade) {
 708              return (float)($grade/$maxgrade);
 709          } else {
 710              return (float)$grade;
 711          }
 712      }
 713  
 714  
 715      /**
 716      * Checks if the response given is correct and returns the id
 717      *
 718      * @return int             The ide number for the stored answer that matches the response
 719      *                         given by the user in a particular attempt.
 720      * @param object $question The question for which the correct answer is to
 721      *                         be retrieved. Question type specific information is
 722      *                         available.
 723      * @param object $state    The state object that corresponds to the question,
 724      *                         for which a correct answer is needed. Question
 725      *                         type specific information is included.
 726      */
 727      // ULPGC ecastro
 728      function check_response(&$question, &$state){
 729          return false;
 730      }
 731  
 732      // Used by the following function, so that it only returns results once per quiz page.
 733      var $already_done = false;
 734      /**
 735       * If this question type requires extra CSS or JavaScript to function,
 736       * then this method will return an array of <link ...> tags that reference
 737       * those stylesheets. This function will also call require_js()
 738       * from ajaxlib.php, to get any necessary JavaScript linked in too.
 739       *
 740       * The two parameters match the first two parameters of print_question.
 741       *
 742       * @param object $question The question object.
 743       * @param object $state    The state object.
 744       *
 745       * @return an array of bits of HTML to add to the head of pages where
 746       * this question is print_question-ed in the body. The array should use
 747       * integer array keys, which have no significance.
 748       */
 749      function get_html_head_contributions(&$question, &$state) {
 750          // By default, we link to any of the files styles.css, styles.php,
 751          // script.js or script.php that exist in the plugin folder.
 752          // Core question types should not use this mechanism. Their styles
 753          // should be included in the standard theme.
 754  
 755          // We only do this once
 756          // for this question type, no matter how often this method is called.
 757          if ($this->already_done) {
 758              return array();
 759          }
 760          $this->already_done = true;
 761  
 762          $plugindir = $this->plugin_dir();
 763          $baseurl = $this->plugin_baseurl();
 764          $stylesheets = array();
 765          if (file_exists($plugindir . '/styles.css')) {
 766              $stylesheets[] = 'styles.css';
 767          }
 768          if (file_exists($plugindir . '/styles.php')) {
 769              $stylesheets[] = 'styles.php';
 770          }
 771          if (file_exists($plugindir . '/script.js')) {
 772              require_js($baseurl . '/script.js');
 773          }
 774          if (file_exists($plugindir . '/script.php')) {
 775              require_js($baseurl . '/script.php');
 776          }
 777          $contributions = array();
 778          foreach ($stylesheets as $stylesheet) {
 779              $contributions[] = '<link rel="stylesheet" type="text/css" href="' .
 780                      $baseurl . '/' . $stylesheet . '" />';
 781          }
 782          return $contributions;
 783      }
 784  
 785      /**
 786       * Prints the question including the number, grading details, content,
 787       * feedback and interactions
 788       *
 789       * This function prints the question including the question number,
 790       * grading details, content for the question, any feedback for the previously
 791       * submitted responses and the interactions. The default implementation calls
 792       * various other methods to print each of these parts and most question types
 793       * will just override those methods.
 794       * @param object $question The question to be rendered. Question type
 795       *                         specific information is included. The
 796       *                         maximum possible grade is in ->maxgrade. The name
 797       *                         prefix for any named elements is in ->name_prefix.
 798       * @param object $state    The state to render the question in. The grading
 799       *                         information is in ->grade, ->raw_grade and
 800       *                         ->penalty. The current responses are in
 801       *                         ->responses. This is an associative array (or the
 802       *                         empty string or null in the case of no responses
 803       *                         submitted). The last graded state is in
 804       *                         ->last_graded (hence the most recently graded
 805       *                         responses are in ->last_graded->responses). The
 806       *                         question type specific information is also
 807       *                         included.
 808       * @param integer $number  The number for this question.
 809       * @param object $cmoptions
 810       * @param object $options  An object describing the rendering options.
 811       */
 812      function print_question(&$question, &$state, $number, $cmoptions, $options) {
 813          /* The default implementation should work for most question types
 814          provided the member functions it calls are overridden where required.
 815          The layout is determined by the template question.html */
 816  
 817          global $CFG;
 818          $isgraded = question_state_is_graded($state->last_graded);
 819  
 820          // get the context so we can determine whether some extra links
 821          // should be shown.
 822          if (!empty($cmoptions->id)) {
 823              $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
 824              $context = get_context_instance(CONTEXT_MODULE, $cm->id);
 825              $cmorcourseid = '&amp;cmid='.$cm->id;
 826          } else if (!empty($cmoptions->course)) {
 827              $context = get_context_instance(CONTEXT_COURSE, $cmoptions->course);
 828              $cmorcourseid = '&amp;courseid='.$cmoptions->course;
 829          } else {
 830              error('Need to provide courseid or cmid to print_question.');
 831          }
 832  
 833          // For editing teachers print a link to an editing popup window
 834          $editlink = '';
 835          if (question_has_capability_on($question, 'edit')) {
 836              $stredit = get_string('edit');
 837              $linktext = '<img src="'.$CFG->pixpath.'/t/edit.gif" alt="'.$stredit.'" />';
 838              $editlink = link_to_popup_window('/question/question.php?inpopup=1&amp;id=' .
 839                      $question->id . $cmorcourseid, 'editquestion',
 840                      $linktext, false, false, $stredit, '', true);
 841          }
 842  
 843          $generalfeedback = '';
 844          if ($isgraded && $options->generalfeedback) {
 845              $generalfeedback = $this->format_text($question->generalfeedback,
 846                      $question->questiontextformat, $cmoptions);
 847          }
 848  
 849          $grade = '';
 850          if ($question->maxgrade and $options->scores) {
 851              if ($cmoptions->optionflags & QUESTION_ADAPTIVE) {
 852                  $grade = !$isgraded ? '--/' : round($state->last_graded->grade, $cmoptions->decimalpoints).'/';
 853              }
 854              $grade .= $question->maxgrade;
 855          }
 856  
 857          $comment = stripslashes($state->manualcomment);
 858          $commentlink = '';
 859  
 860          if (isset($options->questioncommentlink) && $context && has_capability('mod/quiz:grade', $context)) {
 861              $strcomment = get_string('commentorgrade', 'quiz');
 862              $question_to_comment = isset($question->randomquestionid) ? $question->randomquestionid : $question->id;
 863              $commentlink = '<div class="commentlink">'.link_to_popup_window ($options->questioncommentlink.'?attempt='.$state->attempt.'&amp;question='.$question_to_comment,
 864                               'commentquestion', $strcomment, 450, 650, $strcomment, 'none', true).'</div>';
 865          }
 866  
 867          $history = $this->history($question, $state, $number, $cmoptions, $options);
 868  
 869          include "$CFG->dirroot/question/type/question.html";
 870      }
 871  
 872      /*
 873       * Print history of responses
 874       *
 875       * Used by print_question()
 876       */
 877      function history($question, $state, $number, $cmoptions, $options) {
 878          $history = '';
 879          if(isset($options->history) and $options->history) {
 880              if ($options->history == 'all') {
 881                  // show all states
 882                  $states = get_records_select('question_states', "attempt = '$state->attempt' AND question = '$question->id' AND event > '0'", 'seq_number ASC');
 883              } else {
 884                  // show only graded states
 885                  $states = get_records_select('question_states', "attempt = '$state->attempt' AND question = '$question->id' AND event IN (".QUESTION_EVENTS_GRADED.")", 'seq_number ASC');
 886              }
 887              if (count($states) > 1) {
 888                  $strreviewquestion = get_string('reviewresponse', 'quiz');
 889                  $table = new stdClass;
 890                  $table->width = '100%';
 891                  if ($options->scores) {
 892                      $table->head  = array (
 893                                             get_string('numberabbr', 'quiz'),
 894                                             get_string('action', 'quiz'),
 895                                             get_string('response', 'quiz'),
 896                                             get_string('time'),
 897                                             get_string('score', 'quiz'),
 898                                             //get_string('penalty', 'quiz'),
 899                                             get_string('grade', 'quiz'),
 900                                             );
 901                  } else {
 902                      $table->head  = array (
 903                                             get_string('numberabbr', 'quiz'),
 904                                             get_string('action', 'quiz'),
 905                                             get_string('response', 'quiz'),
 906                                             get_string('time'),
 907                                             );
 908                  }
 909  
 910                  foreach ($states as $st) {
 911                      $st->responses[''] = $st->answer;
 912                      $this->restore_session_and_responses($question, $st);
 913                      $b = ($state->id == $st->id) ? '<b>' : '';
 914                      $be = ($state->id == $st->id) ? '</b>' : '';
 915                      if ($state->id == $st->id) {
 916                          $link = '<b>'.$st->seq_number.'</b>';
 917                      } else {
 918                          if(isset($options->questionreviewlink)) {
 919                              $link = link_to_popup_window ($options->questionreviewlink.'?state='.$st->id.'&amp;number='.$number,
 920                               'reviewquestion', $st->seq_number, 450, 650, $strreviewquestion, 'none', true);
 921                          } else {
 922                              $link = $st->seq_number;
 923                          }
 924                      }
 925                      if ($options->scores) {
 926                          $table->data[] = array (
 927                                                  $link,
 928                                                  $b.get_string('event'.$st->event, 'quiz').$be,
 929                                                  $b.$this->response_summary($question, $st).$be,
 930                                                  $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
 931                                                  $b.round($st->raw_grade, $cmoptions->decimalpoints).$be,
 932                                                  //$b.round($st->penalty, $cmoptions->decimalpoints).$be,
 933                                                  $b.round($st->grade, $cmoptions->decimalpoints).$be
 934                                                  );
 935                      } else {
 936                          $table->data[] = array (
 937                                                  $link,
 938                                                  $b.get_string('event'.$st->event, 'quiz').$be,
 939                                                  $b.$this->response_summary($question, $st).$be,
 940                                                  $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
 941                                                  );
 942                      }
 943                  }
 944                  $history = print_table($table, true);
 945              }
 946          }
 947          return $history;
 948      }
 949  
 950  
 951      /**
 952      * Prints the score obtained and maximum score available plus any penalty
 953      * information
 954      *
 955      * This function prints a summary of the scoring in the most recently
 956      * graded state (the question may not have been submitted for marking at
 957      * the current state). The default implementation should be suitable for most
 958      * question types.
 959      * @param object $question The question for which the grading details are
 960      *                         to be rendered. Question type specific information
 961      *                         is included. The maximum possible grade is in
 962      *                         ->maxgrade.
 963      * @param object $state    The state. In particular the grading information
 964      *                          is in ->grade, ->raw_grade and ->penalty.
 965      * @param object $cmoptions
 966      * @param object $options  An object describing the rendering options.
 967      */
 968      function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
 969          /* The default implementation prints the number of marks if no attempt
 970          has been made. Otherwise it displays the grade obtained out of the
 971          maximum grade available and a warning if a penalty was applied for the
 972          attempt and displays the overall grade obtained counting all previous
 973          responses (and penalties) */
 974  
 975          if (QUESTION_EVENTDUPLICATE == $state->event) {
 976              echo ' ';
 977              print_string('duplicateresponse', 'quiz');
 978          }
 979          if (!empty($question->maxgrade) && $options->scores) {
 980              if (question_state_is_graded($state->last_graded)) {
 981                  // Display the grading details from the last graded state
 982                  $grade = new stdClass;
 983                  $grade->cur = round($state->last_graded->grade, $cmoptions->decimalpoints);
 984                  $grade->max = $question->maxgrade;
 985                  $grade->raw = round($state->last_graded->raw_grade, $cmoptions->decimalpoints);
 986  
 987                  // let student know wether the answer was correct
 988                  echo '<div class="correctness ';
 989                  if ($state->last_graded->raw_grade >= $question->maxgrade/1.01) { // We divide by 1.01 so that rounding errors dont matter.
 990                      echo ' correct">';
 991                      print_string('correct', 'quiz');
 992                  } else if ($state->last_graded->raw_grade > 0) {
 993                      echo ' partiallycorrect">';
 994                      print_string('partiallycorrect', 'quiz');
 995                  } else {
 996                      echo ' incorrect">';
 997                      print_string('incorrect', 'quiz');
 998                  }
 999                  echo '</div>';
1000  
1001                  echo '<div class="gradingdetails">';
1002                  // print grade for this submission
1003                  print_string('gradingdetails', 'quiz', $grade);
1004                  if ($cmoptions->penaltyscheme) {
1005                      // print details of grade adjustment due to penalties
1006                      if ($state->last_graded->raw_grade > $state->last_graded->grade){
1007                          echo ' ';
1008                          print_string('gradingdetailsadjustment', 'quiz', $grade);
1009                      }
1010                      // print info about new penalty
1011                      // penalty is relevant only if the answer is not correct and further attempts are possible
1012                      if (($state->last_graded->raw_grade < $question->maxgrade / 1.01)
1013                                  and (QUESTION_EVENTCLOSEANDGRADE !== $state->event)) {
1014  
1015                          if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
1016                              // A penalty was applied so display it
1017                              echo ' ';
1018                              print_string('gradingdetailspenalty', 'quiz', $state->last_graded->penalty);
1019                          } else {
1020                              /* No penalty was applied even though the answer was
1021                              not correct (eg. a syntax error) so tell the student
1022                              that they were not penalised for the attempt */
1023                              echo ' ';
1024                              print_string('gradingdetailszeropenalty', 'quiz');
1025                          }
1026                      }
1027                  }
1028                  echo '</div>';
1029              }
1030          }
1031      }
1032  
1033      /**
1034      * Prints the main content of the question including any interactions
1035      *
1036      * This function prints the main content of the question including the
1037      * interactions for the question in the state given. The last graded responses
1038      * are printed or indicated and the current responses are selected or filled in.
1039      * Any names (eg. for any form elements) are prefixed with $question->name_prefix.
1040      * This method is called from the print_question method.
1041      * @param object $question The question to be rendered. Question type
1042      *                         specific information is included. The name
1043      *                         prefix for any named elements is in ->name_prefix.
1044      * @param object $state    The state to render the question in. The grading
1045      *                         information is in ->grade, ->raw_grade and
1046      *                         ->penalty. The current responses are in
1047      *                         ->responses. This is an associative array (or the
1048      *                         empty string or null in the case of no responses
1049      *                         submitted). The last graded state is in
1050      *                         ->last_graded (hence the most recently graded
1051      *                         responses are in ->last_graded->responses). The
1052      *                         question type specific information is also
1053      *                         included.
1054      *                         The state is passed by reference because some adaptive
1055      *                         questions may want to update it during rendering
1056      * @param object $cmoptions
1057      * @param object $options  An object describing the rendering options.
1058      */
1059      function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
1060          /* This default implementation prints an error and must be overridden
1061          by all question type implementations, unless the default implementation
1062          of print_question has been overridden. */
1063  
1064          notify('Error: Question formulation and input controls has not'
1065                 .'  been implemented for question type '.$this->name());
1066      }
1067  
1068      /**
1069      * Prints the submit button(s) for the question in the given state
1070      *
1071      * This function prints the submit button(s) for the question in the
1072      * given state. The name of any button created will be prefixed with the
1073      * unique prefix for the question in $question->name_prefix. The suffix
1074      * 'submit' is reserved for the single question submit button and the suffix
1075      * 'validate' is reserved for the single question validate button (for
1076      * question types which support it). Other suffixes will result in a response
1077      * of that name in $state->responses which the printing and grading methods
1078      * can then use.
1079      * @param object $question The question for which the submit button(s) are to
1080      *                         be rendered. Question type specific information is
1081      *                         included. The name prefix for any
1082      *                         named elements is in ->name_prefix.
1083      * @param object $state    The state to render the buttons for. The
1084      *                         question type specific information is also
1085      *                         included.
1086      * @param object $cmoptions
1087      * @param object $options  An object describing the rendering options.
1088      */
1089      function print_question_submit_buttons(&$question, &$state, $cmoptions, $options) {
1090          /* The default implementation should be suitable for most question
1091          types. It prints a mark button in the case where individual marking is
1092          allowed. */
1093  
1094          if (($cmoptions->optionflags & QUESTION_ADAPTIVE) and !$options->readonly) {
1095              echo '<input type="submit" name="', $question->name_prefix, 'submit" value="',
1096                      get_string('mark', 'quiz'), '" class="submit btn" onclick="',
1097                      "form.action = form.action + '#q", $question->id, "'; return true;", '" />';
1098          }
1099      }
1100  
1101      /**
1102      * Return a summary of the student response
1103      *
1104      * This function returns a short string of no more than a given length that
1105      * summarizes the student's response in the given $state. This is used for
1106      * example in the response history table. This string should already be,
1107      * for output.
1108      * @return string         The summary of the student response
1109      * @param object $question
1110      * @param object $state   The state whose responses are to be summarized
1111      * @param int $length     The maximum length of the returned string
1112      */
1113      function response_summary($question, $state, $length=80) {
1114          // This should almost certainly be overridden
1115          $responses = $this->get_actual_response($question, $state);
1116          if (empty($responses) || !is_array($responses)) {
1117              $responses = array();
1118          }
1119          if (is_array($responses)) {
1120              $responses = implode(',', array_map('s', $responses));
1121          }
1122          return shorten_text($responses, $length);
1123      }
1124  
1125      /**
1126      * Renders the question for printing and returns the LaTeX source produced
1127      *
1128      * This function should render the question suitable for a printed problem
1129      * or solution sheet in LaTeX and return the rendered output.
1130      * @return string          The LaTeX output.
1131      * @param object $question The question to be rendered. Question type
1132      *                         specific information is included.
1133      * @param object $state    The state to render the question in. The
1134      *                         question type specific information is also
1135      *                         included.
1136      * @param object $cmoptions
1137      * @param string $type     Indicates if the question or the solution is to be
1138      *                         rendered with the values 'question' and
1139      *                         'solution'.
1140      */
1141      function get_texsource(&$question, &$state, $cmoptions, $type) {
1142          // The default implementation simply returns a string stating that
1143          // the question is only available online.
1144  
1145          return get_string('onlineonly', 'texsheet');
1146      }
1147  
1148      /**
1149      * Compares two question states for equivalence of the student's responses
1150      *
1151      * The responses for the two states must be examined to see if they represent
1152      * equivalent answers to the question by the student. This method will be
1153      * invoked for each of the previous states of the question before grading
1154      * occurs. If the student is found to have already attempted the question
1155      * with equivalent responses then the attempt at the question is ignored;
1156      * grading does not occur and the state does not change. Thus they are not
1157      * penalized for this case.
1158      * @return boolean
1159      * @param object $question  The question for which the states are to be
1160      *                          compared. Question type specific information is
1161      *                          included.
1162      * @param object $state     The state of the question. The responses are in
1163      *                          ->responses. This is the only field of $state
1164      *                          that it is safe to use.
1165      * @param object $teststate The state whose responses are to be
1166      *                          compared. The state will be of the same age or
1167      *                          older than $state. If possible, the method should
1168      *                          only use the field $teststate->responses, however
1169      *                          any field that is set up by restore_session_and_responses
1170      *                          can be used.
1171      */
1172      function compare_responses(&$question, $state, $teststate) {
1173          // The default implementation performs a comparison of the response
1174          // arrays. The ordering of the arrays does not matter.
1175          // Question types may wish to override this (eg. to ignore trailing
1176          // white space or to make "7.0" and "7" compare equal).
1177  
1178          // In php neither == nor === compare arrays the way you want. The following
1179          // ensures that the arrays have the same keys, with the same values.
1180          $result = false;
1181          $diff1 = array_diff_assoc($state->responses, $teststate->responses);
1182          if (empty($diff1)) {
1183              $diff2 = array_diff_assoc($teststate->responses, $state->responses);
1184              $result =  empty($diff2);
1185          }
1186  
1187          return $result;
1188      }
1189  
1190      /**
1191      * Checks whether a response matches a given answer
1192      *
1193      * This method only applies to questions that use teacher-defined answers
1194      *
1195      * @return boolean
1196      */
1197      function test_response(&$question, &$state, $answer) {
1198          $response = isset($state->responses['']) ? $state->responses[''] : '';
1199          return ($response == $answer->answer);
1200      }
1201  
1202      /**
1203      * Performs response processing and grading
1204      *
1205      * This function performs response processing and grading and updates
1206      * the state accordingly.
1207      * @return boolean         Indicates success or failure.
1208      * @param object $question The question to be graded. Question type
1209      *                         specific information is included.
1210      * @param object $state    The state of the question to grade. The current
1211      *                         responses are in ->responses. The last graded state
1212      *                         is in ->last_graded (hence the most recently graded
1213      *                         responses are in ->last_graded->responses). The
1214      *                         question type specific information is also
1215      *                         included. The ->raw_grade and ->penalty fields
1216      *                         must be updated. The method is able to
1217      *                         close the question session (preventing any further
1218      *                         attempts at this question) by setting
1219      *                         $state->event to QUESTION_EVENTCLOSEANDGRADE
1220      * @param object $cmoptions
1221      */
1222      function grade_responses(&$question, &$state, $cmoptions) {
1223          // The default implementation uses the test_response method to
1224          // compare what the student entered against each of the possible
1225          // answers stored in the question, and uses the grade from the
1226          // first one that matches. It also sets the marks and penalty.
1227          // This should be good enought for most simple question types.
1228  
1229          $state->raw_grade = 0;
1230          foreach($question->options->answers as $answer) {
1231              if($this->test_response($question, $state, $answer)) {
1232                  $state->raw_grade = $answer->fraction;
1233                  break;
1234              }
1235          }
1236  
1237          // Make sure we don't assign negative or too high marks.
1238          $state->raw_grade = min(max((float) $state->raw_grade,
1239                              0.0), 1.0) * $question->maxgrade;
1240  
1241          // Update the penalty.
1242          $state->penalty = $question->penalty * $question->maxgrade;
1243  
1244          // mark the state as graded
1245          $state->event = ($state->event ==  QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
1246  
1247          return true;
1248      }
1249  
1250  
1251      /**
1252      * Includes configuration settings for the question type on the quiz admin
1253      * page
1254      *
1255      * TODO: It makes no sense any longer to do the admin for question types
1256      * from the quiz admin page. This should be changed.
1257      * Returns an array of objects describing the options for the question type
1258      * to be included on the quiz module admin page.
1259      * Configuration options can be included by setting the following fields in
1260      * the object:
1261      * ->name           The name of the option within this question type.
1262      *                  The full option name will be constructed as
1263      *                  "quiz_{$this->name()}_$name", the human readable name
1264      *                  will be displayed with get_string($name, 'quiz').
1265      * ->code           The code to display the form element, help button, etc.
1266      *                  i.e. the content for the central table cell. Be sure
1267      *                  to name the element "quiz_{$this->name()}_$name" and
1268      *                  set the value to $CFG->{"quiz_{$this->name()}_$name"}.
1269      * ->help           Name of the string from the quiz module language file
1270      *                  to be used for the help message in the third column of
1271      *                  the table. An empty string (or the field not set)
1272      *                  means to leave the box empty.
1273      * Links to custom settings pages can be included by setting the following
1274      * fields in the object:
1275      * ->name           The name of the link text string.
1276      *                  get_string($name, 'quiz') will be called.
1277      * ->link           The filename part of the URL for the link. The full URL
1278      *                  is contructed as
1279      *                  "$CFG->wwwroot/question/type/{$this->name()}/$link?sesskey=$sesskey"
1280      *                  [but with the relavant calls to the s and rawurlencode
1281      *                  functions] where $sesskey is the sesskey for the user.
1282      * @return array    Array of objects describing the configuration options to
1283      *                  be included on the quiz module admin page.
1284      */
1285      function get_config_options() {
1286          // No options by default
1287  
1288          return false;
1289      }
1290  
1291      /**
1292      * Returns true if the editing wizard is finished, false otherwise.
1293      *
1294      * The default implementation returns true, which is suitable for all question-
1295      * types that only use one editing form. This function is used in
1296      * question.php to decide whether we can regrade any states of the edited
1297      * question and redirect to edit.php.
1298      *
1299      * The dataset dependent question-type, which is extended by the calculated
1300      * question-type, overwrites this method because it uses multiple pages (i.e.
1301      * a wizard) to set up the question and associated datasets.
1302      *
1303      * @param object $form  The data submitted by the previous page.
1304      *
1305      * @return boolean      Whether the wizard's last page was submitted or not.
1306      */
1307      function finished_edit_wizard(&$form) {
1308          //In the default case there is only one edit page.
1309          return true;
1310      }
1311  
1312      /**
1313      * Prints a table of course modules in which the question is used
1314      *
1315      * TODO: This should be made quiz-independent
1316      *
1317      * This function is used near the end of the question edit forms in all question types
1318      * It prints the table of quizzes in which the question is used
1319      * containing checkboxes to allow the teacher to replace the old question version
1320      *
1321      * @param object $question
1322      * @param object $course
1323      * @param integer $cmid optional The id of the course module currently being edited
1324      */
1325      function print_replacement_options($question, $course, $cmid='0') {
1326  
1327          // Disable until the versioning code has been fixed
1328          if (true) {
1329              return;
1330          }
1331  
1332          // no need to display replacement options if the question is new
1333          if(empty($question->id)) {
1334              return true;
1335          }
1336  
1337          // get quizzes using the question (using the question_instances table)
1338          $quizlist = array();
1339          if(!$instances = get_records('quiz_question_instances', 'question', $question->id)) {
1340              $instances = array();
1341          }
1342          foreach($instances as $instance) {
1343              $quizlist[$instance->quiz] = $instance->quiz;
1344          }
1345          $quizlist = implode(',', $quizlist);
1346          if(empty($quizlist) or !$quizzes = get_records_list('quiz', 'id', $quizlist)) {
1347              $quizzes = array();
1348          }
1349  
1350          // do the printing
1351          if(count($quizzes) > 0) {
1352              // print the table
1353              $strquizname  = get_string('modulename', 'quiz');
1354              $strdoreplace = get_string('replace', 'quiz');
1355              $straffectedstudents = get_string('affectedstudents', 'quiz', $course->students);
1356              echo "<tr valign=\"top\">\n";
1357              echo "<td align=\"right\"><b>".get_string("replacementoptions", "quiz").":</b></td>\n";
1358              echo "<td align=\"left\">\n";
1359              echo "<table cellpadding=\"5\" align=\"left\" class=\"generalbox\" width=\"100%\">\n";
1360              echo "<tr>\n";
1361              echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strquizname</th>\n";
1362              echo "<th align=\"center\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$strdoreplace</th>\n";
1363              echo "<th align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"generaltableheader c0\" scope=\"col\">$straffectedstudents</th>\n";
1364              echo "</tr>\n";
1365              foreach($quizzes as $quiz) {
1366                  // work out whethere it should be checked by default
1367                  $checked = '';
1368                  if((int)$cmid === (int)$quiz->id
1369                      or empty($quiz->usercount)) {
1370                      $checked = "checked=\"checked\"";
1371                  }
1372  
1373                  // find how many different students have already attempted this quiz
1374                  $students = array();
1375                  if($attempts = get_records_select('quiz_attempts', "quiz = '$quiz->id' AND preview = '0'")) {
1376                      foreach($attempts as $attempt) {
1377                          if (record_exists('question_states', 'attempt', $attempt->uniqueid, 'question', $question->id, 'originalquestion', 0)) {
1378                              $students[$attempt->userid] = 1;
1379                          }
1380                      }
1381                  }
1382                  $studentcount = count($students);
1383  
1384                  $strstudents = $studentcount === 1 ? $course->student : $course->students;
1385                  echo "<tr>\n";
1386                  echo "<td align=\"left\" class=\"generaltablecell c0\">".format_string($quiz->name)."</td>\n";
1387                  echo "<td align=\"center\" class=\"generaltablecell c0\"><input name=\"q{$quiz->id}replace\" type=\"checkbox\" ".$checked." /></td>\n";
1388                  echo "<td align=\"left\" class=\"generaltablecell c0\">".(($studentcount) ? $studentcount.' '.$strstudents : '-')."</td>\n";
1389                  echo "</tr>\n";
1390              }
1391              echo "</table>\n";
1392          }
1393          echo "</td></tr>\n";
1394      }
1395  
1396      /**
1397       * Call format_text from weblib.php with the options appropriate to question types.
1398       *
1399       * @param string $text the text to format.
1400       * @param integer $text the type of text. Normally $question->questiontextformat.
1401       * @param object $cmoptions the context the string is being displayed in. Only $cmoptions->course is used.
1402       * @return string the formatted text.
1403       */
1404      function format_text($text, $textformat, $cmoptions = NULL) {
1405          $formatoptions = new stdClass;
1406          $formatoptions->noclean = true;
1407          $formatoptions->para = false;
1408          return format_text($text, $textformat, $formatoptions, $cmoptions === NULL ? NULL : $cmoptions->course);
1409      }
1410  
1411      /*
1412       * Find all course / site files linked from a question.
1413       *
1414       * Need to check for links to files in question_answers.answer and feedback
1415       * and in question table in generalfeedback and questiontext fields. Methods
1416       * on child classes will also check extra question specific fields.
1417       *
1418       * Needs to be overriden for child classes that have extra fields containing
1419       * html.
1420       *
1421       * @param string html the html to search
1422       * @param int courseid search for files for courseid course or set to siteid for
1423       *              finding site files.
1424       * @return array of url, relative url is key and array with one item = question id as value
1425       *                  relative url is relative to course/site files directory root.
1426       */
1427      function find_file_links($question, $courseid){
1428          $urls = array();
1429  
1430      /// Question image
1431          if ($question->image != ''){
1432              if (substr(strtolower($question->image), 0, 7) == 'http://') {
1433                  $matches = array();
1434  
1435                  //support for older questions where we have a complete url in image field
1436                  if (preg_match('!^'.question_file_links_base_url($courseid).'(.*)!i', $question->image, $matches)){
1437                      if ($cleanedurl = question_url_check($urls[$matches[2]])){
1438                          $urls[$cleanedurl] = null;
1439                      }
1440                  }
1441              } else {
1442                  if ($question->image != ''){
1443                      if ($cleanedurl = question_url_check($question->image)){
1444                          $urls[$cleanedurl] = null;//will be set later
1445                      }
1446                  }
1447  
1448              }
1449  
1450          }
1451  
1452      /// Questiontext and general feedback.
1453          $urls += question_find_file_links_from_html($question->questiontext, $courseid);
1454          $urls += question_find_file_links_from_html($question->generalfeedback, $courseid);
1455  
1456      /// Answers, if this question uses them.
1457          if (isset($question->options->answers)){
1458              foreach ($question->options->answers as $answerkey => $answer){
1459              /// URLs in the answers themselves, if appropriate.
1460                  if ($this->has_html_answers()) {
1461                      $urls += question_find_file_links_from_html($answer->answer, $courseid);
1462                  }
1463              /// URLs in the answer feedback.
1464                  $urls += question_find_file_links_from_html($answer->feedback, $courseid);
1465              }
1466          }
1467  
1468      /// Set all the values of the array to the question object
1469          if ($urls){
1470              $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id)));
1471          }
1472          return $urls;
1473      }
1474      /*
1475       * Find all course / site files linked from a question.
1476       *
1477       * Need to check for links to files in question_answers.answer and feedback
1478       * and in question table in generalfeedback and questiontext fields. Methods
1479       * on child classes will also check extra question specific fields.
1480       *
1481       * Needs to be overriden for child classes that have extra fields containing
1482       * html.
1483       *
1484       * @param string html the html to search
1485       * @param int course search for files for courseid course or set to siteid for
1486       *              finding site files.
1487       * @return array of files, file name is key and array with one item = question id as value
1488       */
1489      function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
1490          global $CFG;
1491          $updateqrec = false;
1492  
1493      /// Question image
1494          if (!empty($question->image)){
1495              //support for older questions where we have a complete url in image field
1496              if (substr(strtolower($question->image), 0, 7) == 'http://') {
1497                  $questionimage = preg_replace('!^'.question_file_links_base_url($fromcourseid).preg_quote($url, '!').'$!i', $destination, $question->image, 1);
1498              } else {
1499                  $questionimage = preg_replace('!^'.preg_quote($url, '!').'$!i', $destination, $question->image, 1);
1500              }
1501              if ($questionimage != $question->image){
1502                  $question->image = $questionimage;
1503                  $updateqrec = true;
1504              }
1505          }
1506  
1507      /// Questiontext and general feedback.
1508          $question->questiontext = question_replace_file_links_in_html($question->questiontext, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
1509          $question->generalfeedback = question_replace_file_links_in_html($question->generalfeedback, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
1510  
1511      /// If anything has changed, update it in the database.
1512          if ($updateqrec){
1513              if (!update_record('question', addslashes_recursive($question))){
1514                  error ('Couldn\'t update question '.$question->name);
1515              }
1516          }
1517  
1518  
1519      /// Answers, if this question uses them.
1520          if (isset($question->options->answers)){
1521              //answers that do not need updating have been unset
1522              foreach ($question->options->answers as $answer){
1523                  $answerchanged = false;
1524              /// URLs in the answers themselves, if appropriate.
1525                  if ($this->has_html_answers()) {
1526                      $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1527                  }
1528              /// URLs in the answer feedback.
1529                  $answer->feedback = question_replace_file_links_in_html($answer->feedback, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1530              /// If anything has changed, update it in the database.
1531                  if ($answerchanged){
1532                      if (!update_record('question_answers', addslashes_recursive($answer))){
1533                          error ('Couldn\'t update question ('.$question->name.') answer '.$answer->id);
1534                      }
1535                  }
1536              }
1537          }
1538      }
1539      /**
1540       * @return the best link to pass to print_error.
1541       * @param $cmoptions as passed in from outside.
1542       */
1543      function error_link($cmoptions) {
1544          global $CFG;
1545          $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
1546          if (!empty($cm->id)) {
1547              return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
1548          } else if (!empty($cm->course)) {
1549              return $CFG->wwwroot . '/course/view.php?id=' . $cm->course;
1550          } else {
1551              return '';
1552          }
1553      }
1554  
1555  /// BACKUP FUNCTIONS ////////////////////////////
1556  
1557      /*
1558       * Backup the data in the question
1559       *
1560       * This is used in question/backuplib.php
1561       */
1562      function backup($bf,$preferences,$question,$level=6) {
1563          // The default type has nothing to back up
1564          return true;
1565      }
1566  
1567  /// RESTORE FUNCTIONS /////////////////
1568  
1569      /*
1570       * Restores the data in the question
1571       *
1572       * This is used in question/restorelib.php
1573       */
1574      function restore($old_question_id,$new_question_id,$info,$restore) {
1575          // The default question type has nothing to restore
1576          return true;
1577      }
1578  
1579      function restore_map($old_question_id,$new_question_id,$info,$restore) {
1580          // There is nothing to decode
1581          return true;
1582      }
1583  
1584      function restore_recode_answer($state, $restore) {
1585          // There is nothing to decode
1586          return $state->answer;
1587      }
1588  
1589      /**
1590       * Abstract function implemented by each question type. It runs all the code
1591       * required to set up and save a question of any type for testing purposes.
1592       * Alternate DB table prefix may be used to facilitate data deletion.
1593       */
1594      function generate_test($name, $courseid=null) {
1595          $form = new stdClass();
1596          $form->name = $name;
1597          $form->questiontextformat = 1;
1598          $form->questiontext = 'test question, generated by script';
1599          $form->defaultgrade = 1;
1600          $form->penalty = 0.1;
1601          $form->generalfeedback = "Well done";
1602  
1603          $context = get_context_instance(CONTEXT_COURSE, $courseid);
1604          $newcategory = question_make_default_categories(array($context));
1605          $form->category = $newcategory->id . ',1';
1606  
1607          $question = new stdClass();
1608          $question->courseid = $courseid;
1609          $question->qtype = $this->qtype;
1610          return array($form, $question);
1611      }
1612  }
1613  ?>


Generated: Wed Jan 14 11:33:29 2009 Cross-referenced by PHPXref 0.7