Commit | Line | Data |
---|---|---|
da6bb0c5 JP |
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 attempt walk through using data from csv file. | |
19 | * | |
20 | * @package mod_quiz | |
21 | * @category phpunit | |
22 | * @copyright 2013 The Open University | |
23 | * @author Jamie Pratt <me@jamiep.org> | |
24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
25 | */ | |
26 | ||
27 | defined('MOODLE_INTERNAL') || die(); | |
28 | ||
29 | global $CFG; | |
30 | require_once($CFG->dirroot . '/mod/quiz/editlib.php'); | |
31 | require_once($CFG->dirroot . '/mod/quiz/locallib.php'); | |
32 | ||
33 | /** | |
34 | * Quiz attempt walk through using data from csv file. | |
35 | * | |
36 | * @package mod_quiz | |
37 | * @category phpunit | |
38 | * @copyright 2013 The Open University | |
39 | * @author Jamie Pratt <me@jamiep.org> | |
40 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
41 | */ | |
42 | class mod_quiz_attempt_walkthrough_from_csv_testcase extends advanced_testcase { | |
43 | ||
3652dddd JP |
44 | protected $files = array('questions', 'steps', 'results'); |
45 | ||
da6bb0c5 JP |
46 | /** |
47 | * @var stdClass the quiz record we create. | |
48 | */ | |
49 | protected $quiz; | |
50 | ||
51 | /** | |
52 | * @var array with slot no => question name => questionid. Question ids of questions created in the same category as random q. | |
53 | */ | |
54 | protected $randqids; | |
55 | ||
764f6153 | 56 | public function create_quiz($quizsettings, $qs) { |
da6bb0c5 JP |
57 | global $SITE, $DB; |
58 | $this->setAdminUser(); | |
59 | ||
60 | $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); | |
61 | $slots = array(); | |
62 | $qidsbycat = array(); | |
63 | $sumofgrades = 0; | |
64 | for ($rowno = 0; $rowno < $qs->getRowCount(); $rowno++) { | |
65 | $q = $this->explode_dot_separated_keys_to_make_subindexs($qs->getRow($rowno)); | |
66 | ||
67 | $catname = array('name' => $q['cat']); | |
68 | if (!$cat = $DB->get_record('question_categories', array('name' => $q['cat']))) { | |
69 | $cat = $questiongenerator->create_question_category($catname); | |
70 | } | |
71 | $q['catid'] = $cat->id; | |
72 | foreach (array('which' => null, 'overrides' => array()) as $key => $default) { | |
73 | if (empty($q[$key])) { | |
74 | $q[$key] = $default; | |
75 | } | |
76 | } | |
77 | ||
78 | if ($q['type'] !== 'random') { | |
79 | // Don't actually create random questions here. | |
80 | $overrides = array('category' => $cat->id, 'defaultmark' => $q['mark']) + $q['overrides']; | |
81 | $question = $questiongenerator->create_question($q['type'], $q['which'], $overrides); | |
82 | $q['id'] = $question->id; | |
83 | ||
84 | if (!isset($qidsbycat[$q['cat']])) { | |
85 | $qidsbycat[$q['cat']] = array(); | |
86 | } | |
87 | if (!empty($q['which'])) { | |
88 | $name = $q['type'].'_'.$q['which']; | |
89 | } else { | |
90 | $name = $q['type']; | |
91 | } | |
92 | $qidsbycat[$q['catid']][$name] = $q['id']; | |
93 | } | |
94 | if (!empty($q['slot'])) { | |
95 | $slots[$q['slot']] = $q; | |
96 | $sumofgrades += $q['mark']; | |
97 | } | |
98 | } | |
99 | ||
100 | ksort($slots); | |
101 | ||
102 | // Make a quiz. | |
103 | $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); | |
764f6153 JP |
104 | |
105 | // Settings from param override defaults. | |
106 | $aggregratedsettings = $quizsettings + array('course'=>$SITE->id, | |
107 | 'questionsperpage' => 0, | |
108 | 'grade' => 100.0, | |
109 | 'sumgrades' => $sumofgrades); | |
110 | ||
111 | $this->quiz = $quizgenerator->create_instance($aggregratedsettings); | |
da6bb0c5 JP |
112 | |
113 | $this->randqids = array(); | |
114 | foreach ($slots as $slotno => $slotquestion) { | |
115 | if ($slotquestion['type'] !== 'random') { | |
116 | quiz_add_quiz_question($slotquestion['id'], $this->quiz, 0); | |
117 | // Setting default mark above does not affect the grade for multi-answer question type (and maybe others??). | |
118 | // Set the mark again just to be sure. | |
119 | quiz_update_question_instance($slotquestion['mark'], $slotquestion['id'], $this->quiz); | |
120 | } else { | |
121 | quiz_add_random_questions($this->quiz, 0, $slotquestion['catid'], 1, 0); | |
122 | $this->randqids[$slotno] = $qidsbycat[$slotquestion['catid']]; | |
123 | } | |
124 | } | |
125 | } | |
126 | ||
da6bb0c5 JP |
127 | /** |
128 | * Get full path of CSV file. | |
129 | * | |
130 | * @param string $setname | |
131 | * @param string $test | |
132 | * @return string full path of file. | |
133 | */ | |
134 | protected function get_full_path_of_csv_file($setname, $test) { | |
135 | return __DIR__."/fixtures/{$setname}{$test}.csv"; | |
136 | } | |
137 | ||
138 | /** | |
139 | * Load dataset from CSV file "{$setname}{$test}.csv". | |
140 | * | |
141 | * @param string $setname | |
142 | * @param string $test | |
143 | * @return \PHPUnit_Extensions_Database_DataSet_ITable | |
144 | */ | |
764f6153 | 145 | protected function load_csv_data_file($setname, $test='') { |
da6bb0c5 JP |
146 | $files = array($setname => $this->get_full_path_of_csv_file($setname, $test)); |
147 | return $this->createCsvDataSet($files)->getTable($setname); | |
148 | } | |
149 | ||
e39a2faa JP |
150 | /** |
151 | * Break down row of csv data into sub arrays, according to column names. | |
152 | * | |
153 | * @param array $row from csv file with field names with parts separate by '.'. | |
154 | * @return array the row with each part of the field name following a '.' being a separate sub array's index. | |
155 | */ | |
156 | protected function explode_dot_separated_keys_to_make_subindexs(array $row) { | |
157 | $parts = array(); | |
158 | foreach ($row as $columnkey => $value) { | |
159 | $newkeys = explode('.', trim($columnkey)); | |
160 | $placetoputvalue =& $parts; | |
161 | foreach ($newkeys as $newkeydepth => $newkey) { | |
162 | if ($newkeydepth + 1 === count($newkeys)) { | |
163 | $placetoputvalue[$newkey] = $value; | |
164 | } else { | |
165 | // Going deeper down. | |
166 | if (!isset($placetoputvalue[$newkey])) { | |
167 | $placetoputvalue[$newkey] = array(); | |
168 | } | |
169 | $placetoputvalue =& $placetoputvalue[$newkey]; | |
170 | } | |
171 | } | |
172 | } | |
173 | return $parts; | |
174 | } | |
175 | ||
3652dddd JP |
176 | /** |
177 | * Data provider method for test_walkthrough_from_csv. Called by PHPUnit. | |
178 | * | |
764f6153 | 179 | * @return array One array element for each run of the test. Each element contains an array with the params for |
3652dddd JP |
180 | * test_walkthrough_from_csv. |
181 | */ | |
182 | public function get_data_for_walkthrough() { | |
764f6153 | 183 | $quizzes = $this->load_csv_data_file('quizzes'); |
3652dddd | 184 | $datasets = array(); |
764f6153 JP |
185 | for ($rowno = 0; $rowno < $quizzes->getRowCount(); $rowno++) { |
186 | $quizsettings = $quizzes->getRow($rowno); | |
3652dddd JP |
187 | $dataset = array(); |
188 | foreach ($this->files as $file) { | |
764f6153 | 189 | $dataset[$file] = $this->load_csv_data_file($file, $quizsettings['testnumber']); |
3652dddd | 190 | } |
764f6153 | 191 | $datasets[] = array($quizsettings, $dataset); |
3652dddd JP |
192 | } |
193 | return $datasets; | |
194 | } | |
195 | ||
da6bb0c5 JP |
196 | /** |
197 | * Create a quiz add questions to it, walk through quiz attempts and then check results. | |
198 | * | |
764f6153 JP |
199 | * @param $quizsettings array of settings read from csv file quizzes.csv |
200 | * @param $csvdata \PHPUnit_Extensions_Database_DataSet_ITable[] of data read from csv file "questionsXX.csv", | |
3652dddd | 201 | * "stepsXX.csv" and "resultsXX.csv". |
da6bb0c5 JP |
202 | * @dataProvider get_data_for_walkthrough |
203 | */ | |
764f6153 | 204 | public function test_walkthrough_from_csv($quizsettings, $csvdata) { |
3652dddd JP |
205 | |
206 | // CSV data files for these tests were generated using : | |
207 | // https://github.com/jamiepratt/moodle-quiz-tools/tree/master/responsegenerator | |
208 | ||
da6bb0c5 JP |
209 | $this->resetAfterTest(true); |
210 | question_bank::get_qtype('random')->clear_caches_before_testing(); | |
211 | ||
764f6153 | 212 | $this->create_quiz($quizsettings, $csvdata['questions']); |
3652dddd JP |
213 | |
214 | $attemptids = $this->walkthrough_attempts($csvdata['steps']); | |
da6bb0c5 | 215 | |
3652dddd JP |
216 | $this->check_attempts_results($csvdata['results'], $attemptids); |
217 | } | |
218 | ||
219 | /** | |
220 | * @param $steps PHPUnit_Extensions_Database_DataSet_ITable the step data from the csv file. | |
221 | * @return array attempt no as in csv file => the id of the quiz_attempt as stored in the db. | |
222 | */ | |
223 | protected function walkthrough_attempts($steps) { | |
224 | global $DB; | |
e39a2faa | 225 | $attemptids = array(); |
da6bb0c5 JP |
226 | for ($rowno = 0; $rowno < $steps->getRowCount(); $rowno++) { |
227 | ||
228 | $step = $this->explode_dot_separated_keys_to_make_subindexs($steps->getRow($rowno)); | |
229 | // Find existing user or make a new user to do the quiz. | |
230 | $username = array('firstname' => $step['firstname'], | |
3652dddd | 231 | 'lastname' => $step['lastname']); |
da6bb0c5 JP |
232 | |
233 | if (!$user = $DB->get_record('user', $username)) { | |
234 | $user = $this->getDataGenerator()->create_user($username); | |
235 | } | |
764f6153 JP |
236 | |
237 | if (!isset($attemptids[$step['quizattempt']])) { | |
238 | // Start the attempt. | |
239 | $quizobj = quiz::create($this->quiz->id, $user->id); | |
240 | $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); | |
241 | $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); | |
242 | ||
243 | $prevattempts = quiz_get_user_attempts($this->quiz->id, $user->id, 'all', true); | |
244 | $attemptnumber = count($prevattempts) + 1; | |
245 | $timenow = time(); | |
246 | $attempt = quiz_create_attempt($quizobj, $attemptnumber, false, $timenow, false, $user->id); | |
247 | // Select variant and / or random sub question. | |
248 | if (!isset($step['variants'])) { | |
249 | $step['variants'] = array(); | |
da6bb0c5 | 250 | } |
764f6153 JP |
251 | if (isset($step['randqs'])) { |
252 | // Replace 'names' with ids. | |
253 | foreach ($step['randqs'] as $slotno => $randqname) { | |
254 | $step['randqs'][$slotno] = $this->randqids[$slotno][$randqname]; | |
255 | } | |
256 | } else { | |
257 | $step['randqs'] = array(); | |
258 | } | |
259 | ||
260 | quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timenow, $step['randqs'], $step['variants']); | |
261 | quiz_attempt_save_started($quizobj, $quba, $attempt); | |
262 | $attemptid = $attemptids[$step['quizattempt']] = $attempt->id; | |
da6bb0c5 | 263 | } else { |
764f6153 | 264 | $attemptid = $attemptids[$step['quizattempt']]; |
da6bb0c5 | 265 | } |
da6bb0c5 | 266 | |
e39a2faa | 267 | |
da6bb0c5 | 268 | // Process some responses from the student. |
764f6153 | 269 | $attemptobj = quiz_attempt::create($attemptid); |
da6bb0c5 JP |
270 | $attemptobj->process_submitted_actions($timenow, false, $step['responses']); |
271 | ||
272 | // Finish the attempt. | |
764f6153 JP |
273 | if (!isset($step['finished']) || ($step['finished'] == 1)) { |
274 | $attemptobj = quiz_attempt::create($attemptid); | |
275 | $attemptobj->process_finish($timenow, false); | |
276 | } | |
e39a2faa | 277 | } |
3652dddd JP |
278 | return $attemptids; |
279 | } | |
e39a2faa | 280 | |
3652dddd JP |
281 | /** |
282 | * @param $results PHPUnit_Extensions_Database_DataSet_ITable the results data from the csv file. | |
283 | * @param $attemptids array attempt no as in csv file => the id of the quiz_attempt as stored in the db. | |
284 | */ | |
285 | protected function check_attempts_results($results, $attemptids) { | |
e39a2faa JP |
286 | for ($rowno = 0; $rowno < $results->getRowCount(); $rowno++) { |
287 | $result = $this->explode_dot_separated_keys_to_make_subindexs($results->getRow($rowno)); | |
da6bb0c5 | 288 | // Re-load quiz attempt data. |
e39a2faa JP |
289 | $attemptobj = quiz_attempt::create($attemptids[$result['quizattempt']]); |
290 | $this->check_attempt_results($result, $attemptobj); | |
da6bb0c5 JP |
291 | } |
292 | } | |
293 | ||
294 | /** | |
e39a2faa | 295 | * Check that attempt results are as specified in $result. |
da6bb0c5 | 296 | * |
e39a2faa JP |
297 | * @param array $result row of data read from csv file. |
298 | * @param quiz_attempt $attemptobj the attempt object loaded from db. | |
299 | * @throws coding_exception | |
da6bb0c5 | 300 | */ |
e39a2faa JP |
301 | protected function check_attempt_results($result, $attemptobj) { |
302 | foreach ($result as $fieldname => $value) { | |
764f6153 JP |
303 | if ($value === '!NULL!') { |
304 | $value = null; | |
305 | } | |
e39a2faa JP |
306 | switch ($fieldname) { |
307 | case 'quizattempt' : | |
308 | break; | |
309 | case 'attemptnumber' : | |
310 | $this->assertEquals($value, $attemptobj->get_attempt_number()); | |
311 | break; | |
312 | case 'slots' : | |
313 | foreach ($value as $slotno => $slottests) { | |
314 | foreach ($slottests as $slotfieldname => $slotvalue) { | |
315 | switch ($slotfieldname) { | |
316 | case 'mark' : | |
317 | $this->assertEquals(round($slotvalue, 2), $attemptobj->get_question_mark($slotno), | |
318 | "Mark for slot $slotno of attempt {$result['quizattempt']}."); | |
319 | break; | |
320 | default : | |
321 | throw new coding_exception('Unknown slots sub field column in csv file ' | |
3652dddd | 322 | .s($slotfieldname)); |
e39a2faa JP |
323 | } |
324 | } | |
da6bb0c5 | 325 | } |
e39a2faa JP |
326 | break; |
327 | case 'finished' : | |
764f6153 | 328 | $this->assertEquals((bool)$value, $attemptobj->is_finished()); |
e39a2faa JP |
329 | break; |
330 | case 'summarks' : | |
331 | $this->assertEquals($value, $attemptobj->get_sum_marks(), "Sum of marks of attempt {$result['quizattempt']}."); | |
332 | break; | |
333 | case 'quizgrade' : | |
334 | // Check quiz grades. | |
335 | $grades = quiz_get_user_grades($attemptobj->get_quiz(), $attemptobj->get_userid()); | |
336 | $grade = array_shift($grades); | |
337 | $this->assertEquals($value, $grade->rawgrade, "Quiz grade for attempt {$result['quizattempt']}."); | |
338 | break; | |
339 | case 'gradebookgrade' : | |
340 | // Check grade book. | |
341 | $gradebookgrades = grade_get_grades($attemptobj->get_courseid(), | |
342 | 'mod', 'quiz', | |
343 | $attemptobj->get_quizid(), | |
344 | $attemptobj->get_userid()); | |
345 | $gradebookitem = array_shift($gradebookgrades->items); | |
346 | $gradebookgrade = array_shift($gradebookitem->grades); | |
347 | $this->assertEquals($value, $gradebookgrade->grade, "Gradebook grade for attempt {$result['quizattempt']}."); | |
348 | break; | |
349 | default : | |
350 | throw new coding_exception('Unknown column in csv file '.s($fieldname)); | |
da6bb0c5 JP |
351 | } |
352 | } | |
da6bb0c5 JP |
353 | } |
354 | } |