weekly release 4.0dev
[moodle.git] / question / tests / generator / lib.php
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/>.
17 defined('MOODLE_INTERNAL') || die();
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 {
29     /**
30      * @var number of created instances
31      */
32     protected $categorycount = 0;
34     public function reset() {
35         $this->categorycount = 0;
36     }
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;
46         $this->categorycount++;
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         );
57         $record = $this->datagenerator->combine_defaults_and_record($defaults, $record);
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     }
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');
85         $fromform = test_question_maker::get_question_form_data($qtype, $which);
86         $fromform = (object) $this->datagenerator->combine_defaults_and_record(
87                 (array) $fromform, $overrides);
89         $question = new stdClass();
90         $question->category  = $fromform->category;
91         $question->qtype     = $qtype;
92         $question->createdby = 0;
93         $question->idnumber = null;
95         return $this->update_question($question, $which, $overrides);
96     }
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     }
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');
122         $qtype = $question->qtype;
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);
130         $question = question_bank::get_qtype($qtype)->save_question($question, $fromform);
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         }
139         return $question;
140     }
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         ]);
157         switch ($type) {
158             case 'category':
159                 $context = context_coursecat::instance($category->id);
160                 break;
162             case 'system':
163                 $context = context_system::instance();
164                 break;
166             default:
167                 $context = context_course::instance($course->id);
168                 break;
169         }
171         $qcat = $this->create_question_category(['contextid' => $context->id]);
173         $questions = array(
174                 $this->create_question('shortanswer', null, ['category' => $qcat->id]),
175                 $this->create_question('shortanswer', null, ['category' => $qcat->id]),
176         );
178         return array($category, $course, $qcat, $questions);
179     }
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.
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.
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(
198             question_usage_by_activity $quba, array $responses, $checkbutton) {
199         $postdata = [];
201         foreach ($responses as $slot => $responsesummary) {
202             $postdata += $this->get_simulated_post_data_for_question_attempt(
203                     $quba->get_question_attempt($slot), $responsesummary, $checkbutton);
204         }
206         return $postdata;
207     }
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.
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.
222      */
223     public function get_simulated_post_data_for_question_attempt(
224             question_attempt $qa, $responsesummary, $checkbutton) {
226         $question = $qa->get_question();
227         if (!$question instanceof question_with_responses) {
228             return [];
229         }
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();
235         $response = $question->un_summarise_response($responsesummary);
236         foreach ($response as $name => $value) {
237             $postdata[$qa->get_qt_field_name($name)] = (string)$value;
238         }
240         // TODO handle behaviour variables better than this.
241         if ($checkbutton) {
242             $postdata[$qa->get_behaviour_field_name('submit')] = 1;
243         }
245         return $postdata;
246     }