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,
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}
253