Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   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  defined('MOODLE_INTERNAL') || die();
  18  
  19  /**
  20   * Quiz module test data generator class
  21   *
  22   * @package    moodlecore
  23   * @subpackage question
  24   * @copyright  2013 The Open University
  25   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  class core_question_generator extends component_generator_base {
  28  
  29      /**
  30       * @var number of created instances
  31       */
  32      protected $categorycount = 0;
  33  
  34      public function reset() {
  35          $this->categorycount = 0;
  36      }
  37  
  38      /**
  39       * Create a new question category.
  40       * @param array|stdClass $record
  41       * @return stdClass question_categories record.
  42       */
  43      public function create_question_category($record = null) {
  44          global $DB;
  45  
  46          $this->categorycount++;
  47  
  48          $defaults = array(
  49              'name'       => 'Test question category ' . $this->categorycount,
  50              'info'       => '',
  51              'infoformat' => FORMAT_HTML,
  52              'stamp'      => make_unique_id_code(),
  53              'sortorder'  => 999,
  54              'idnumber'   => null
  55          );
  56  
  57          $record = $this->datagenerator->combine_defaults_and_record($defaults, $record);
  58  
  59          if (!isset($record['contextid'])) {
  60              $record['contextid'] = context_system::instance()->id;
  61          }
  62          if (!isset($record['parent'])) {
  63              $record['parent'] = question_get_top_category($record['contextid'], true)->id;
  64          }
  65          $record['id'] = $DB->insert_record('question_categories', $record);
  66          return (object) $record;
  67      }
  68  
  69      /**
  70       * Create a new question. The question is initialised using one of the
  71       * examples from the appropriate {@link question_test_helper} subclass.
  72       * Then, any files you want to change from the value in the base example you
  73       * can override using $overrides.
  74       *
  75       * @param string $qtype the question type to create an example of.
  76       * @param string $which as for the corresponding argument of
  77       *      {@link question_test_helper::get_question_form_data}. null for the default one.
  78       * @param array|stdClass $overrides any fields that should be different from the base example.
  79       * @return stdClass the question data.
  80       */
  81      public function create_question($qtype, $which = null, $overrides = null) {
  82          global $CFG;
  83          require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
  84  
  85          $fromform = test_question_maker::get_question_form_data($qtype, $which);
  86          $fromform = (object) $this->datagenerator->combine_defaults_and_record(
  87                  (array) $fromform, $overrides);
  88  
  89          $question = new stdClass();
  90          $question->category  = $fromform->category;
  91          $question->qtype     = $qtype;
  92          $question->createdby = 0;
  93          $question->idnumber = null;
  94  
  95          return $this->update_question($question, $which, $overrides);
  96      }
  97  
  98      /**
  99       * Create a tag on a question.
 100       *
 101       * @param array $data with two elements ['questionid' => 123, 'tag' => 'mytag'].
 102       */
 103      public function create_question_tag(array $data): void {
 104          $question = question_bank::load_question($data['questionid']);
 105          core_tag_tag::add_item_tag('core_question', 'question', $question->id,
 106                  context::instance_by_id($question->contextid), $data['tag'], 0);
 107      }
 108  
 109      /**
 110       * Update an existing question.
 111       *
 112       * @param stdClass $question the question data to update.
 113       * @param string $which as for the corresponding argument of
 114       *      {@link question_test_helper::get_question_form_data}. null for the default one.
 115       * @param array|stdClass $overrides any fields that should be different from the base example.
 116       * @return stdClass the question data.
 117       */
 118      public function update_question($question, $which = null, $overrides = null) {
 119          global $CFG, $DB;
 120          require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
 121  
 122          $qtype = $question->qtype;
 123  
 124          $fromform = test_question_maker::get_question_form_data($qtype, $which);
 125          $fromform = (object) $this->datagenerator->combine_defaults_and_record(
 126                  (array) $question, $fromform);
 127          $fromform = (object) $this->datagenerator->combine_defaults_and_record(
 128                  (array) $fromform, $overrides);
 129  
 130          $question = question_bank::get_qtype($qtype)->save_question($question, $fromform);
 131  
 132          if ($overrides && (array_key_exists('createdby', $overrides) || array_key_exists('modifiedby', $overrides))) {
 133              // Manually update the createdby and modifiedby because questiontypebase forces
 134              // current user and some tests require a specific user.
 135              if (array_key_exists('createdby', $overrides)) {
 136                  $question->createdby = $overrides['createdby'];
 137              }
 138              if (array_key_exists('modifiedby', $overrides)) {
 139                  $question->modifiedby = $overrides['modifiedby'];
 140              }
 141              $DB->update_record('question', $question);
 142          }
 143  
 144          return $question;
 145      }
 146  
 147      /**
 148       * Setup a course category, course, a question category, and 2 questions
 149       * for testing.
 150       *
 151       * @param string $type The type of question category to create.
 152       * @return array The created data objects
 153       */
 154      public function setup_course_and_questions($type = 'course') {
 155          $datagenerator = $this->datagenerator;
 156          $category = $datagenerator->create_category();
 157          $course = $datagenerator->create_course([
 158              'numsections' => 5,
 159              'category' => $category->id
 160          ]);
 161  
 162          switch ($type) {
 163              case 'category':
 164                  $context = context_coursecat::instance($category->id);
 165                  break;
 166  
 167              case 'system':
 168                  $context = context_system::instance();
 169                  break;
 170  
 171              default:
 172                  $context = context_course::instance($course->id);
 173                  break;
 174          }
 175  
 176          $qcat = $this->create_question_category(['contextid' => $context->id]);
 177  
 178          $questions = array(
 179                  $this->create_question('shortanswer', null, ['category' => $qcat->id]),
 180                  $this->create_question('shortanswer', null, ['category' => $qcat->id]),
 181          );
 182  
 183          return array($category, $course, $qcat, $questions);
 184      }
 185  
 186      /**
 187       * This method can construct what the post data would be to simulate a user submitting
 188       * responses to a number of questions within a question usage.
 189       *
 190       * In the responses array, the array keys are the slot numbers for which a response will
 191       * be submitted. You can submit a response to any number of responses within the usage.
 192       * There is no need to do them all. The values are a string representation of the response.
 193       * The exact meaning of that depends on the particular question type. These strings
 194       * are passed to the un_summarise_response method of the question to decode.
 195       *
 196       * @param question_usage_by_activity $quba the question usage.
 197       * @param array $responses the resonses to submit, in the format described above.
 198       * @param bool $checkbutton if simulate a click on the check button for each question, else simulate save.
 199       *      This should only be used with behaviours that have a check button.
 200       * @return array that can be passed to methods like $quba->process_all_actions as simulated POST data.
 201       */
 202      public function get_simulated_post_data_for_questions_in_usage(
 203              question_usage_by_activity $quba, array $responses, $checkbutton) {
 204          $postdata = [];
 205  
 206          foreach ($responses as $slot => $responsesummary) {
 207              $postdata += $this->get_simulated_post_data_for_question_attempt(
 208                      $quba->get_question_attempt($slot), $responsesummary, $checkbutton);
 209          }
 210  
 211          return $postdata;
 212      }
 213  
 214      /**
 215       * This method can construct what the post data would be to simulate a user submitting
 216       * responses to one particular question attempt.
 217       *
 218       * The $responsesummary is a string representation of the response to be submitted.
 219       * The exact meaning of that depends on the particular question type. These strings
 220       * are passed to the un_summarise_response method of the question to decode.
 221       *
 222       * @param question_attempt $qa the question attempt for which we are generating POST data.
 223       * @param string $responsesummary a textual summary of the response, as described above.
 224       * @param bool $checkbutton if simulate a click on the check button, else simulate save.
 225       *      This should only be used with behaviours that have a check button.
 226       * @return array the simulated post data that can be passed to $quba->process_all_actions.
 227       */
 228      public function get_simulated_post_data_for_question_attempt(
 229              question_attempt $qa, $responsesummary, $checkbutton) {
 230  
 231          $question = $qa->get_question();
 232          if (!$question instanceof question_with_responses) {
 233              return [];
 234          }
 235  
 236          $postdata = [];
 237          $postdata[$qa->get_control_field_name('sequencecheck')] = (string)$qa->get_sequence_check_count();
 238          $postdata[$qa->get_flag_field_name()] = (string)(int)$qa->is_flagged();
 239  
 240          $response = $question->un_summarise_response($responsesummary);
 241          foreach ($response as $name => $value) {
 242              $postdata[$qa->get_qt_field_name($name)] = (string)$value;
 243          }
 244  
 245          // TODO handle behaviour variables better than this.
 246          if ($checkbutton) {
 247              $postdata[$qa->get_behaviour_field_name('submit')] = 1;
 248          }
 249  
 250          return $postdata;
 251      }
 252  }