MDL-69068 behat: New step to support singular generators
[moodle.git] / question / tests / generator / lib.php
CommitLineData
ba203de1
TH
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
17defined('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 */
27class 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,
ba203de1
TH
50 'info' => '',
51 'infoformat' => FORMAT_HTML,
d7d4a097 52 'stamp' => make_unique_id_code(),
ba203de1 53 'sortorder' => 999,
6189fda4 54 'idnumber' => null
ba203de1
TH
55 );
56
57 $record = $this->datagenerator->combine_defaults_and_record($defaults, $record);
869320c7
SR
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 }
ba203de1
TH
65 $record['id'] = $DB->insert_record('question_categories', $record);
66 return (object) $record;
67 }
0a606a2b
TH
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.
c689726d 74 *
0a606a2b
TH
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.
c689726d 79 * @return stdClass the question data.
0a606a2b
TH
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;
6189fda4 93 $question->idnumber = null;
d44064cd
AN
94
95 return $this->update_question($question, $which, $overrides);
96 }
97
c689726d
TH
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
d44064cd
AN
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.
c689726d 116 * @return stdClass the question data.
d44064cd
AN
117 */
118 public function update_question($question, $which = null, $overrides = null) {
221bfed3 119 global $CFG, $DB;
d44064cd
AN
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
221bfed3
AN
130 $question = question_bank::get_qtype($qtype)->save_question($question, $fromform);
131
132 if ($overrides && array_key_exists('createdby', $overrides)) {
133 // Manually update the createdby because questiontypebase forces current user and some tests require a
134 // specific user.
135 $question->createdby = $overrides['createdby'];
136 $DB->update_record('question', $question);
137 }
138
139 return $question;
0a606a2b 140 }
c53beb73
RW
141
142 /**
143 * Setup a course category, course, a question category, and 2 questions
144 * for testing.
145 *
146 * @param string $type The type of question category to create.
147 * @return array The created data objects
148 */
149 public function setup_course_and_questions($type = 'course') {
150 $datagenerator = $this->datagenerator;
151 $category = $datagenerator->create_category();
152 $course = $datagenerator->create_course([
153 'numsections' => 5,
154 'category' => $category->id
155 ]);
156
157 switch ($type) {
158 case 'category':
159 $context = context_coursecat::instance($category->id);
160 break;
161
162 case 'system':
163 $context = context_system::instance();
164 break;
165
166 default:
167 $context = context_course::instance($course->id);
168 break;
169 }
170
171 $qcat = $this->create_question_category(['contextid' => $context->id]);
172
173 $questions = array(
174 $this->create_question('shortanswer', null, ['category' => $qcat->id]),
175 $this->create_question('shortanswer', null, ['category' => $qcat->id]),
176 );
177
178 return array($category, $course, $qcat, $questions);
179 }
5c0654d7
SL
180
181 /**
182 * This method can construct what the post data would be to simulate a user submitting
183 * responses to a number of questions within a question usage.
184 *
185 * In the responses array, the array keys are the slot numbers for which a response will
186 * be submitted. You can submit a response to any number of responses within the usage.
187 * There is no need to do them all. The values are a string representation of the response.
188 * The exact meaning of that depends on the particular question type. These strings
189 * are passed to the un_summarise_response method of the question to decode.
190 *
191 * @param question_usage_by_activity $quba the question usage.
192 * @param array $responses the resonses to submit, in the format described above.
c5499eda
TH
193 * @param bool $checkbutton if simulate a click on the check button for each question, else simulate save.
194 * This should only be used with behaviours that have a check button.
5c0654d7
SL
195 * @return array that can be passed to methods like $quba->process_all_actions as simulated POST data.
196 */
197 public function get_simulated_post_data_for_questions_in_usage(
c5499eda 198 question_usage_by_activity $quba, array $responses, $checkbutton) {
5c0654d7
SL
199 $postdata = [];
200
201 foreach ($responses as $slot => $responsesummary) {
202 $postdata += $this->get_simulated_post_data_for_question_attempt(
c5499eda 203 $quba->get_question_attempt($slot), $responsesummary, $checkbutton);
5c0654d7
SL
204 }
205
206 return $postdata;
207 }
208
209 /**
210 * This method can construct what the post data would be to simulate a user submitting
211 * responses to one particular question attempt.
212 *
213 * The $responsesummary is a string representation of the response to be submitted.
214 * The exact meaning of that depends on the particular question type. These strings
215 * are passed to the un_summarise_response method of the question to decode.
216 *
217 * @param question_attempt $qa the question attempt for which we are generating POST data.
c5499eda
TH
218 * @param string $responsesummary a textual summary of the response, as described above.
219 * @param bool $checkbutton if simulate a click on the check button, else simulate save.
220 * This should only be used with behaviours that have a check button.
221 * @return array the simulated post data that can be passed to $quba->process_all_actions.
5c0654d7
SL
222 */
223 public function get_simulated_post_data_for_question_attempt(
c5499eda 224 question_attempt $qa, $responsesummary, $checkbutton) {
5c0654d7
SL
225
226 $question = $qa->get_question();
227 if (!$question instanceof question_with_responses) {
228 return [];
229 }
230
231 $postdata = [];
232 $postdata[$qa->get_control_field_name('sequencecheck')] = (string)$qa->get_sequence_check_count();
233 $postdata[$qa->get_flag_field_name()] = (string)(int)$qa->is_flagged();
234
235 $response = $question->un_summarise_response($responsesummary);
236 foreach ($response as $name => $value) {
237 $postdata[$qa->get_qt_field_name($name)] = (string)$value;
238 }
239
c5499eda
TH
240 // TODO handle behaviour variables better than this.
241 if ($checkbutton) {
242 $postdata[$qa->get_behaviour_field_name('submit')] = 1;
243 }
5c0654d7
SL
244
245 return $postdata;
246 }
ba203de1 247}