Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

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